From ecb657545ec49a23fa70e48e6f110c322e90d927 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 13 May 2022 14:23:23 +1000 Subject: [PATCH 001/133] sample of emoji reacts bar --- .../Context Menu/ContextMenuVC.swift | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 376022914..9bf1733ba 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -8,6 +8,16 @@ final class ContextMenuVC : UIViewController { // MARK: UI Components private lazy var blurView = 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 menuView: UIView = { let result = UIView() @@ -75,6 +85,27 @@ final class ContextMenuVC : UIViewController { } else { 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) + let emojiLabels = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€", "๐Ÿ˜ƒ"].map { emoji -> UILabel in + let label = UILabel() + label.text = emoji + label.font = .systemFont(ofSize: Values.veryLargeFontSize) + return label + } + let emojiBarStackView = UIStackView(arrangedSubviews: emojiLabels) + emojiBarStackView.axis = .horizontal + emojiBarStackView.spacing = Values.smallSpacing + emojiBarStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.mediumSpacing, bottom: 0, right: Values.mediumSpacing) + emojiBarStackView.isLayoutMarginsRelativeArrangement = true + emojiBar.addSubview(emojiBarStackView) + emojiBarStackView.pin(to: emojiBar) + view.addSubview(emojiBar) // Menu let menuBackgroundView = UIView() menuBackgroundView.backgroundColor = Colors.receivedMessageBackground @@ -88,17 +119,24 @@ final class ContextMenuVC : UIViewController { menuView.addSubview(menuStackView) menuStackView.pin(to: menuView) view.addSubview(menuView) + // Constrains let menuHeight = CGFloat(actionViews.count) * ContextMenuVC.actionViewHeight let spacing = Values.smallSpacing let margin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing) if frame.maxY + spacing + menuHeight > UIScreen.main.bounds.height - margin { menuView.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) + emojiBar.pin(.top, to: .bottom, of: snapshot, withInset: spacing) } else { menuView.pin(.top, to: .bottom, of: snapshot, withInset: spacing) + emojiBar.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) } switch viewItem.interaction.interactionType() { - case .outgoingMessage: menuView.pin(.right, to: .right, of: snapshot) - case .incomingMessage: menuView.pin(.left, to: .left, of: snapshot) + case .outgoingMessage: + menuView.pin(.right, to: .right, of: snapshot) + emojiBar.pin(.right, to: .right, of: snapshot) + case .incomingMessage: + menuView.pin(.left, to: .left, of: snapshot) + emojiBar.pin(.left, to: .left, of: snapshot) default: break // Should never occur } // Tap gesture @@ -118,6 +156,7 @@ final class ContextMenuVC : UIViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() menuView.layer.shadowPath = UIBezierPath(roundedRect: menuView.bounds, cornerRadius: ContextMenuVC.menuCornerRadius).cgPath + emojiBar.layer.shadowPath = UIBezierPath(roundedRect: emojiBar.bounds, cornerRadius: ContextMenuVC.actionViewHeight / 2).cgPath } // MARK: Interaction From 631424018945410d517e3ecdfe6a654b1a2f987e Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 16 May 2022 13:56:39 +1000 Subject: [PATCH 002/133] plus button for more emoji --- Session.xcodeproj/project.pbxproj | 4 + .../ContextMenuVC+EmojiReactsView.swift | 89 +++++++++++++++++++ .../Context Menu/ContextMenuVC.swift | 27 ++++-- SessionUIKit/Style Guide/Colors.swift | 1 + .../Contents.json | 38 ++++++++ 5 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift create mode 100644 SessionUIKit/Style Guide/Colors.xcassets/session_emoji_plus_button_background.colorset/Contents.json diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index f1a416dd1..9241aa40b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -176,6 +176,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 */; }; @@ -1166,6 +1167,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 = ""; }; @@ -2616,6 +2618,7 @@ C328253F25CA55880062D0A7 /* ContextMenuVC.swift */, C328254825CA60E60062D0A7 /* ContextMenuVC+Action.swift */, C328255125CA64470062D0A7 /* ContextMenuVC+ActionView.swift */, + 7BFA8AE22831D0D4001876F3 /* ContextMenuVC+EmojiReactsView.swift */, ); path = "Context Menu"; sourceTree = ""; @@ -4853,6 +4856,7 @@ 452EC6DF205E9E30000E787C /* MediaGalleryViewController.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 */, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift new file mode 100644 index 000000000..d46573888 --- /dev/null +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -0,0 +1,89 @@ + +extension ContextMenuVC { + + final class EmojiReactsView: UIView { + private let emoji: String + private let dismiss: () -> Void + + // MARK: Settings + private static let size: CGFloat = 40 + + // MARK: Lifecycle + init(for emoji: String, dismiss: @escaping () -> Void) { + self.emoji = emoji + 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.emoji + 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() { + dismiss() + } + } + + final class EmojiPlusButton: UIView { + private let dismiss: () -> Void + + // MARK: Settings + public static let size: CGFloat = 28 + private let iconSize: CGFloat = 14 + + // MARK: Lifecycle + init(dismiss: @escaping () -> Void) { + 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").withTint(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() + } + } + +} diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 9bf1733ba..d1d7a2dd2 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -18,6 +18,15 @@ final class ContextMenuVC : UIViewController { result.set(.height, to: ContextMenuVC.actionViewHeight) return result }() + + private lazy var emojiPlusButton: EmojiPlusButton = { + let result = EmojiPlusButton(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() @@ -92,19 +101,21 @@ final class ContextMenuVC : UIViewController { emojiBarBackgroundView.layer.masksToBounds = true emojiBar.addSubview(emojiBarBackgroundView) emojiBarBackgroundView.pin(to: emojiBar) - let emojiLabels = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€", "๐Ÿ˜ƒ"].map { emoji -> UILabel in - let label = UILabel() - label.text = emoji - label.font = .systemFont(ofSize: Values.veryLargeFontSize) - return label - } + + emojiBar.addSubview(emojiPlusButton) + emojiPlusButton.pin(.right, to: .right, of: emojiBar, withInset: -Values.smallSpacing) + emojiPlusButton.center(.vertical, in: emojiBar) + + let emojiLabels = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"].map { EmojiReactsView(for: $0, dismiss: snDismiss) } let emojiBarStackView = UIStackView(arrangedSubviews: emojiLabels) emojiBarStackView.axis = .horizontal emojiBarStackView.spacing = Values.smallSpacing - emojiBarStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.mediumSpacing, bottom: 0, right: Values.mediumSpacing) + emojiBarStackView.layoutMargins = UIEdgeInsets(top: 0, left: Values.smallSpacing, bottom: 0, right: Values.smallSpacing) emojiBarStackView.isLayoutMarginsRelativeArrangement = true emojiBar.addSubview(emojiBarStackView) - emojiBarStackView.pin(to: emojiBar) + emojiBarStackView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: emojiBar) + emojiBarStackView.pin(.right, to: .left, of: emojiPlusButton) + view.addSubview(emojiBar) // Menu let menuBackgroundView = UIView() 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 + } +} From fe1b8ebb9344f38e0535d9ddd45b2747ad564267 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 16 May 2022 14:43:54 +1000 Subject: [PATCH 003/133] fix context menu shows wrong colour in dark mode --- .../Conversations/Context Menu/ContextMenuVC+ActionView.swift | 3 ++- .../Context Menu/ContextMenuVC+EmojiReactsView.swift | 3 ++- Session/Conversations/ConversationVC+Interaction.swift | 3 +++ Session/Meta/Session-Info.plist | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift index 0f0e99ffc..20e511aec 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+ActionView.swift @@ -28,7 +28,8 @@ extension ContextMenuVC { private func setUpViewHierarchy() { // Icon let iconSize = ActionView.iconSize - let iconImageView = UIImageView(image: action.icon.resizedImage(to: CGSize(width: iconSize, height: iconSize))!.withTint(Colors.text)) + let iconImageView = UIImageView(image: action.icon.resizedImage(to: CGSize(width: iconSize, height: iconSize))!.withRenderingMode(.alwaysTemplate)) + iconImageView.tintColor = Colors.text let iconImageViewSize = ActionView.iconImageViewSize iconImageView.set(.width, to: iconImageViewSize) iconImageView.set(.height, to: iconImageViewSize) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift index d46573888..691d9a0e9 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -66,7 +66,8 @@ extension ContextMenuVC { private func setUpViewHierarchy() { // Icon image - let iconImageView = UIImageView(image: #imageLiteral(resourceName: "ic_plus_24").withTint(Colors.text)) + 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 diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 19fda911b..bffe01694 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -530,6 +530,9 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc window.rootViewController = contextMenuVC window.makeKeyAndVisible() window.backgroundColor = .clear + if #available(iOS 13.0, *) { + window.overrideUserInterfaceStyle = isDarkMode ? .dark : .light + } } func handleViewItemTapped(_ viewItem: ConversationViewItem, gestureRecognizer: UITapGestureRecognizer) { diff --git a/Session/Meta/Session-Info.plist b/Session/Meta/Session-Info.plist index 54e23fb8d..3bb826685 100644 --- a/Session/Meta/Session-Info.plist +++ b/Session/Meta/Session-Info.plist @@ -90,7 +90,7 @@ NSContactsUsageDescription Signal uses your contacts to find users you know. We do not store your contacts on the server. NSFaceIDUsageDescription - Session's Screen Lock feature uses Face ID. + Session's Screen Lock feature uses Face ID. NSHumanReadableCopyright com.loki-project.loki-messenger NSMicrophoneUsageDescription From 965c1c48e5928cbe9fa52b617d64844241cc2501 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 16 May 2022 17:06:06 +1000 Subject: [PATCH 004/133] add action to emoji reacts --- .../Context Menu/ContextMenuVC+Action.swift | 2 ++ .../ContextMenuVC+EmojiReactsView.swift | 10 +++++-- .../Context Menu/ContextMenuVC.swift | 8 +++-- .../ConversationVC+Interaction.swift | 10 +++++++ .../General/SNUserDefaults.swift | 29 +++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index d7eaaae65..4b324c522 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -95,5 +95,7 @@ protocol ContextMenuActionDelegate : AnyObject { func save(_ viewItem: ConversationViewItem) func ban(_ viewItem: ConversationViewItem) func banAndDeleteAllMessages(_ viewItem: ConversationViewItem) + func react(_ viewItem: ConversationViewItem, with emoji: String) + func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) func contextMenuDismissed() } diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift index 691d9a0e9..83adfd966 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -4,14 +4,16 @@ extension ContextMenuVC { final class EmojiReactsView: UIView { private let emoji: String private let dismiss: () -> Void + private let work: () -> Void // MARK: Settings private static let size: CGFloat = 40 // MARK: Lifecycle - init(for emoji: String, dismiss: @escaping () -> Void) { + init(for emoji: String, dismiss: @escaping () -> Void, work: @escaping () -> Void) { self.emoji = emoji self.dismiss = dismiss + self.work = work super.init(frame: CGRect.zero) setUpViewHierarchy() } @@ -38,20 +40,23 @@ extension ContextMenuVC { // MARK: Interaction @objc private func handleTap() { + work() dismiss() } } final class EmojiPlusButton: UIView { private let dismiss: () -> Void + private let work: () -> Void // MARK: Settings public static let size: CGFloat = 28 private let iconSize: CGFloat = 14 // MARK: Lifecycle - init(dismiss: @escaping () -> Void) { + init(dismiss: @escaping () -> Void, work: @escaping () -> Void) { self.dismiss = dismiss + self.work = work super.init(frame: CGRect.zero) setUpViewHierarchy() } @@ -84,6 +89,7 @@ extension ContextMenuVC { // MARK: Interaction @objc private func handleTap() { dismiss() + work() } } diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index d1d7a2dd2..7aee8bdc5 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -20,7 +20,7 @@ final class ContextMenuVC : UIViewController { }() private lazy var emojiPlusButton: EmojiPlusButton = { - let result = EmojiPlusButton(dismiss: snDismiss) + let result = EmojiPlusButton(dismiss: snDismiss) { self.delegate?.showFullEmojiKeyboard(self.viewItem) } result.set(.width, to: EmojiPlusButton.size) result.set(.height, to: EmojiPlusButton.size) result.layer.cornerRadius = EmojiPlusButton.size / 2 @@ -106,7 +106,11 @@ final class ContextMenuVC : UIViewController { emojiPlusButton.pin(.right, to: .right, of: emojiBar, withInset: -Values.smallSpacing) emojiPlusButton.center(.vertical, in: emojiBar) - let emojiLabels = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"].map { EmojiReactsView(for: $0, dismiss: snDismiss) } + let emojiLabels = UserDefaults.standard.getRecentlyUsedEmojis().map { emoji -> EmojiReactsView in + EmojiReactsView(for: emoji, dismiss: snDismiss) { + self.delegate?.react(self.viewItem, with: emoji) + } + } let emojiBarStackView = UIStackView(arrangedSubviews: emojiLabels) emojiBarStackView.axis = .horizontal emojiBarStackView.spacing = Values.smallSpacing diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index bffe01694..eb1208b39 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -819,6 +819,16 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc presentAlert(alert) } + func react(_ viewItem: ConversationViewItem, with emoji: String) { + print("Ryan Test: \(emoji)") + // TODO: send emoji react message + UserDefaults.standard.addNewRecentlyUsedEmoji(emoji) + } + + func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) { + // TODO: to be implemented + } + func contextMenuDismissed() { recoverInputView() } diff --git a/SessionUtilitiesKit/General/SNUserDefaults.swift b/SessionUtilitiesKit/General/SNUserDefaults.swift index 351c64cfc..6288744db 100644 --- a/SessionUtilitiesKit/General/SNUserDefaults.swift +++ b/SessionUtilitiesKit/General/SNUserDefaults.swift @@ -33,6 +33,10 @@ public enum SNUserDefaults { public enum String : Swift.String { case deviceToken } + + public enum Array : Swift.String { + case recentlyUsedEmojis + } } public extension UserDefaults { @@ -61,4 +65,29 @@ public extension UserDefaults { get { return self.string(forKey: string.rawValue) } set { set(newValue, forKey: string.rawValue) } } + + subscript(array: SNUserDefaults.Array) -> [String] { + get { return self.stringArray(forKey: array.rawValue) ?? []} + set { set(newValue, forKey: array.rawValue) } + } + + func getRecentlyUsedEmojis() -> [String] { + let result = self[.recentlyUsedEmojis] + if result.isEmpty { + return ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"] + } + return result + } + + func addNewRecentlyUsedEmoji(_ emoji: String) { + var recentlyUsedEmojis = getRecentlyUsedEmojis() + if let index = recentlyUsedEmojis.firstIndex(of: emoji) { + recentlyUsedEmojis.remove(at: index) + } + if recentlyUsedEmojis.count >= 6 { + recentlyUsedEmojis.remove(at: 5) + } + recentlyUsedEmojis.insert(emoji, at: 0) + self[.recentlyUsedEmojis] = recentlyUsedEmojis + } } From 651b271ba07bc556868c696def2f1ab0d2a84952 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 17 May 2022 11:57:34 +1000 Subject: [PATCH 005/133] update protobuf --- .../Protos/Generated/SNProto.swift | 165 ++++++++++++++++++ .../Protos/Generated/SessionProtos.pb.swift | 148 ++++++++++++++++ .../Protos/SessionProtos.proto | 14 ++ 3 files changed, 327 insertions(+) diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 457848361..442d87c2e 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 { diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index 854fd1ad2..cc1bf5c28 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -821,6 +821,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 +1132,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. } @@ -2428,6 +2512,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..6df37aef9 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; From a20afe1c9796b50f55af558fe4bd0fbab2e32e68 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 17 May 2022 12:12:29 +1000 Subject: [PATCH 006/133] add reaction message --- Session.xcodeproj/project.pbxproj | 4 + .../VisibleMessage+Reaction.swift | 105 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 9241aa40b..b0f77c254 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ 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 */; }; + 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; 7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */; }; 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; }; @@ -1135,6 +1136,7 @@ 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 = ""; }; + 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 = ""; }; 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; @@ -2525,6 +2527,7 @@ isa = PBXGroup; children = ( C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */, + 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */, C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */, C3C2A7552553A3AB00C340D1 /* VisibleMessage+Quote.swift */, C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */, @@ -4728,6 +4731,7 @@ C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, 7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */, C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */, + 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */, C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */, C352A3892557876500338F3E /* JobQueue.swift in Sources */, C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */, diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift new file mode 100644 index 000000000..b1372fe7e --- /dev/null +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage+Reaction.swift @@ -0,0 +1,105 @@ + +public extension VisibleMessage { + + @objc(SNReaction) + class Reaction : NSObject, NSCoding { + public var timestamp: UInt64? + public var publicKey: String? + public var emoji: String? + public var kind: Kind? + + // MARK: Kind + public enum Kind : Int, CustomStringConvertible { + case react, remove + + static func fromProto(_ proto: SNProtoDataMessageReaction.SNProtoDataMessageReactionAction) -> Kind { + switch proto { + case .react: return .react + case .remove: return .remove + } + } + + func toProto() -> SNProtoDataMessageReaction.SNProtoDataMessageReactionAction { + switch self { + case .react: return .react + case .remove: return .remove + } + } + + public var description: String { + switch self { + case .react: return "react" + case .remove: return "remove" + } + } + } + + // MARK: Validation + public var isValid: Bool { timestamp != nil && publicKey != nil } + + // MARK: Initialization + public override init() { super.init() } + + internal init(timestamp: UInt64, publicKey: String, emoji: String?, kind: Kind?) { + self.timestamp = timestamp + self.publicKey = publicKey + self.emoji = emoji + self.kind = kind + } + + // MARK: Coding + public required init?(coder: NSCoder) { + if let timestamp = coder.decodeObject(forKey: "timestamp") as! UInt64? { self.timestamp = timestamp } + if let publicKey = coder.decodeObject(forKey: "authorId") as! String? { self.publicKey = publicKey } + if let emoji = coder.decodeObject(forKey: "emoji") as! String? { self.emoji = emoji } + if let rawKind = coder.decodeObject(forKey: "action") as! Int? { self.kind = Kind(rawValue: rawKind) } + } + + public func encode(with coder: NSCoder) { + coder.encode(timestamp, forKey: "timestamp") + coder.encode(publicKey, forKey: "authorId") + coder.encode(emoji, forKey: "emoji") + coder.encode(kind?.rawValue, forKey: "action") + } + + // MARK: Proto Conversion + public static func fromProto(_ proto: SNProtoDataMessageReaction) -> Reaction? { + let timestamp = proto.id + let publicKey = proto.author + let emoji = proto.emoji + let kind = Kind.fromProto(proto.action) + return Reaction(timestamp: timestamp, publicKey: publicKey, emoji: emoji, kind: kind) + } + + public func toProto() -> SNProtoDataMessageReaction? { + preconditionFailure("Use toProto(using:) instead.") + } + + public func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoDataMessageReaction? { + guard let timestamp = timestamp, let publicKey = publicKey, let kind = kind else { + SNLog("Couldn't construct reaction proto from: \(self).") + return nil + } + let reactionProto = SNProtoDataMessageReaction.builder(id: timestamp, author: publicKey, action: kind.toProto()) + if let emoji = emoji { reactionProto.setEmoji(emoji) } + do { + return try reactionProto.build() + } catch { + SNLog("Couldn't construct quote proto from: \(self).") + return nil + } + } + + // MARK: Description + public override var description: String { + """ + Reaction( + timestamp: \(timestamp?.description ?? "null"), + publicKey: \(publicKey ?? "null"), + emoji: \(emoji ?? "null"), + kind: \(kind?.description ?? "null") + ) + """ + } + } +} From 692df74af6879dd8277c43bd7beeb6e0801ce177 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 18 May 2022 17:19:11 +1000 Subject: [PATCH 007/133] wip: emoji reacts message cell ui --- Session.xcodeproj/project.pbxproj | 24 +++++++++ .../ConversationVC+Interaction.swift | 17 ++++++- .../Content Views/ReactionContainerView.swift | 46 +++++++++++++++++ .../Content Views/ReactionView.swift | 49 +++++++++++++++++++ .../Message Cells/VisibleMessageCell.swift | 31 +++++++++++- .../Messages/Signal/TSMessage.h | 4 ++ .../Messages/Signal/TSMessage.m | 18 +++++++ .../Visible Messages/VisibleMessage.swift | 15 +++++- .../Protos/Generated/SNProto.swift | 17 +++++++ .../Protos/Generated/SessionProtos.pb.swift | 18 +++++++ .../Protos/SessionProtos.proto | 1 + .../ReactMessage+Conversion.swift | 24 +++++++++ .../Emoji Reacts/ReactMessage.swift | 39 +++++++++++++++ .../Utilities/OrderedDictionary.swift | 12 +++++ 14 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift create mode 100644 Session/Conversations/Message Cells/Content Views/ReactionView.swift create mode 100644 SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage+Conversion.swift create mode 100644 SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index b0f77c254..ae0c87b79 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -140,12 +140,16 @@ 7B251C3627D82D9E001A6284 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; + 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 */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; + 7B8D5FC728336093008324D9 /* ReactMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC628336093008324D9 /* ReactMessage.swift */; }; + 7B8D5FC9283369CC008324D9 /* ReactMessage+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC8283369CC008324D9 /* ReactMessage+Conversion.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; 7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */; }; 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; }; @@ -1131,12 +1135,16 @@ 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; 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 = ""; }; 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = ""; }; + 7B8D5FC628336093008324D9 /* ReactMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactMessage.swift; sourceTree = ""; }; + 7B8D5FC8283369CC008324D9 /* ReactMessage+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReactMessage+Conversion.swift"; sourceTree = ""; }; 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; 7B93D06C27CF175800811CB6 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; @@ -2083,6 +2091,15 @@ path = "Views & Modals"; sourceTree = ""; }; + 7B8D5FC528336071008324D9 /* Emoji Reacts */ = { + isa = PBXGroup; + children = ( + 7B8D5FC628336093008324D9 /* ReactMessage.swift */, + 7B8D5FC8283369CC008324D9 /* ReactMessage+Conversion.swift */, + ); + path = "Emoji Reacts"; + sourceTree = ""; + }; 7B93D06827CF173D00811CB6 /* Message Requests */ = { isa = PBXGroup; children = ( @@ -2222,6 +2239,8 @@ B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */, 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */, 7B7CB188270430D20079FF93 /* CallMessageView.swift */, + 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */, + 7B7037442834BCC0000DCF35 /* ReactionView.swift */, ); path = "Content Views"; sourceTree = ""; @@ -2559,6 +2578,7 @@ C300A5F02554B08500555489 /* Sending & Receiving */ = { isa = PBXGroup; children = ( + 7B8D5FC528336071008324D9 /* Emoji Reacts */, C3D9E3B52567685D0040E4F3 /* Attachments */, B8F5F61925EDE4B0003BF8D4 /* Data Extraction */, C32C5B01256DC054003C73A2 /* Expiration */, @@ -4715,6 +4735,7 @@ C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */, C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */, 7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */, + 7B8D5FC9283369CC008324D9 /* ReactMessage+Conversion.swift in Sources */, 7BD477AA27F15F24004E2822 /* OpenGroupServerIdLookup.swift in Sources */, C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */, C352A3932557883D00338F3E /* JobDelegate.swift in Sources */, @@ -4803,6 +4824,7 @@ C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */, C3A3A0EC256E1949004D228D /* OWSRecipientIdentity.m in Sources */, B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */, + 7B8D5FC728336093008324D9 /* ReactMessage.swift in Sources */, C32C5AB2256DBE8F003C73A2 /* TSMessage.m in Sources */, C3A3A0FE256E1A3C004D228D /* TSDatabaseSecondaryIndexes.m in Sources */, C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */, @@ -4929,6 +4951,7 @@ B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */, B8214A2B25D63EB9009C0F2A /* MessagesTableView.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 */, @@ -4963,6 +4986,7 @@ 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */, B85357C323A1BD1200AAF6CD /* SeedVC.swift in Sources */, 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, + 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */, B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */, 7B7CB190270FB2150079FF93 /* MiniCallView.swift in Sources */, B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index eb1208b39..1a1614631 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -821,8 +821,23 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc func react(_ viewItem: ConversationViewItem, with emoji: String) { print("Ryan Test: \(emoji)") - // TODO: send emoji react message UserDefaults.standard.addNewRecentlyUsedEmoji(emoji) + guard let message = viewItem.interaction as? TSMessage else { return } + var authorId = getUserHexEncodedPublicKey() + if let incomingMessage = message as? TSIncomingMessage { authorId = incomingMessage.authorId } + let reactMessage = ReactMessage(timestamp: message.timestamp, authorId: authorId, emoji: emoji) + reactMessage.sender = getUserHexEncodedPublicKey() + let thread = self.thread + let sentTimestamp: UInt64 = NSDate.millisecondTimestamp() + let visibleMessage = VisibleMessage() + visibleMessage.sentTimestamp = sentTimestamp + visibleMessage.reaction = .from(reactMessage) + visibleMessage.reaction?.kind = .react + Storage.write { transaction in + message.update(withReaction: reactMessage, transaction: transaction) + // TODO: send emoji react message +// MessageSender.send(<#T##message: Message##Message#>, in: thread, using: <#T##YapDatabaseReadWriteTransaction#>) + } } func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) { 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..f603174fb --- /dev/null +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -0,0 +1,46 @@ + +final class ReactionContainerView : UIView { + private lazy var containerView: UIStackView = { + let result = UIStackView() + result.axis = .vertical + result.spacing = Values.smallSpacing + return result + }() + + private var showingAllReactions = false + + // 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(containerView) + containerView.pin(to: self) + } + + public func update(_ reactions: [(String, Int)]) { + for subview in containerView.arrangedSubviews { + containerView.removeArrangedSubview(subview) + } + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = Values.smallSpacing + for reaction in reactions { + let reactionView = ReactionView(emoji: reaction.0, number: reaction.1) + stackView.addArrangedSubview(reactionView) + } + containerView.addArrangedSubview(stackView) + } +} + + 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..4f25b7925 --- /dev/null +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -0,0 +1,49 @@ +import UIKit + +final class ReactionView : UIView { + private let emoji: String + private let number: Int + + // MARK: Settings + private static let height: CGFloat = 22 + + // MARK: Lifecycle + init(emoji: String, number: Int) { + self.emoji = emoji + self.number = number + 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 = emoji + emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + + let numberLabel = UILabel() + numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.2f", Float(number) / 1000) + "k" + numberLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + numberLabel.textColor = Colors.text + + let stackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ]) + stackView.axis = .horizontal + stackView.spacing = Values.verySmallSpacing + 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: ReactionView.height) + backgroundColor = Colors.receivedMessageBackground + layer.cornerRadius = ReactionView.height / 2 + } +} diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index f247fcb47..f2caaeb6e 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -9,11 +9,16 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { 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 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: bubbleView, withInset: 0) private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) private lazy var messageStatusImageViewHeightConstraint = messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize) @@ -81,6 +86,8 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { private lazy var snContentView = UIView() + private lazy var reactionContainerView = ReactionContainerView() + internal lazy var messageStatusImageView: UIImageView = { let result = UIImageView() result.contentMode = .scaleAspectFit @@ -163,7 +170,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { 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) moderatorIconImageView.set(.height, to: 20) @@ -182,6 +188,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { // Content view bubbleView.addSubview(snContentView) snContentView.pin(to: bubbleView) + // Reaction view + addSubview(reactionContainerView) + reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.smallSpacing) + reactionContainerViewLeftConstraint.isActive = true // Message status image view addSubview(messageStatusImageView) messageStatusImageViewTopConstraint.isActive = true @@ -244,6 +254,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { updateBubbleViewCorners() // Content view populateContentView(for: viewItem, message: message) + // Reaction view + reactionContainerViewLeftConstraint.isActive = (direction == .incoming) + reactionContainerViewRightConstraint.isActive = (direction == .outgoing) + populateReaction(for: viewItem, message: message) // Date break headerViewTopConstraint.constant = shouldInsetHeader ? Values.mediumSpacing : 1 headerView.subviews.forEach { $0.removeFromSuperview() } @@ -438,6 +452,21 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } + private func populateReaction(for viewItem: ConversationViewItem, message: TSMessage) { + let reactions: OrderedDictionary = OrderedDictionary() + for reaction in message.reactions { + if let reactMessage = reaction as? ReactMessage, let emoji = reactMessage.emoji { + if let number = reactions.value(forKey: emoji) { + reactions.replace(key: emoji, value: number + 1) + } else { + reactions.append(key: emoji, value: 1) + } + } + } + print("Ryan Test: \(reactions.orderedKeys)") + reactionContainerView.update(reactions.orderedItems) + } + override func layoutSubviews() { super.layoutSubviews() updateBubbleViewCorners() diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.h b/SessionMessagingKit/Messages/Signal/TSMessage.h index 8739181c0..dc58ab759 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.h +++ b/SessionMessagingKit/Messages/Signal/TSMessage.h @@ -20,6 +20,7 @@ typedef NS_ENUM(NSUInteger, TSMessageDirection) { @class TSAttachment; @class TSAttachmentStream; @class TSQuotedMessage; +@class SNReactMessage; @class YapDatabaseReadWriteTransaction; extern const NSUInteger kOversizeTextMessageSizeThreshold; @@ -41,6 +42,7 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold; @property (nonatomic, nullable) NSString *serverHash; @property (nonatomic) BOOL isDeleted; @property (nonatomic) BOOL isCallMessage; +@property (nonatomic, readonly) NSMutableArray *reactions; - (instancetype)initInteractionWithTimestamp:(uint64_t)timestamp inThread:(TSThread *)thread NS_UNAVAILABLE; @@ -88,6 +90,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold; - (void)updateCallMessageWithNewBody:(NSString *)newBody transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)updateWithReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.m b/SessionMessagingKit/Messages/Signal/TSMessage.m index 0584aaf74..db112d305 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSMessage.m @@ -29,6 +29,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; @property (nonatomic, nullable) NSString *body; @property (nonatomic) uint32_t expiresInSeconds; @property (nonatomic) uint64_t expireStartedAt; +@property (nonatomic) NSMutableArray *reactions; /** * The version of the model class's schema last used to serialize this model. Use this to manage data migrations during @@ -88,6 +89,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; _serverHash = serverHash; _isDeleted = false; _isCallMessage = false; + _reactions = [NSMutableArray new]; return self; } @@ -137,6 +139,10 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; if (!_attachmentIds) { _attachmentIds = [NSMutableArray new]; } + + if (!_reactions) { + _reactions = [NSMutableArray new]; + } _schemaVersion = OWSMessageSchemaVersion; @@ -451,6 +457,18 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; }]; } +- (void)updateWithReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if ([self isKindOfClass:[TSIncomingMessage class]] || [self isKindOfClass:[TSOutgoingMessage class]]) { + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSMessage *message) { + if (![message.reactions containsObject:reaction]) { + [message.reactions addObject:reaction]; + } + }]; + } +} + @end NS_ASSUME_NONNULL_END diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index 21c1a41be..b35186428 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -13,6 +13,7 @@ public final class VisibleMessage : Message { @objc public var contact: Contact? @objc public var profile: Profile? @objc public var openGroupInvitation: OpenGroupInvitation? + @objc public var reaction: Reaction? public override var isSelfSendValid: Bool { true } @@ -24,6 +25,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 } @@ -39,6 +41,7 @@ public final class VisibleMessage : Message { // TODO: Contact if let profile = coder.decodeObject(forKey: "profile") as! Profile? { self.profile = profile } if let openGroupInvitation = coder.decodeObject(forKey: "openGroupInvitation") as! OpenGroupInvitation? { self.openGroupInvitation = openGroupInvitation } + if let reaction = coder.decodeObject(forKey: "reaction") as! Reaction? { self.reaction = reaction } } public override func encode(with coder: NSCoder) { @@ -51,6 +54,7 @@ public final class VisibleMessage : Message { // TODO: Contact coder.encode(profile, forKey: "profile") coder.encode(openGroupInvitation, forKey: "openGroupInvitation") + coder.encode(reaction, forKey: "reaction") } // MARK: Proto Conversion @@ -65,6 +69,8 @@ public final class VisibleMessage : Message { if let profile = Profile.fromProto(dataMessage) { result.profile = profile } if let openGroupInvitationProto = dataMessage.openGroupInvitation, let openGroupInvitation = OpenGroupInvitation.fromProto(openGroupInvitationProto) { result.openGroupInvitation = openGroupInvitation } + if let reactionProto = dataMessage.reaction, + let reaction = Reaction.fromProto(reactionProto) { result.reaction = reaction } result.syncTarget = dataMessage.syncTarget return result } @@ -103,6 +109,10 @@ public final class VisibleMessage : Message { // TODO: Contact // Open group invitation if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) } + // Emoji react + if let reaction = reaction, let reactionProto = reaction.toProto() { + dataMessage.setReaction(reactionProto) + } // Group context do { try setGroupContextIfNeeded(on: dataMessage, using: transaction) @@ -133,8 +143,9 @@ public final class VisibleMessage : Message { quote: \(quote?.description ?? "null"), linkPreview: \(linkPreview?.description ?? "null"), contact: \(contact?.description ?? "null"), - profile: \(profile?.description ?? "null") - "openGroupInvitation": \(openGroupInvitation?.description ?? "null") + profile: \(profile?.description ?? "null"), + reaction: \(reaction?.description ?? "null"), + openGroupInvitation: \(openGroupInvitation?.description ?? "null") ) """ } diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index 442d87c2e..a575ba288 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -2434,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) } @@ -2503,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 } @@ -2538,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? @@ -2600,6 +2609,7 @@ extension SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGr group: SNProtoGroupContext?, quote: SNProtoDataMessageQuote?, preview: [SNProtoDataMessagePreview], + reaction: SNProtoDataMessageReaction?, profile: SNProtoDataMessageLokiProfile?, openGroupInvitation: SNProtoDataMessageOpenGroupInvitation?, closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage?) { @@ -2608,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 @@ -2640,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) @@ -2664,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 cc1bf5c28..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} @@ -2178,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"), @@ -2194,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 @@ -2213,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 @@ -2233,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 @@ -2256,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) }() @@ -2295,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) } @@ -2325,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} diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 6df37aef9..88f1fc115 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -198,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/Emoji Reacts/ReactMessage+Conversion.swift b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage+Conversion.swift new file mode 100644 index 000000000..34cc1bfdf --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage+Conversion.swift @@ -0,0 +1,24 @@ + +extension ReactMessage { + + /// To be used for outgoing messages only. + public static func from(_ reaction: VisibleMessage.Reaction?) -> ReactMessage? { + guard let reaction = reaction else { return nil } + return ReactMessage( + timestamp: reaction.timestamp!, + authorId: reaction.publicKey!, + emoji: reaction.emoji) + } +} + +extension VisibleMessage.Reaction { + + public static func from(_ reaction: ReactMessage?) -> VisibleMessage.Reaction? { + guard let reaction = reaction else { return nil } + let result = VisibleMessage.Reaction() + result.timestamp = reaction.timestamp + result.publicKey = reaction.authorId + result.emoji = reaction.emoji + return result + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift new file mode 100644 index 000000000..40b6c2f13 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift @@ -0,0 +1,39 @@ + +@objc(SNReactMessage) +public final class ReactMessage : MTLModel { + + public var timestamp: UInt64? + public var authorId: String? + + @objc + public var emoji: String? + + @objc + public var sender: String? + + @objc + public var messageId: String? + + @objc + public init(timestamp: UInt64, authorId: String, emoji: String?) { + self.timestamp = timestamp + self.authorId = authorId + self.emoji = emoji + super.init() + } + + @objc + public override init() { + super.init() + } + + @objc + public required init!(coder: NSCoder) { + super.init(coder: coder) + } + + @objc + public required init(dictionary dictionaryValue: [String: Any]!) throws { + try super.init(dictionary: dictionaryValue) + } +} 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 + } } From 9ae1f3e904bf6b29bb6797788bad23e1352a9a96 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 19 May 2022 10:57:46 +1000 Subject: [PATCH 008/133] minor fix on cell UI for emoji reacts --- .../Message Cells/VisibleMessageCell.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index f2caaeb6e..65478758e 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -19,9 +19,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { 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: bubbleView, withInset: 0) + 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) @@ -181,6 +182,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { bubbleViewLeftConstraint1.isActive = true bubbleViewTopConstraint.isActive = true bubbleViewRightConstraint1.isActive = true + bubbleView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1) // Timer view addSubview(timerView) timerView.center(.vertical, in: bubbleView) @@ -190,8 +192,9 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { snContentView.pin(to: bubbleView) // Reaction view addSubview(reactionContainerView) - reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.smallSpacing) + reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.verySmallSpacing) reactionContainerViewLeftConstraint.isActive = true +// reactionContainerView.pin(.bottom, to: .bottom, of: self) // Message status image view addSubview(messageStatusImageView) messageStatusImageViewTopConstraint.isActive = true @@ -255,6 +258,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { // Content view populateContentView(for: viewItem, message: message) // Reaction view + reactionContainerView.isHidden = (message.reactions.count == 0) reactionContainerViewLeftConstraint.isActive = (direction == .incoming) reactionContainerViewRightConstraint.isActive = (direction == .outgoing) populateReaction(for: viewItem, message: message) @@ -484,7 +488,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { override func prepareForReuse() { super.prepareForReuse() unloadContent?() - let viewsToMove = [ bubbleView, profilePictureView, replyButton, timerView, messageStatusImageView ] + let viewsToMove = [ bubbleView, profilePictureView, replyButton, timerView, messageStatusImageView, reactionContainerView ] viewsToMove.forEach { $0.transform = .identity } replyButton.alpha = 0 timerView.prepareForReuse() From 3239aa521e97dd989bdd8e7ebb292eb14253a42f Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 19 May 2022 11:57:45 +1000 Subject: [PATCH 009/133] add border to reacts including current user --- .../Content Views/ReactionContainerView.swift | 6 ++++-- .../Message Cells/Content Views/ReactionView.swift | 13 ++++++++++--- .../Message Cells/VisibleMessageCell.swift | 10 +++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index f603174fb..31376ffff 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -4,6 +4,7 @@ final class ReactionContainerView : UIView { let result = UIStackView() result.axis = .vertical result.spacing = Values.smallSpacing + result.alignment = .center return result }() @@ -28,15 +29,16 @@ final class ReactionContainerView : UIView { containerView.pin(to: self) } - public func update(_ reactions: [(String, Int)]) { + public func update(_ reactions: [(String, (Int, Bool))]) { for subview in containerView.arrangedSubviews { containerView.removeArrangedSubview(subview) } let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = Values.smallSpacing + stackView.alignment = .center for reaction in reactions { - let reactionView = ReactionView(emoji: reaction.0, number: reaction.1) + let reactionView = ReactionView(emoji: reaction.0, value: reaction.1) stackView.addArrangedSubview(reactionView) } containerView.addArrangedSubview(stackView) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 4f25b7925..c72f3f835 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -3,14 +3,16 @@ import UIKit final class ReactionView : UIView { private let emoji: String private let number: Int + private let hasCurrentUser: Bool // MARK: Settings private static let height: CGFloat = 22 // MARK: Lifecycle - init(emoji: String, number: Int) { + init(emoji: String, value: (Int, Bool)) { self.emoji = emoji - self.number = number + self.number = value.0 + self.hasCurrentUser = value.1 super.init(frame: CGRect.zero) setUpViewHierarchy() } @@ -29,7 +31,7 @@ final class ReactionView : UIView { emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize) let numberLabel = UILabel() - numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.2f", Float(number) / 1000) + "k" + numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.1f", Float(number) / 1000) + "k" numberLabel.font = .systemFont(ofSize: Values.verySmallFontSize) numberLabel.textColor = Colors.text @@ -45,5 +47,10 @@ final class ReactionView : UIView { set(.height, to: ReactionView.height) backgroundColor = Colors.receivedMessageBackground layer.cornerRadius = ReactionView.height / 2 + + if hasCurrentUser { + layer.borderWidth = 1 + layer.borderColor = Colors.accent.cgColor + } } } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 65478758e..f3a7e45f8 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -457,17 +457,17 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } private func populateReaction(for viewItem: ConversationViewItem, message: TSMessage) { - let reactions: OrderedDictionary = OrderedDictionary() + let reactions: OrderedDictionary = OrderedDictionary() for reaction in message.reactions { if let reactMessage = reaction as? ReactMessage, let emoji = reactMessage.emoji { - if let number = reactions.value(forKey: emoji) { - reactions.replace(key: emoji, value: number + 1) + let isSelfSend = (reactMessage.sender! == getUserHexEncodedPublicKey()) + if let value = reactions.value(forKey: emoji) { + reactions.replace(key: emoji, value: (value.0 + 1, value.1 || isSelfSend)) } else { - reactions.append(key: emoji, value: 1) + reactions.append(key: emoji, value: (1, isSelfSend)) } } } - print("Ryan Test: \(reactions.orderedKeys)") reactionContainerView.update(reactions.orderedItems) } From 9d795d4bfb2228df9ec55637af67123800268feb Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 19 May 2022 14:02:52 +1000 Subject: [PATCH 010/133] ui: add expand react button --- .../Content Views/ReactionContainerView.swift | 18 ++++++- .../Content Views/ReactionView.swift | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 31376ffff..91c465c5d 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -37,10 +37,26 @@ final class ReactionContainerView : UIView { stackView.axis = .horizontal stackView.spacing = Values.smallSpacing stackView.alignment = .center - for reaction in reactions { + + var displayedReactions: [(String, (Int, Bool))] + var expandButtonReactions: [String] + + if reactions.count >= 6 { + displayedReactions = Array(reactions[0...2]) + expandButtonReactions = Array(reactions[3...5]).map{ $0.0 } + } else { + displayedReactions = reactions + expandButtonReactions = [] + } + + for reaction in displayedReactions { let reactionView = ReactionView(emoji: reaction.0, value: reaction.1) stackView.addArrangedSubview(reactionView) } + if expandButtonReactions.count > 0 { + let expandButton = ExpandingReactionButton(emojis: expandButtonReactions) + stackView.addArrangedSubview(expandButton) + } containerView.addArrangedSubview(stackView) } } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index c72f3f835..662dbff2b 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -54,3 +54,53 @@ final class ReactionView : UIView { } } } + +final class ExpandingReactionButton: UIView { + private let emojis: [String] + + // MARK: Settings + private let size: CGFloat = 22 + private let margin: CGFloat = 15 + + // MARK: Lifecycle + init(emojis: [String]) { + 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 + container.layer.borderColor = isDarkMode ? UIColor.black.cgColor : UIColor.white.cgColor + + let emojiLabel = UILabel() + emojiLabel.text = emoji + 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) + } +} From acddafd1314708576cfc9b4720ab1b902eadf437 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 19 May 2022 14:34:21 +1000 Subject: [PATCH 011/133] minor fix on remove stack view's subviews --- .../Message Cells/Content Views/ReactionContainerView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 91c465c5d..d8c768f4a 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -32,6 +32,7 @@ final class ReactionContainerView : UIView { public func update(_ reactions: [(String, (Int, Bool))]) { for subview in containerView.arrangedSubviews { containerView.removeArrangedSubview(subview) + subview.removeFromSuperview() } let stackView = UIStackView() stackView.axis = .horizontal From 5feb87f069b70d169ccb58778ca7a9f5e780227e Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 19 May 2022 17:10:53 +1000 Subject: [PATCH 012/133] ui: add interaction to reaction view --- .../ConversationVC+Interaction.swift | 26 +++++++++++++++---- .../Content Views/ReactionContainerView.swift | 17 +++++++++--- .../Content Views/ReactionView.swift | 6 ++--- .../Message Cells/MessageCell.swift | 2 ++ .../Message Cells/VisibleMessageCell.swift | 12 +++++++++ .../Messages/Signal/TSMessage.h | 3 ++- .../Messages/Signal/TSMessage.m | 12 ++++++++- 7 files changed, 64 insertions(+), 14 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 1a1614631..30e8dddaf 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -820,8 +820,21 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func react(_ viewItem: ConversationViewItem, with emoji: String) { - print("Ryan Test: \(emoji)") UserDefaults.standard.addNewRecentlyUsedEmoji(emoji) + react(viewItem, with: emoji, cancel: false) + } + + func quickReact(_ viewItem: ConversationViewItem, with emoji: String) { + print("Ryan Test: Quick react with \(emoji)") + react(viewItem, with: emoji) + } + + func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) { + print("Ryan Test: Cancel react for \(emoji)") + react(viewItem, with: emoji, cancel: true) + } + + private func react(_ viewItem: ConversationViewItem, with emoji: String, cancel: Bool) { guard let message = viewItem.interaction as? TSMessage else { return } var authorId = getUserHexEncodedPublicKey() if let incomingMessage = message as? TSIncomingMessage { authorId = incomingMessage.authorId } @@ -832,11 +845,14 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let visibleMessage = VisibleMessage() visibleMessage.sentTimestamp = sentTimestamp visibleMessage.reaction = .from(reactMessage) - visibleMessage.reaction?.kind = .react + visibleMessage.reaction?.kind = cancel ? .remove : .react Storage.write { transaction in - message.update(withReaction: reactMessage, transaction: transaction) - // TODO: send emoji react message -// MessageSender.send(<#T##message: Message##Message#>, in: thread, using: <#T##YapDatabaseReadWriteTransaction#>) + if cancel { + message.removeReaction(reactMessage, transaction: transaction) } + else { + message.addReaction(reactMessage, transaction: transaction) + } + // MessageSender.send(visibleMessage, in: thread, using: transaction) } } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index d8c768f4a..1c9bda6de 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -10,6 +10,8 @@ final class ReactionContainerView : UIView { private var showingAllReactions = false + var reactionViews: [ReactionView] = [] + // MARK: Lifecycle init() { super.init(frame: CGRect.zero) @@ -30,10 +32,8 @@ final class ReactionContainerView : UIView { } public func update(_ reactions: [(String, (Int, Bool))]) { - for subview in containerView.arrangedSubviews { - containerView.removeArrangedSubview(subview) - subview.removeFromSuperview() - } + prepareForUpdate() + let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = Values.smallSpacing @@ -53,6 +53,7 @@ final class ReactionContainerView : UIView { for reaction in displayedReactions { let reactionView = ReactionView(emoji: reaction.0, value: reaction.1) stackView.addArrangedSubview(reactionView) + reactionViews.append(reactionView) } if expandButtonReactions.count > 0 { let expandButton = ExpandingReactionButton(emojis: expandButtonReactions) @@ -60,6 +61,14 @@ final class ReactionContainerView : UIView { } containerView.addArrangedSubview(stackView) } + + private func prepareForUpdate() { + for subview in containerView.arrangedSubviews { + containerView.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + reactionViews = [] + } } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 662dbff2b..d535d2cd5 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -1,9 +1,9 @@ import UIKit final class ReactionView : UIView { - private let emoji: String - private let number: Int - private let hasCurrentUser: Bool + let emoji: String + let number: Int + let hasCurrentUser: Bool // MARK: Settings private static let height: CGFloat = 22 diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 9bc79bcfe..a059cb079 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -80,4 +80,6 @@ protocol MessageCellDelegate : AnyObject { func openURL(_ url: URL) func handleReplyButtonTapped(for viewItem: ConversationViewItem) func showUserDetails(for sessionID: String) + func quickReact(_ viewItem: ConversationViewItem, with emoji: String) + func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index f3a7e45f8..8c75ac9fa 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -548,6 +548,18 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } else if replyButton.frame.contains(location) { UIImpactFeedbackGenerator(style: .heavy).impactOccurred() reply() + } else if reactionContainerView.frame.contains(location) { + let convertedLocation = reactionContainerView.convert(location, from: self) + for reactionView in reactionContainerView.reactionViews { + if reactionView.frame.contains(convertedLocation) { + if reactionView.hasCurrentUser { + delegate?.cancelReact(viewItem, for: reactionView.emoji) + } else { + delegate?.quickReact(viewItem, with: reactionView.emoji) + } + break + } + } } else { delegate?.handleViewItemTapped(viewItem, gestureRecognizer: gestureRecognizer) } diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.h b/SessionMessagingKit/Messages/Signal/TSMessage.h index dc58ab759..951b42a4c 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.h +++ b/SessionMessagingKit/Messages/Signal/TSMessage.h @@ -90,7 +90,8 @@ extern const NSUInteger kOversizeTextMessageSizeThreshold; - (void)updateCallMessageWithNewBody:(NSString *)newBody transaction:(YapDatabaseReadWriteTransaction *)transaction; -- (void)updateWithReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)addReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)removeReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction; @end diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.m b/SessionMessagingKit/Messages/Signal/TSMessage.m index db112d305..7a8d4e845 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSMessage.m @@ -457,7 +457,7 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; }]; } -- (void)updateWithReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction +- (void)addReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction { if ([self isKindOfClass:[TSIncomingMessage class]] || [self isKindOfClass:[TSOutgoingMessage class]]) { [self applyChangeToSelfAndLatestCopy:transaction @@ -469,6 +469,16 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; } } +- (void)removeReaction:(SNReactMessage *)reaction transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + if ([self isKindOfClass:[TSIncomingMessage class]] || [self isKindOfClass:[TSOutgoingMessage class]]) { + [self applyChangeToSelfAndLatestCopy:transaction + changeBlock:^(TSMessage *message) { + [message.reactions removeObject:reaction]; + }]; + } +} + @end NS_ASSUME_NONNULL_END From 6af2d295820899161655638718bab841f17e20e3 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 19 May 2022 17:15:36 +1000 Subject: [PATCH 013/133] ui: add to do for expand button --- .../Message Cells/Content Views/ReactionContainerView.swift | 5 ++++- Session/Conversations/Message Cells/VisibleMessageCell.swift | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 1c9bda6de..4d5d91d45 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -11,6 +11,7 @@ final class ReactionContainerView : UIView { private var showingAllReactions = false var reactionViews: [ReactionView] = [] + var expandButton: ExpandingReactionButton? // MARK: Lifecycle init() { @@ -56,8 +57,10 @@ final class ReactionContainerView : UIView { reactionViews.append(reactionView) } if expandButtonReactions.count > 0 { - let expandButton = ExpandingReactionButton(emojis: expandButtonReactions) + expandButton = ExpandingReactionButton(emojis: expandButtonReactions) stackView.addArrangedSubview(expandButton) + } else { + expandButton = nil } containerView.addArrangedSubview(stackView) } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 8c75ac9fa..55cf89058 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -557,9 +557,12 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } else { delegate?.quickReact(viewItem, with: reactionView.emoji) } - break + return } } + if let expandButton = reactionContainerView.expandButton, expandButton.frame.contains(convertedLocation) { + // TODO: show all emojis + } } else { delegate?.handleViewItemTapped(viewItem, gestureRecognizer: gestureRecognizer) } From 38665e6c33d6a34f1a3f0b488a6ff3d44145562e Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 19 May 2022 17:20:50 +1000 Subject: [PATCH 014/133] wip: expand/collapse emojis --- .../Content Views/ReactionContainerView.swift | 10 +++++++++- .../Message Cells/VisibleMessageCell.swift | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 4d5d91d45..8ddf28bb6 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -10,6 +10,7 @@ final class ReactionContainerView : UIView { private var showingAllReactions = false + var reactions: [(String, (Int, Bool))] = [] var reactionViews: [ReactionView] = [] var expandButton: ExpandingReactionButton? @@ -33,6 +34,7 @@ final class ReactionContainerView : UIView { } public func update(_ reactions: [(String, (Int, Bool))]) { + self.reactions = reactions prepareForUpdate() let stackView = UIStackView() @@ -58,7 +60,7 @@ final class ReactionContainerView : UIView { } if expandButtonReactions.count > 0 { expandButton = ExpandingReactionButton(emojis: expandButtonReactions) - stackView.addArrangedSubview(expandButton) + stackView.addArrangedSubview(expandButton!) } else { expandButton = nil } @@ -72,6 +74,12 @@ final class ReactionContainerView : UIView { } reactionViews = [] } + + public func showAllEmojis() { + guard !showingAllReactions else { return } + showingAllReactions = true + update(reactions) + } } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 55cf89058..45f6f3647 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -562,6 +562,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } if let expandButton = reactionContainerView.expandButton, expandButton.frame.contains(convertedLocation) { // TODO: show all emojis + reactionContainerView. } } else { delegate?.handleViewItemTapped(viewItem, gestureRecognizer: gestureRecognizer) From 947869ef4c9baaecd496286711e4f5e97bdc8a73 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 20 May 2022 16:44:53 +1000 Subject: [PATCH 015/133] expand & collapse emoji reacts --- Session/Conversations/ConversationVC.swift | 7 ++ .../Conversations/Input View/InputView.swift | 2 +- .../Content Views/ReactionContainerView.swift | 65 ++++++++++++++++--- .../Message Cells/MessageCell.swift | 1 + .../Message Cells/VisibleMessageCell.swift | 24 +++++-- .../Views & Modals/BodyTextView.swift | 6 +- 6 files changed, 89 insertions(+), 16 deletions(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 057f5e0b0..f358eb553 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -761,6 +761,13 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableView.automaticDimension } + + func needsLayout() { + UIView.setAnimationsEnabled(false) + messagesTableView.beginUpdates() + messagesTableView.endUpdates() + UIView.setAnimationsEnabled(true) + } func getMediaCache() -> NSCache { return mediaCache diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index 8f9c0ff26..cb7d4779b 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -337,7 +337,7 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, inputTextView.resignFirstResponder() } - func handleLongPress() { + func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { // Not relevant in this case } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 8ddf28bb6..f6dede54e 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -1,18 +1,40 @@ final class ReactionContainerView : UIView { - private lazy var containerView: UIStackView = { - let result = UIStackView() + 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 + }() + private var showingAllReactions = false var reactions: [(String, (Int, Bool))] = [] var reactionViews: [ReactionView] = [] 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() { @@ -29,14 +51,21 @@ final class ReactionContainerView : UIView { } private func setUpViewHierarchy() { - addSubview(containerView) - containerView.pin(to: self) + addSubview(mainStackView) + mainStackView.pin(to: self) } public func update(_ reactions: [(String, (Int, Bool))]) { self.reactions = reactions prepareForUpdate() - + if showingAllReactions { + updateAllReactions() + } else { + updateCollapsedReactions(reactions) + } + } + + private func updateCollapsedReactions(_ reactions: [(String, (Int, Bool))]) { let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = Values.smallSpacing @@ -64,14 +93,28 @@ final class ReactionContainerView : UIView { } else { expandButton = nil } - containerView.addArrangedSubview(stackView) + reactionContainerView.addArrangedSubview(stackView) + } + + private func updateAllReactions() { + var reactions = self.reactions + while reactions.count > 0 { + var line: [(String, (Int, Bool))] = [] + while reactions.count > 0 && line.count < 5 { + line.append(reactions.removeFirst()) + } + updateCollapsedReactions(line) + } + mainStackView.addArrangedSubview(collapseButton) } private func prepareForUpdate() { - for subview in containerView.arrangedSubviews { - containerView.removeArrangedSubview(subview) + for subview in reactionContainerView.arrangedSubviews { + reactionContainerView.removeArrangedSubview(subview) subview.removeFromSuperview() } + mainStackView.removeArrangedSubview(collapseButton) + collapseButton.removeFromSuperview() reactionViews = [] } @@ -80,6 +123,12 @@ final class ReactionContainerView : UIView { showingAllReactions = true update(reactions) } + + public func showLessEmojis() { + guard showingAllReactions else { return } + showingAllReactions = false + update(reactions) + } } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index a059cb079..ddc280db7 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -82,4 +82,5 @@ protocol MessageCellDelegate : AnyObject { func showUserDetails(for sessionID: String) func quickReact(_ viewItem: ConversationViewItem, with emoji: String) func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) + func needsLayout() } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 45f6f3647..91423d3ee 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -533,9 +533,21 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } - @objc func handleLongPress() { + @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { guard let viewItem = viewItem else { return } - delegate?.handleViewItemLongPressed(viewItem) + let location = gestureRecognizer.location(in: self) + if reactionContainerView.frame.contains(location) { + let convertedLocation = reactionContainerView.convert(location, from: self) + for reactionView in reactionContainerView.reactionViews { + if reactionView.frame.contains(convertedLocation) { + // TODO: Show react list + print("Ryan Test: long press on emoji.") + break + } + } + } else { + delegate?.handleViewItemLongPressed(viewItem) + } } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { @@ -561,8 +573,12 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } if let expandButton = reactionContainerView.expandButton, expandButton.frame.contains(convertedLocation) { - // TODO: show all emojis - reactionContainerView. + reactionContainerView.showAllEmojis() + delegate?.needsLayout() + } + if reactionContainerView.collapseButton.frame.contains(convertedLocation) { + reactionContainerView.showLessEmojis() + delegate?.needsLayout() } } else { delegate?.handleViewItemTapped(viewItem, gestureRecognizer: gestureRecognizer) diff --git a/Session/Conversations/Views & Modals/BodyTextView.swift b/Session/Conversations/Views & Modals/BodyTextView.swift index 3048db56d..6f8563e02 100644 --- a/Session/Conversations/Views & Modals/BodyTextView.swift +++ b/Session/Conversations/Views & Modals/BodyTextView.swift @@ -34,8 +34,8 @@ final class BodyTextView : UITextView { addGestureRecognizer(doubleTapGestureRecognizer) } - @objc private func handleLongPress() { - snDelegate.handleLongPress() + @objc private func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + snDelegate.handleLongPress(gestureRecognizer) } @objc private func handleDoubleTap() { @@ -45,5 +45,5 @@ final class BodyTextView : UITextView { protocol BodyTextViewDelegate { - func handleLongPress() + func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) } From d3cc3c810da4d8f691690ed2260d6b398888dcf2 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 30 May 2022 16:20:55 +1000 Subject: [PATCH 016/133] align outgoing message reacts to right --- .../Content Views/ReactionContainerView.swift | 15 ++++++++++++--- .../Message Cells/VisibleMessageCell.swift | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index f6dede54e..b5ee6b926 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -17,6 +17,7 @@ final class ReactionContainerView : UIView { }() private var showingAllReactions = false + private var isOutgoingMessage = false var reactions: [(String, (Int, Bool))] = [] var reactionViews: [ReactionView] = [] @@ -55,8 +56,9 @@ final class ReactionContainerView : UIView { mainStackView.pin(to: self) } - public func update(_ reactions: [(String, (Int, Bool))]) { + public func update(_ reactions: [(String, (Int, Bool))], isOutgoingMessage: Bool) { self.reactions = reactions + self.isOutgoingMessage = isOutgoingMessage prepareForUpdate() if showingAllReactions { updateAllReactions() @@ -70,6 +72,13 @@ final class ReactionContainerView : UIView { stackView.axis = .horizontal stackView.spacing = Values.smallSpacing stackView.alignment = .center + if isOutgoingMessage { + stackView.semanticContentAttribute = .forceRightToLeft + reactionContainerView.semanticContentAttribute = .forceRightToLeft + } else { + stackView.semanticContentAttribute = .unspecified + reactionContainerView.semanticContentAttribute = .unspecified + } var displayedReactions: [(String, (Int, Bool))] var expandButtonReactions: [String] @@ -121,13 +130,13 @@ final class ReactionContainerView : UIView { public func showAllEmojis() { guard !showingAllReactions else { return } showingAllReactions = true - update(reactions) + update(reactions, isOutgoingMessage: isOutgoingMessage) } public func showLessEmojis() { guard showingAllReactions else { return } showingAllReactions = false - update(reactions) + update(reactions, isOutgoingMessage: isOutgoingMessage) } } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 0ec6d5830..dc352de16 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -467,7 +467,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } } - reactionContainerView.update(reactions.orderedItems) + reactionContainerView.update(reactions.orderedItems, isOutgoingMessage: direction == .outgoing) } override func layoutSubviews() { From 49706f620f3ad97142af8f96f5a131af2d931eb0 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 31 May 2022 11:59:24 +1000 Subject: [PATCH 017/133] dynamic number of emojis per line --- .../Content Views/ReactionContainerView.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index b5ee6b926..2b3bfac42 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -18,6 +18,7 @@ final class ReactionContainerView : UIView { private var showingAllReactions = false private var isOutgoingMessage = false + private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 var reactions: [(String, (Int, Bool))] = [] var reactionViews: [ReactionView] = [] @@ -83,9 +84,9 @@ final class ReactionContainerView : UIView { var displayedReactions: [(String, (Int, Bool))] var expandButtonReactions: [String] - if reactions.count >= 6 { - displayedReactions = Array(reactions[0...2]) - expandButtonReactions = Array(reactions[3...5]).map{ $0.0 } + if reactions.count > maxEmojisPerLine { + displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)]) + expandButtonReactions = Array(reactions[(maxEmojisPerLine - 2)...maxEmojisPerLine]).map{ $0.0 } } else { displayedReactions = reactions expandButtonReactions = [] @@ -109,7 +110,7 @@ final class ReactionContainerView : UIView { var reactions = self.reactions while reactions.count > 0 { var line: [(String, (Int, Bool))] = [] - while reactions.count > 0 && line.count < 5 { + while reactions.count > 0 && line.count < maxEmojisPerLine { line.append(reactions.removeFirst()) } updateCollapsedReactions(line) From 927f44ac685cdd3c34967b8947cc4191573fd753 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 31 May 2022 16:00:36 +1000 Subject: [PATCH 018/133] sending & receiving --- .../ConversationVC+Interaction.swift | 2 +- SessionMessagingKit/Jobs/MessageSendJob.swift | 2 +- .../Messages/Signal/TSMessage.m | 3 +++ .../Visible Messages/VisibleMessage.swift | 2 +- .../Emoji Reacts/ReactMessage.swift | 14 ++++++++++++ .../MessageReceiver+Handling.swift | 22 +++++++++++++++++++ 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 30e8dddaf..e299ffc67 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -852,7 +852,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc else { message.addReaction(reactMessage, transaction: transaction) } - // MessageSender.send(visibleMessage, in: thread, using: transaction) + MessageSender.send(visibleMessage, in: thread, using: transaction) } } diff --git a/SessionMessagingKit/Jobs/MessageSendJob.swift b/SessionMessagingKit/Jobs/MessageSendJob.swift index 1a302c174..3be60900f 100644 --- a/SessionMessagingKit/Jobs/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/MessageSendJob.swift @@ -75,7 +75,7 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi } let storage = SNMessagingKitConfiguration.shared.storage if let message = message as? VisibleMessage { - guard TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) != nil else { return } // The message has been deleted + guard TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) != nil || message.reaction != nil else { return } // The message has been deleted let attachments = message.attachmentIDs.compactMap { TSAttachment.fetch(uniqueId: $0) as? TSAttachmentStream } let attachmentsToUpload = attachments.filter { !$0.isUploaded } attachmentsToUpload.forEach { attachment in diff --git a/SessionMessagingKit/Messages/Signal/TSMessage.m b/SessionMessagingKit/Messages/Signal/TSMessage.m index 7a8d4e845..088f5d639 100644 --- a/SessionMessagingKit/Messages/Signal/TSMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSMessage.m @@ -464,6 +464,9 @@ const NSUInteger kOversizeTextMessageSizeThreshold = 2 * 1024; changeBlock:^(TSMessage *message) { if (![message.reactions containsObject:reaction]) { [message.reactions addObject:reaction]; + } else { + NSUInteger index = [message.reactions indexOfObject:reaction]; + [message.reactions replaceObjectAtIndex:index withObject:reaction]; } }]; } diff --git a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift index b35186428..4464fbb5b 100644 --- a/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Messages/VisibleMessage.swift @@ -110,7 +110,7 @@ public final class VisibleMessage : Message { // Open group invitation if let openGroupInvitation = openGroupInvitation, let openGroupInvitationProto = openGroupInvitation.toProto() { dataMessage.setOpenGroupInvitation(openGroupInvitationProto) } // Emoji react - if let reaction = reaction, let reactionProto = reaction.toProto() { + if let reaction = reaction, let reactionProto = reaction.toProto(using: transaction) { dataMessage.setReaction(reactionProto) } // Group context diff --git a/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift index 40b6c2f13..c3eed2cf7 100644 --- a/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift +++ b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift @@ -36,4 +36,18 @@ public final class ReactMessage : MTLModel { public required init(dictionary dictionaryValue: [String: Any]!) throws { try super.init(dictionary: dictionaryValue) } + + @objc + public func isSelfReact() -> Bool { + return sender == getUserHexEncodedPublicKey() + } + + @objc + public override func isEqual(_ object: Any!) -> Bool { + guard let other = object as? ReactMessage else { return false } + return other.sender == self.sender && + other.emoji == self.emoji && + other.timestamp == self.timestamp && + other.authorId == self.authorId + } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index e3d69d3ea..9b72c6c84 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -376,6 +376,28 @@ extension MessageReceiver { } // Get or create thread guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread } + // Handle emoji reacts first + if let reaction = message.reaction, proto.dataMessage?.reaction != nil, let author = reaction.publicKey, let timestamp = reaction.timestamp { + var tsMessage: TSMessage? + if author == getUserHexEncodedPublicKey() { + tsMessage = TSOutgoingMessage.find(withTimestamp: timestamp) + } else { + tsMessage = TSIncomingMessage.find(withAuthorId: author, timestamp: timestamp, transaction: transaction) + } + let reactionMessage = ReactMessage(timestamp: timestamp, authorId: author, emoji: reaction.emoji) + reactionMessage.sender = message.sender + if let serverID = message.openGroupServerMessageID { reactionMessage.messageId = "\(serverID)" } + if let serverHash = message.serverHash { reactionMessage.messageId = serverHash } + switch reaction.kind { + case .react: + tsMessage?.addReaction(reactionMessage, transaction: transaction) + case .remove: + tsMessage?.removeReaction(reactionMessage, transaction: transaction) + case .none: + break + } + return "" + } // Parse quote if needed var tsQuotedMessage: TSQuotedMessage? = nil if message.quote != nil && proto.dataMessage?.quote != nil, let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) { From 23af72fb3e0385b48f76af2a254d3526b1bdf149 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 31 May 2022 16:37:19 +1000 Subject: [PATCH 019/133] minor ui fix --- Session/Conversations/Message Cells/VisibleMessageCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index dc352de16..18480d2d4 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -282,7 +282,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { messageStatusImageView.tintColor = tintColor messageStatusImageView.backgroundColor = backgroundColor if let message = message as? TSOutgoingMessage { - messageStatusImageView.isHidden = (message.isCallMessage || message.messageState == .sent && thread?.lastInteraction != message) + messageStatusImageView.isHidden = (message.isCallMessage || message.messageState == .sent && thread?.lastInteraction.uniqueId != message.uniqueId) } else { messageStatusImageView.isHidden = true } From 67f79a88db8aee3cf711408d8249a1c8b2e9196e Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 31 May 2022 16:37:52 +1000 Subject: [PATCH 020/133] clean --- Session/Conversations/ConversationVC+Interaction.swift | 2 -- Session/Conversations/Message Cells/VisibleMessageCell.swift | 1 - 2 files changed, 3 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index e299ffc67..6a55dd5e0 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -825,12 +825,10 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func quickReact(_ viewItem: ConversationViewItem, with emoji: String) { - print("Ryan Test: Quick react with \(emoji)") react(viewItem, with: emoji) } func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) { - print("Ryan Test: Cancel react for \(emoji)") react(viewItem, with: emoji, cancel: true) } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 18480d2d4..0c387c5d9 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -540,7 +540,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { for reactionView in reactionContainerView.reactionViews { if reactionView.frame.contains(convertedLocation) { // TODO: Show react list - print("Ryan Test: long press on emoji.") break } } From 3af692ed01095c19983e149ea0891a729cc9cb31 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 31 May 2022 17:02:52 +1000 Subject: [PATCH 021/133] fix removing reacts --- .../Sending & Receiving/Emoji Reacts/ReactMessage.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift index c3eed2cf7..b82594987 100644 --- a/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift +++ b/SessionMessagingKit/Sending & Receiving/Emoji Reacts/ReactMessage.swift @@ -45,9 +45,6 @@ public final class ReactMessage : MTLModel { @objc public override func isEqual(_ object: Any!) -> Bool { guard let other = object as? ReactMessage else { return false } - return other.sender == self.sender && - other.emoji == self.emoji && - other.timestamp == self.timestamp && - other.authorId == self.authorId + return other.sender == self.sender && other.emoji == self.emoji } } From b5c7902833c96f9692168f11f30113e6938ecdf6 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 1 Jun 2022 17:06:02 +1000 Subject: [PATCH 022/133] WIP: reactions list --- Session.xcodeproj/project.pbxproj | 4 + .../ConversationVC+Interaction.swift | 8 ++ .../Content Views/ReactionContainerView.swift | 4 +- .../Content Views/ReactionView.swift | 35 ++++-- .../Message Cells/MessageCell.swift | 1 + .../Message Cells/VisibleMessageCell.swift | 4 +- .../Views & Modals/ReactionListSheet.swift | 106 ++++++++++++++++++ 7 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 Session/Conversations/Views & Modals/ReactionListSheet.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e8f4952ba..d060d7eed 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -157,6 +157,7 @@ 7B93D07127CF194000811CB6 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */; }; 7B93D07327CF19C800811CB6 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */; }; 7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07527CF1A8900811CB6 /* MockDataGenerator.swift */; }; + 7B9F71C928470667006DFE7B /* ReactionListSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9F71C828470667006DFE7B /* ReactionListSheet.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 */; }; @@ -1153,6 +1154,7 @@ 7B93D06F27CF194000811CB6 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; 7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.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 = ""; }; 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 = ""; }; @@ -2278,6 +2280,7 @@ B848A4C4269EAAA200617031 /* UserDetailsSheet.swift */, 7B1581E3271FC59C00848B49 /* CallModal.swift */, 7BFFB33B27D02F5800BEA04E /* CallPermissionRequestModal.swift */, + 7B9F71C828470667006DFE7B /* ReactionListSheet.swift */, ); path = "Views & Modals"; sourceTree = ""; @@ -4990,6 +4993,7 @@ 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.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 */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 6a55dd5e0..7c1fd8da4 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -819,6 +819,13 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc presentAlert(alert) } + func showReactionList(_ viewItem: ConversationViewItem) { + guard let message = viewItem.interaction as? TSMessage, message.reactions.count > 0 else { return } + let reactionListSheet = ReactionListSheet(for: message.reactions as! [ReactMessage]) + reactionListSheet.modalPresentationStyle = .overFullScreen + present(reactionListSheet, animated: true, completion: nil) + } + func react(_ viewItem: ConversationViewItem, with emoji: String) { UserDefaults.standard.addNewRecentlyUsedEmoji(emoji) react(viewItem, with: emoji, cancel: false) @@ -856,6 +863,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) { // TODO: to be implemented + } func contextMenuDismissed() { diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 2b3bfac42..d99aca43a 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -21,7 +21,7 @@ final class ReactionContainerView : UIView { private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 var reactions: [(String, (Int, Bool))] = [] - var reactionViews: [ReactionView] = [] + var reactionViews: [ReactionButton] = [] var expandButton: ExpandingReactionButton? var collapseButton: UIStackView = { let arrow = UIImageView(image: UIImage(named: "ic_chevron_up")?.resizedImage(to: CGSize(width: 15, height: 13))?.withRenderingMode(.alwaysTemplate)) @@ -93,7 +93,7 @@ final class ReactionContainerView : UIView { } for reaction in displayedReactions { - let reactionView = ReactionView(emoji: reaction.0, value: reaction.1) + let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.0, showBorder: reaction.1.1) stackView.addArrangedSubview(reactionView) reactionViews.append(reactionView) } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index d535d2cd5..f3d7888bf 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -1,18 +1,29 @@ import UIKit -final class ReactionView : UIView { +final class ReactionButton : UIView { let emoji: String let number: Int - let hasCurrentUser: Bool + let showBorder: Bool + let largeSize: Bool // MARK: Settings - private static let height: CGFloat = 22 + private var height: CGFloat { + return largeSize ? 32 : 22 + } + private var fontSize: CGFloat { + return largeSize ? Values.mediumFontSize : Values.verySmallFontSize + } + + private var spacing: CGFloat { + return largeSize ? Values.mediumSpacing : Values.verySmallSpacing + } // MARK: Lifecycle - init(emoji: String, value: (Int, Bool)) { + init(emoji: String, value: Int, showBorder: Bool = false, largeSize: Bool = false) { self.emoji = emoji - self.number = value.0 - self.hasCurrentUser = value.1 + self.number = value + self.showBorder = showBorder + self.largeSize = largeSize super.init(frame: CGRect.zero) setUpViewHierarchy() } @@ -28,27 +39,27 @@ final class ReactionView : UIView { private func setUpViewHierarchy() { let emojiLabel = UILabel() emojiLabel.text = emoji - emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + emojiLabel.font = .systemFont(ofSize: fontSize) let numberLabel = UILabel() numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.1f", Float(number) / 1000) + "k" - numberLabel.font = .systemFont(ofSize: Values.verySmallFontSize) + numberLabel.font = .systemFont(ofSize: fontSize) numberLabel.textColor = Colors.text let stackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ]) stackView.axis = .horizontal - stackView.spacing = Values.verySmallSpacing + 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: ReactionView.height) + set(.height, to: self.height) backgroundColor = Colors.receivedMessageBackground - layer.cornerRadius = ReactionView.height / 2 + layer.cornerRadius = self.height / 2 - if hasCurrentUser { + if showBorder { layer.borderWidth = 1 layer.borderColor = Colors.accent.cgColor } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index ddc280db7..55e15d5e9 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -80,6 +80,7 @@ protocol MessageCellDelegate : AnyObject { func openURL(_ url: URL) func handleReplyButtonTapped(for viewItem: ConversationViewItem) func showUserDetails(for sessionID: String) + func showReactionList(_ viewItem: ConversationViewItem) func quickReact(_ viewItem: ConversationViewItem, with emoji: String) func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) func needsLayout() diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 0c387c5d9..bb22f9614 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -539,7 +539,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let convertedLocation = reactionContainerView.convert(location, from: self) for reactionView in reactionContainerView.reactionViews { if reactionView.frame.contains(convertedLocation) { - // TODO: Show react list + delegate?.showReactionList(viewItem) break } } @@ -562,7 +562,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let convertedLocation = reactionContainerView.convert(location, from: self) for reactionView in reactionContainerView.reactionViews { if reactionView.frame.contains(convertedLocation) { - if reactionView.hasCurrentUser { + if reactionView.showBorder { delegate?.cancelReact(viewItem, for: reactionView.emoji) } else { delegate?.quickReact(viewItem, with: reactionView.emoji) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift new file mode 100644 index 000000000..64426ed49 --- /dev/null +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -0,0 +1,106 @@ +import UIKit + +final class ReactionListSheet : BaseVC { + private let reactions: [ReactMessage] + private var reactionMap: OrderedDictionary = OrderedDictionary() + + // MARK: Components + + lazy var contentView: UIView = { + let result = UIView() + result.layer.borderWidth = 0.5 + result.layer.borderColor = Colors.border.withAlphaComponent(0.5).cgColor + result.backgroundColor = Colors.modalBackground + return result + }() + + lazy var reactionContainer: UIStackView = { + let result = UIStackView() + let spacing = Values.smallSpacing + result.spacing = spacing + result.layoutMargins = UIEdgeInsets(top: spacing, leading: spacing, bottom: spacing, trailing: spacing) + result.isLayoutMarginsRelativeArrangement = true + return result + }() + + // MARK: Lifecycle + + init(for reactions: [ReactMessage]) { + self.reactions = reactions + 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) + populateData() + 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() { + // Reactions container + let scrollableContainer = UIScrollView(wrapping: reactionContainer, withInsets: .zero) + scrollableContainer.showsVerticalScrollIndicator = false + scrollableContainer.showsHorizontalScrollIndicator = false + scrollableContainer.set(.height, to: 48) + for reaction in reactionMap.orderedItems { + let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.count, largeSize: true) + reactionContainer.addArrangedSubview(reactionView) + } + contentView.addSubview(scrollableContainer) + scrollableContainer.pin([ UIView.VerticalEdge.top, UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) + // Line + let lineView = UIView() + lineView.backgroundColor = Colors.border.withAlphaComponent(0.5) + lineView.set(.height, to: 0.5) + contentView.addSubview(lineView) + lineView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) + lineView.pin(.top, to: .bottom, of: scrollableContainer) + + } + + private func populateData() { + for reaction in reactions { + if let emoji = reaction.emoji { + if !reactionMap.hasValue(forKey: emoji) { reactionMap.append(key: emoji, value: []) } + var value = reactionMap.value(forKey: emoji)! + value.append(reaction) + reactionMap.replace(key: emoji, value: value) + } + } + } + + // 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: nil) + } +} From 76aac26f7ed2903047cf36ff1c68b7ba4f0549e5 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 2 Jun 2022 14:58:43 +1000 Subject: [PATCH 023/133] redo reaction list sheet with collection view --- .../ConversationVC+Interaction.swift | 3 +- .../Content Views/ReactionView.swift | 19 +-- .../Message Cells/MessageCell.swift | 2 +- .../Message Cells/VisibleMessageCell.swift | 2 +- .../Views & Modals/ReactionListSheet.swift | 146 +++++++++++++++--- 5 files changed, 136 insertions(+), 36 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 7c1fd8da4..3e4c008c2 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -819,9 +819,10 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc presentAlert(alert) } - func showReactionList(_ viewItem: ConversationViewItem) { + func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?) { guard let message = viewItem.interaction as? TSMessage, message.reactions.count > 0 else { return } let reactionListSheet = ReactionListSheet(for: message.reactions as! [ReactMessage]) + reactionListSheet.selectedReaction = selectedReaction reactionListSheet.modalPresentationStyle = .overFullScreen present(reactionListSheet, animated: true, completion: nil) } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index f3d7888bf..627195692 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -4,26 +4,18 @@ final class ReactionButton : UIView { let emoji: String let number: Int let showBorder: Bool - let largeSize: Bool // MARK: Settings - private var height: CGFloat { - return largeSize ? 32 : 22 - } - private var fontSize: CGFloat { - return largeSize ? Values.mediumFontSize : Values.verySmallFontSize - } + private var height: CGFloat = 22 + private var fontSize: CGFloat = Values.verySmallFontSize - private var spacing: CGFloat { - return largeSize ? Values.mediumSpacing : Values.verySmallSpacing - } + private var spacing: CGFloat = Values.verySmallSpacing // MARK: Lifecycle - init(emoji: String, value: Int, showBorder: Bool = false, largeSize: Bool = false) { + init(emoji: String, value: Int, showBorder: Bool = false) { self.emoji = emoji self.number = value self.showBorder = showBorder - self.largeSize = largeSize super.init(frame: CGRect.zero) setUpViewHierarchy() } @@ -60,8 +52,7 @@ final class ReactionButton : UIView { layer.cornerRadius = self.height / 2 if showBorder { - layer.borderWidth = 1 - layer.borderColor = Colors.accent.cgColor + self.addBorder(with: Colors.accent) } } } diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 55e15d5e9..d676c16fb 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -80,7 +80,7 @@ protocol MessageCellDelegate : AnyObject { func openURL(_ url: URL) func handleReplyButtonTapped(for viewItem: ConversationViewItem) func showUserDetails(for sessionID: String) - func showReactionList(_ viewItem: ConversationViewItem) + func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?) func quickReact(_ viewItem: ConversationViewItem, with emoji: String) func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) func needsLayout() diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index bb22f9614..c189354fd 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -539,7 +539,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let convertedLocation = reactionContainerView.convert(location, from: self) for reactionView in reactionContainerView.reactionViews { if reactionView.frame.contains(convertedLocation) { - delegate?.showReactionList(viewItem) + delegate?.showReactionList(viewItem, selectedReaction: reactionView.emoji) break } } diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 64426ed49..39c480977 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -1,12 +1,13 @@ import UIKit -final class ReactionListSheet : BaseVC { +final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let reactions: [ReactMessage] private var reactionMap: OrderedDictionary = OrderedDictionary() + var selectedReaction: String? // MARK: Components - lazy var contentView: UIView = { + private lazy var contentView: UIView = { let result = UIView() result.layer.borderWidth = 0.5 result.layer.borderColor = Colors.border.withAlphaComponent(0.5).cgColor @@ -14,12 +15,24 @@ final class ReactionListSheet : BaseVC { return result }() - lazy var reactionContainer: UIStackView = { - let result = UIStackView() - let spacing = Values.smallSpacing - result.spacing = spacing - result.layoutMargins = UIEdgeInsets(top: spacing, leading: spacing, bottom: spacing, trailing: spacing) - result.isLayoutMarginsRelativeArrangement = true + private lazy var layout: UICollectionViewFlowLayout = { + let result = UICollectionViewFlowLayout() + result.scrollDirection = .horizontal + result.minimumLineSpacing = Values.smallSpacing + result.minimumInteritemSpacing = Values.smallSpacing + result.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + return result + }() + + private lazy var reactionContainer: UICollectionView = { + let result = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) + result.register(Cell.self, forCellWithReuseIdentifier: Cell.identifier) + result.set(.height, to: 48) + result.backgroundColor = .clear + result.isScrollEnabled = true + result.showsHorizontalScrollIndicator = false + result.dataSource = self + result.delegate = self return result }() @@ -46,6 +59,7 @@ final class ReactionListSheet : BaseVC { view.addGestureRecognizer(swipeGestureRecognizer) populateData() setUpViewHierarchy() + reactionContainer.reloadData() } private func setUpViewHierarchy() { @@ -57,23 +71,18 @@ final class ReactionListSheet : BaseVC { private func populateContentView() { // Reactions container - let scrollableContainer = UIScrollView(wrapping: reactionContainer, withInsets: .zero) - scrollableContainer.showsVerticalScrollIndicator = false - scrollableContainer.showsHorizontalScrollIndicator = false - scrollableContainer.set(.height, to: 48) - for reaction in reactionMap.orderedItems { - let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.count, largeSize: true) - reactionContainer.addArrangedSubview(reactionView) - } - contentView.addSubview(scrollableContainer) - scrollableContainer.pin([ UIView.VerticalEdge.top, UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) + contentView.addSubview(reactionContainer) + reactionContainer.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) + reactionContainer.pin(.top, to: .top, of: contentView, withInset: Values.verySmallSpacing) + // Reactions + // Line let lineView = UIView() lineView.backgroundColor = Colors.border.withAlphaComponent(0.5) lineView.set(.height, to: 0.5) contentView.addSubview(lineView) lineView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) - lineView.pin(.top, to: .bottom, of: scrollableContainer) + lineView.pin(.top, to: .bottom, of: reactionContainer, withInset: Values.verySmallSpacing) } @@ -86,6 +95,30 @@ final class ReactionListSheet : BaseVC { reactionMap.replace(key: emoji, value: value) } } + if selectedReaction == nil { + selectedReaction = reactionMap.orderedKeys[0] + } + } + + // MARK: Layout + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 0, leading: Values.smallSpacing, bottom: 0, trailing: Values.smallSpacing) + } + + // MARK: Data Source + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return reactionMap.orderedKeys.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell + let item = reactionMap.orderedItems[indexPath.item] + cell.data = (item.0, item.1.count) + return cell + } + + // MARK: Interaction + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { } // MARK: Interaction @@ -104,3 +137,78 @@ final class ReactionListSheet : BaseVC { dismiss(animated: true, completion: nil) } } + + +// MARK: Cell + +extension ReactionListSheet { + + fileprivate final class Cell : UICollectionViewCell { + var data: (String, Int)? { didSet { update() } } + override var isSelected: Bool { didSet { updateBorder() } } + + static let identifier = "ReactionListSheetCell" + + 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 + }() + + private static var contentViewHeight: CGFloat = 32 + private static var contentViewCornerRadius: CGFloat { contentViewHeight / 2 } + + 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) + } + + private func update() { + guard let data = data else { return } + emojiLabel.text = data.0 + numberLabel.text = data.1 < 1000 ? "\(data.1)" : String(format: "%.1f", Float(data.1) / 1000) + "k" + } + + private func updateBorder() { + if isSelected { + snContentView.addBorder(with: Colors.accent) + } else { + snContentView.addBorder(with: .clear) + } + } + } +} From c71290aa850b17d12ef1070001c1c5a7415b8b0d Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 2 Jun 2022 15:37:18 +1000 Subject: [PATCH 024/133] ui: detailed info label for reaction list sheet --- .../Views & Modals/ReactionListSheet.swift | 36 ++++++++++++++++--- .../Contents.json | 6 ++-- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 39c480977..ce512de20 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -1,4 +1,3 @@ -import UIKit final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let reactions: [ReactMessage] @@ -36,6 +35,13 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection return result }() + private lazy var detailInfoLabel: UILabel = { + let result = UILabel() + result.font = .systemFont(ofSize: Values.mediumFontSize) + result.textColor = Colors.grey.withAlphaComponent(0.8) + return result + }() + // MARK: Lifecycle init(for reactions: [ReactMessage]) { @@ -60,6 +66,15 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection populateData() setUpViewHierarchy() reactionContainer.reloadData() + update() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + if let index = reactionMap.orderedKeys.firstIndex(of: selectedReaction!) { + let indexPath = IndexPath(item: index, section: 0) + reactionContainer.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally) + } } private func setUpViewHierarchy() { @@ -74,15 +89,18 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection contentView.addSubview(reactionContainer) reactionContainer.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) reactionContainer.pin(.top, to: .top, of: contentView, withInset: Values.verySmallSpacing) - // Reactions - // Line let lineView = UIView() - lineView.backgroundColor = Colors.border.withAlphaComponent(0.5) + lineView.backgroundColor = Colors.border.withAlphaComponent(0.1) lineView.set(.height, to: 0.5) contentView.addSubview(lineView) - lineView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) + lineView.pin(.leading, to: .leading, of: contentView, withInset: Values.smallSpacing) + lineView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.smallSpacing) lineView.pin(.top, to: .bottom, of: reactionContainer, withInset: Values.verySmallSpacing) + // Detail info label + contentView.addSubview(detailInfoLabel) + detailInfoLabel.pin(.top, to: .bottom, of: lineView, withInset: Values.smallSpacing) + detailInfoLabel.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) } @@ -100,6 +118,11 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection } } + private func update() { + let seletedData = reactionMap.value(forKey: selectedReaction!)! + detailInfoLabel.text = "\(selectedReaction!) ยท \(seletedData.count)" + } + // MARK: Layout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return UIEdgeInsets(top: 0, leading: Values.smallSpacing, bottom: 0, trailing: Values.smallSpacing) @@ -114,11 +137,14 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell let item = reactionMap.orderedItems[indexPath.item] cell.data = (item.0, item.1.count) + cell.isSelected = item.0 == selectedReaction! return cell } // MARK: Interaction func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + selectedReaction = reactionMap.orderedKeys[indexPath.item] + update() } // MARK: Interaction 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" From a55e5857c7f77887be6671677dc3d395d82671de Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 2 Jun 2022 17:02:50 +1000 Subject: [PATCH 025/133] ui: reaction sender list --- .../Views & Modals/ReactionListSheet.swift | 105 +++++++++++++----- Session/Shared/UserCell.swift | 8 ++ 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index ce512de20..86f163dcc 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -1,5 +1,5 @@ -final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { +final class ReactionListSheet : BaseVC { private let reactions: [ReactMessage] private var reactionMap: OrderedDictionary = OrderedDictionary() var selectedReaction: String? @@ -39,6 +39,19 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection let result = UILabel() result.font = .systemFont(ofSize: Values.mediumFontSize) result.textColor = Colors.grey.withAlphaComponent(0.8) + result.set(.height, to: 32) + return result + }() + + private lazy var userListView: UITableView = { + let result = UITableView() + result.dataSource = self + result.delegate = self + result.register(UserCell.self, forCellReuseIdentifier: "UserCell") + result.backgroundColor = .clear + result.showsVerticalScrollIndicator = false + result.layer.borderWidth = 0.5 + result.layer.borderColor = Colors.border.withAlphaComponent(0.5).cgColor return result }() @@ -101,7 +114,10 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection contentView.addSubview(detailInfoLabel) detailInfoLabel.pin(.top, to: .bottom, of: lineView, withInset: Values.smallSpacing) detailInfoLabel.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) - + // Reactor list + contentView.addSubview(userListView) + userListView.pin([ UIView.HorizontalEdge.trailing, UIView.HorizontalEdge.leading, UIView.VerticalEdge.bottom ], to: contentView) + userListView.pin(.top, to: .bottom, of: detailInfoLabel, withInset: Values.smallSpacing) } private func populateData() { @@ -109,7 +125,11 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection if let emoji = reaction.emoji { if !reactionMap.hasValue(forKey: emoji) { reactionMap.append(key: emoji, value: []) } var value = reactionMap.value(forKey: emoji)! - value.append(reaction) + if reaction.sender == getUserHexEncodedPublicKey() { + value.insert(reaction, at: 0) + } else { + value.append(reaction) + } reactionMap.replace(key: emoji, value: value) } } @@ -121,30 +141,7 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection private func update() { let seletedData = reactionMap.value(forKey: selectedReaction!)! detailInfoLabel.text = "\(selectedReaction!) ยท \(seletedData.count)" - } - - // MARK: Layout - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - return UIEdgeInsets(top: 0, leading: Values.smallSpacing, bottom: 0, trailing: Values.smallSpacing) - } - - // MARK: Data Source - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return reactionMap.orderedKeys.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell - let item = reactionMap.orderedItems[indexPath.item] - cell.data = (item.0, item.1.count) - cell.isSelected = item.0 == selectedReaction! - return cell - } - - // MARK: Interaction - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - selectedReaction = reactionMap.orderedKeys[indexPath.item] - update() + userListView.reloadData() } // MARK: Interaction @@ -164,6 +161,60 @@ final class ReactionListSheet : BaseVC, UICollectionViewDataSource, UICollection } } +// MARK: UICollectionView + +extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + // MARK: Layout + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + return UIEdgeInsets(top: 0, leading: Values.smallSpacing, bottom: 0, trailing: Values.smallSpacing) + } + + // MARK: Data Source + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return reactionMap.orderedKeys.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell + let item = reactionMap.orderedItems[indexPath.item] + cell.data = (item.0, item.1.count) + cell.isSelected = item.0 == selectedReaction! + return cell + } + + // MARK: Interaction + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + selectedReaction = reactionMap.orderedKeys[indexPath.item] + update() + } +} + +// MARK: UITableView + +extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { + // MARK: Table View Data Source + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return reactionMap.value(forKey: selectedReaction!)?.count ?? 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell") as! UserCell + let publicKey = reactionMap.value(forKey: selectedReaction!)![indexPath.row].sender! + cell.publicKey = publicKey + cell.normalFont = true + if publicKey == getUserHexEncodedPublicKey() { + cell.accessory = .x + } else { + cell.accessory = .none + } + cell.update() + return cell + } +} // MARK: Cell diff --git a/Session/Shared/UserCell.swift b/Session/Shared/UserCell.swift index 8fd199a28..fed925240 100644 --- a/Session/Shared/UserCell.swift +++ b/Session/Shared/UserCell.swift @@ -4,12 +4,14 @@ final class UserCell : UITableViewCell { var accessory = Accessory.none var publicKey = "" var isZombie = false + var normalFont = false // MARK: Accessory enum Accessory { case none case lock case tick(isSelected: Bool) + case x } // MARK: Components @@ -83,6 +85,7 @@ final class UserCell : UITableViewCell { profilePictureView.publicKey = publicKey profilePictureView.update() displayNameLabel.text = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey + if normalFont { displayNameLabel.font = .systemFont(ofSize: Values.mediumFontSize) } switch accessory { case .none: accessoryImageView.isHidden = true @@ -97,6 +100,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 From d36b2b2d9f7e76a9aefd787a3d6f00acc1887432 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 2 Jun 2022 17:11:05 +1000 Subject: [PATCH 026/133] wip: cancel react in reaction list sheet --- .../Conversations/Views & Modals/ReactionListSheet.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 86f163dcc..d208a6844 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -214,6 +214,15 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { cell.update() return cell } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let cell = tableView.cellForRow(at: indexPath) as? UserCell else { return } + let publicKey = reactionMap.value(forKey: selectedReaction!)![indexPath.row].sender! + if publicKey == getUserHexEncodedPublicKey() { + // TODO: cancel emoji react + } + tableView.deselectRow(at: indexPath, animated: true) + } } // MARK: Cell From 26011c69a2d722b98cd6f89ecf76a3f9e1357148 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 3 Jun 2022 14:44:31 +1000 Subject: [PATCH 027/133] wip: clear all --- .../ConversationVC+Interaction.swift | 8 ++- .../Message Cells/MessageCell.swift | 4 +- .../Views & Modals/ReactionListSheet.swift | 55 +++++++++++++++---- 3 files changed, 53 insertions(+), 14 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 3e4c008c2..0048cf50f 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -820,8 +820,10 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?) { + guard let thread = thread as? TSGroupThread else { return } guard let message = viewItem.interaction as? TSMessage, message.reactions.count > 0 else { return } - let reactionListSheet = ReactionListSheet(for: message.reactions as! [ReactMessage]) + let reactionListSheet = ReactionListSheet(for: viewItem, thread: thread) + reactionListSheet.delegate = self reactionListSheet.selectedReaction = selectedReaction reactionListSheet.modalPresentationStyle = .overFullScreen present(reactionListSheet, animated: true, completion: nil) @@ -840,6 +842,10 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc react(viewItem, with: emoji, cancel: true) } + func cancelAllReact(reactMessages: [ReactMessage]) { + + } + private func react(_ viewItem: ConversationViewItem, with emoji: String, cancel: Bool) { guard let message = viewItem.interaction as? TSMessage else { return } var authorId = getUserHexEncodedPublicKey() diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index d676c16fb..03bac3d15 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -68,7 +68,7 @@ class MessageCell : UITableViewCell { } } -protocol MessageCellDelegate : AnyObject { +protocol MessageCellDelegate : ReactionDelegate { var lastSearchedText: String? { get } func getMediaCache() -> NSCache @@ -81,7 +81,5 @@ protocol MessageCellDelegate : AnyObject { func handleReplyButtonTapped(for viewItem: ConversationViewItem) func showUserDetails(for sessionID: String) func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?) - func quickReact(_ viewItem: ConversationViewItem, with emoji: String) - func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) func needsLayout() } diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index d208a6844..d7336fe17 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -1,8 +1,11 @@ final class ReactionListSheet : BaseVC { + private let thread: TSGroupThread + private let viewItem: ConversationViewItem private let reactions: [ReactMessage] private var reactionMap: OrderedDictionary = OrderedDictionary() var selectedReaction: String? + var delegate: ReactionDelegate? // MARK: Components @@ -43,6 +46,15 @@ final class ReactionListSheet : BaseVC { return result }() + private lazy var clearAllButton: Button = { + let result = Button(style: .destructiveOutline, size: .small) + result.translatesAutoresizingMaskIntoConstraints = false + result.setTitle(NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL", comment: ""), for: .normal) + result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside) + result.layer.borderWidth = 0 + return result + }() + private lazy var userListView: UITableView = { let result = UITableView() result.dataSource = self @@ -57,8 +69,14 @@ final class ReactionListSheet : BaseVC { // MARK: Lifecycle - init(for reactions: [ReactMessage]) { - self.reactions = reactions + init(for viewItem: ConversationViewItem, thread: TSGroupThread) { + self.viewItem = viewItem + self.thread = thread + if let message = viewItem.interaction as? TSMessage { + self.reactions = message.reactions as! [ReactMessage] + } else { + self.reactions = [] + } super.init(nibName: nil, bundle: nil) } @@ -110,14 +128,16 @@ final class ReactionListSheet : BaseVC { lineView.pin(.leading, to: .leading, of: contentView, withInset: Values.smallSpacing) lineView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.smallSpacing) lineView.pin(.top, to: .bottom, of: reactionContainer, withInset: Values.verySmallSpacing) - // Detail info label - contentView.addSubview(detailInfoLabel) - detailInfoLabel.pin(.top, to: .bottom, of: lineView, withInset: Values.smallSpacing) - detailInfoLabel.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) + // Detail info & clear all + let stackView = UIStackView(arrangedSubviews: [ detailInfoLabel, clearAllButton ]) + contentView.addSubview(stackView) + stackView.pin(.top, to: .bottom, of: lineView, withInset: Values.smallSpacing) + stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.mediumSpacing) + stackView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.mediumSpacing) // Reactor list contentView.addSubview(userListView) userListView.pin([ UIView.HorizontalEdge.trailing, UIView.HorizontalEdge.leading, UIView.VerticalEdge.bottom ], to: contentView) - userListView.pin(.top, to: .bottom, of: detailInfoLabel, withInset: Values.smallSpacing) + userListView.pin(.top, to: .bottom, of: stackView, withInset: Values.smallSpacing) } private func populateData() { @@ -159,6 +179,11 @@ final class ReactionListSheet : BaseVC { @objc func close() { dismiss(animated: true, completion: nil) } + + @objc private func clearAllTapped() { + guard let reactMessages = reactionMap.value(forKey: selectedReaction!) else { return } + delegate?.cancelAllReact(reactMessages: reactMessages) + } } // MARK: UICollectionView @@ -216,10 +241,9 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let cell = tableView.cellForRow(at: indexPath) as? UserCell else { return } - let publicKey = reactionMap.value(forKey: selectedReaction!)![indexPath.row].sender! + guard let reactMessage = reactionMap.value(forKey: selectedReaction!)?[indexPath.row], let publicKey = reactMessage.sender else { return } if publicKey == getUserHexEncodedPublicKey() { - // TODO: cancel emoji react + delegate?.cancelReact(viewItem, for: selectedReaction!) } tableView.deselectRow(at: indexPath, animated: true) } @@ -298,3 +322,14 @@ extension ReactionListSheet { } } } + +// MARK: Delegate + +protocol ReactionDelegate : AnyObject { + + func quickReact(_ viewItem: ConversationViewItem, with emoji: String) + func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) + func cancelAllReact(reactMessages: [ReactMessage]) + +} + From eb125baca3f8eb4829434bd72b0576ba763568f1 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 6 Jun 2022 15:38:38 +1000 Subject: [PATCH 028/133] cancel reaction in reaction list --- .../ConversationVC+Interaction.swift | 21 ++++-- Session/Conversations/ConversationVC.swift | 5 ++ .../Views & Modals/ReactionListSheet.swift | 75 ++++++++++++------- .../Utilities/Notification+Loki.swift | 1 + 4 files changed, 69 insertions(+), 33 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 0048cf50f..95f180ae7 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -823,6 +823,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc guard let thread = thread as? TSGroupThread else { return } guard let message = viewItem.interaction as? TSMessage, message.reactions.count > 0 else { return } let reactionListSheet = ReactionListSheet(for: viewItem, thread: thread) + showingReactionListForMessageId = viewItem.interaction.uniqueId reactionListSheet.delegate = self reactionListSheet.selectedReaction = selectedReaction reactionListSheet.modalPresentationStyle = .overFullScreen @@ -858,14 +859,20 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc visibleMessage.sentTimestamp = sentTimestamp visibleMessage.reaction = .from(reactMessage) visibleMessage.reaction?.kind = cancel ? .remove : .react - Storage.write { transaction in - if cancel { - message.removeReaction(reactMessage, transaction: transaction) } - else { - message.addReaction(reactMessage, transaction: transaction) + Storage.write( + with: { transaction in + if cancel { + message.removeReaction(reactMessage, transaction: transaction) } + else { + message.addReaction(reactMessage, transaction: transaction) + } + }, + completion: { + Storage.write { transaction in + MessageSender.send(visibleMessage, in: thread, using: transaction) + } } - MessageSender.send(visibleMessage, in: thread, using: transaction) - } + ) } func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) { diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index b600e44db..d01ebfa9f 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -37,6 +37,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat var isLoadingMore = false var scrollDistanceToBottomBeforeUpdate: CGFloat? var baselineKeyboardHeight: CGFloat = 0 + // Reaction + var showingReactionListForMessageId: String? var audioSession: OWSAudioSession { Environment.shared.audioSession } var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection } @@ -640,6 +642,9 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat } case .update: self.messagesTableView.reloadRows(at: [ IndexPath(row: Int(update.oldIndex), section: 0) ], with: .none) + if update.viewItem?.interaction.uniqueId == self.showingReactionListForMessageId { + NotificationCenter.default.post(name: .emojiReactsUpdated, object: nil) + } default: preconditionFailure() } diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index d7336fe17..f57027401 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -2,7 +2,7 @@ final class ReactionListSheet : BaseVC { private let thread: TSGroupThread private let viewItem: ConversationViewItem - private let reactions: [ReactMessage] + private var reactions: [ReactMessage] = [] private var reactionMap: OrderedDictionary = OrderedDictionary() var selectedReaction: String? var delegate: ReactionDelegate? @@ -11,8 +11,11 @@ final class ReactionListSheet : BaseVC { private lazy var contentView: UIView = { let result = UIView() - result.layer.borderWidth = 0.5 - result.layer.borderColor = Colors.border.withAlphaComponent(0.5).cgColor + 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 }() @@ -52,6 +55,7 @@ final class ReactionListSheet : BaseVC { result.setTitle(NSLocalizedString("MESSAGE_REQUESTS_CLEAR_ALL", comment: ""), for: .normal) result.addTarget(self, action: #selector(clearAllTapped), for: .touchUpInside) result.layer.borderWidth = 0 + result.isHidden = true return result }() @@ -60,10 +64,9 @@ final class ReactionListSheet : BaseVC { result.dataSource = self result.delegate = self result.register(UserCell.self, forCellReuseIdentifier: "UserCell") + result.separatorStyle = .none result.backgroundColor = .clear result.showsVerticalScrollIndicator = false - result.layer.borderWidth = 0.5 - result.layer.borderColor = Colors.border.withAlphaComponent(0.5).cgColor return result }() @@ -72,11 +75,6 @@ final class ReactionListSheet : BaseVC { init(for viewItem: ConversationViewItem, thread: TSGroupThread) { self.viewItem = viewItem self.thread = thread - if let message = viewItem.interaction as? TSMessage { - self.reactions = message.reactions as! [ReactMessage] - } else { - self.reactions = [] - } super.init(nibName: nil, bundle: nil) } @@ -94,9 +92,8 @@ final class ReactionListSheet : BaseVC { let swipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(close)) swipeGestureRecognizer.direction = .down view.addGestureRecognizer(swipeGestureRecognizer) - populateData() + NotificationCenter.default.addObserver(self, selector: #selector(update), name: .emojiReactsUpdated, object: nil) setUpViewHierarchy() - reactionContainer.reloadData() update() } @@ -120,27 +117,39 @@ final class ReactionListSheet : BaseVC { contentView.addSubview(reactionContainer) reactionContainer.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: contentView) reactionContainer.pin(.top, to: .top, of: contentView, withInset: Values.verySmallSpacing) - // Line - let lineView = UIView() - lineView.backgroundColor = Colors.border.withAlphaComponent(0.1) - lineView.set(.height, to: 0.5) - contentView.addSubview(lineView) - lineView.pin(.leading, to: .leading, of: contentView, withInset: Values.smallSpacing) - lineView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.smallSpacing) - lineView.pin(.top, to: .bottom, of: reactionContainer, 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: lineView, withInset: Values.smallSpacing) + 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: stackView, withInset: Values.smallSpacing) + userListView.pin(.top, to: .bottom, of: line, withInset: 0) } private func populateData() { + self.reactions = [] + self.reactionMap = OrderedDictionary() + if let messageId = viewItem.interaction.uniqueId, let message = TSMessage.fetch(uniqueId: messageId) { + self.reactions = message.reactions as! [ReactMessage] + } for reaction in reactions { if let emoji = reaction.emoji { if !reactionMap.hasValue(forKey: emoji) { reactionMap.append(key: emoji, value: []) } @@ -153,17 +162,31 @@ final class ReactionListSheet : BaseVC { reactionMap.replace(key: emoji, value: value) } } - if selectedReaction == nil { + if (selectedReaction == nil || reactionMap.value(forKey: selectedReaction!) == nil) && reactionMap.orderedKeys.count > 0 { selectedReaction = reactionMap.orderedKeys[0] } } - private func update() { + private func reloadData() { + reactionContainer.reloadData() let seletedData = reactionMap.value(forKey: selectedReaction!)! detailInfoLabel.text = "\(selectedReaction!) ยท \(seletedData.count)" + if thread.isOpenGroup, let threadId = thread.uniqueId, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) { + let isUserModerator = OpenGroupAPIV2.isUserModerator(getUserHexEncodedPublicKey(), for: openGroupV2.room, on: openGroupV2.server) + clearAllButton.isHidden = !isUserModerator + } userListView.reloadData() } + @objc private func update() { + populateData() + if reactions.isEmpty { + close() + return + } + reloadData() + } + // MARK: Interaction override func touchesBegan(_ touches: Set, with event: UIEvent?) { @@ -213,7 +236,7 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { selectedReaction = reactionMap.orderedKeys[indexPath.item] - update() + reloadData() } } @@ -241,11 +264,11 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) guard let reactMessage = reactionMap.value(forKey: selectedReaction!)?[indexPath.row], let publicKey = reactMessage.sender else { return } if publicKey == getUserHexEncodedPublicKey() { delegate?.cancelReact(viewItem, for: selectedReaction!) } - tableView.deselectRow(at: indexPath, animated: true) } } diff --git a/SignalUtilitiesKit/Utilities/Notification+Loki.swift b/SignalUtilitiesKit/Utilities/Notification+Loki.swift index 5f1d9f101..8f5105fdc 100644 --- a/SignalUtilitiesKit/Utilities/Notification+Loki.swift +++ b/SignalUtilitiesKit/Utilities/Notification+Loki.swift @@ -6,6 +6,7 @@ public extension Notification.Name { static let contactOnlineStatusChanged = Notification.Name("contactOnlineStatusChanged") static let threadDeleted = Notification.Name("threadDeleted") static let threadSessionRestoreDevicesChanged = Notification.Name("threadSessionRestoreDevicesChanged") + static let emojiReactsUpdated = Notification.Name("emojiReactsUpdated") // Onboarding static let seedViewed = Notification.Name("seedViewed") // Interaction From a572c4274b43cdf8545aabda044dc9379bf432f9 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 6 Jun 2022 15:56:02 +1000 Subject: [PATCH 029/133] minor fix --- .../Conversations/Views & Modals/ReactionListSheet.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index f57027401..fb688a698 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -228,7 +228,7 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell let item = reactionMap.orderedItems[indexPath.item] cell.data = (item.0, item.1.count) - cell.isSelected = item.0 == selectedReaction! + cell.isCurrentSelection = item.0 == selectedReaction! return cell } @@ -278,7 +278,7 @@ extension ReactionListSheet { fileprivate final class Cell : UICollectionViewCell { var data: (String, Int)? { didSet { update() } } - override var isSelected: Bool { didSet { updateBorder() } } + var isCurrentSelection: Bool? { didSet { updateBorder() } } static let identifier = "ReactionListSheetCell" @@ -337,7 +337,7 @@ extension ReactionListSheet { } private func updateBorder() { - if isSelected { + if isCurrentSelection == true { snContentView.addBorder(with: Colors.accent) } else { snContentView.addBorder(with: .clear) From c33680fe700f8676674757abf88ad090f1379298 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 6 Jun 2022 16:42:35 +1000 Subject: [PATCH 030/133] do not show number when there is only 1 react per emoji in 1-1 convos --- .../Content Views/ReactionContainerView.swift | 10 ++++++---- .../Content Views/ReactionView.swift | 19 ++++++++++++------- .../Message Cells/VisibleMessageCell.swift | 2 +- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index d99aca43a..5a71d539d 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -18,6 +18,7 @@ final class ReactionContainerView : UIView { private var showingAllReactions = false private var isOutgoingMessage = false + private var showNumbers = true private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 var reactions: [(String, (Int, Bool))] = [] @@ -57,9 +58,10 @@ final class ReactionContainerView : UIView { mainStackView.pin(to: self) } - public func update(_ reactions: [(String, (Int, Bool))], isOutgoingMessage: Bool) { + public func update(_ reactions: [(String, (Int, Bool))], isOutgoingMessage: Bool, showNumbers: Bool) { self.reactions = reactions self.isOutgoingMessage = isOutgoingMessage + self.showNumbers = showNumbers prepareForUpdate() if showingAllReactions { updateAllReactions() @@ -93,7 +95,7 @@ final class ReactionContainerView : UIView { } for reaction in displayedReactions { - let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.0, showBorder: reaction.1.1) + let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.0, showBorder: reaction.1.1, showNumber: showNumbers) stackView.addArrangedSubview(reactionView) reactionViews.append(reactionView) } @@ -131,13 +133,13 @@ final class ReactionContainerView : UIView { public func showAllEmojis() { guard !showingAllReactions else { return } showingAllReactions = true - update(reactions, isOutgoingMessage: isOutgoingMessage) + update(reactions, isOutgoingMessage: isOutgoingMessage, showNumbers: showNumbers) } public func showLessEmojis() { guard showingAllReactions else { return } showingAllReactions = false - update(reactions, isOutgoingMessage: isOutgoingMessage) + update(reactions, isOutgoingMessage: isOutgoingMessage, showNumbers: showNumbers) } } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 627195692..3c1c6cd8d 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -4,6 +4,7 @@ final class ReactionButton : UIView { let emoji: String let number: Int let showBorder: Bool + let showNumber: Bool // MARK: Settings private var height: CGFloat = 22 @@ -12,10 +13,11 @@ final class ReactionButton : UIView { private var spacing: CGFloat = Values.verySmallSpacing // MARK: Lifecycle - init(emoji: String, value: Int, showBorder: Bool = false) { + init(emoji: String, value: Int, showBorder: Bool = false, showNumber: Bool = true) { self.emoji = emoji self.number = value self.showBorder = showBorder + self.showNumber = showNumber super.init(frame: CGRect.zero) setUpViewHierarchy() } @@ -33,12 +35,7 @@ final class ReactionButton : UIView { emojiLabel.text = emoji emojiLabel.font = .systemFont(ofSize: fontSize) - let numberLabel = UILabel() - numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.1f", Float(number) / 1000) + "k" - numberLabel.font = .systemFont(ofSize: fontSize) - numberLabel.textColor = Colors.text - - let stackView = UIStackView(arrangedSubviews: [ emojiLabel, numberLabel ]) + let stackView = UIStackView(arrangedSubviews: [ emojiLabel ]) stackView.axis = .horizontal stackView.spacing = spacing stackView.alignment = .center @@ -54,6 +51,14 @@ final class ReactionButton : UIView { if showBorder { self.addBorder(with: Colors.accent) } + + if showNumber || self.number > 1 { + let numberLabel = UILabel() + numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.1f", Float(number) / 1000) + "k" + numberLabel.font = .systemFont(ofSize: fontSize) + numberLabel.textColor = Colors.text + stackView.addArrangedSubview(numberLabel) + } } } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index c189354fd..35b03b8c6 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -467,7 +467,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } } - reactionContainerView.update(reactions.orderedItems, isOutgoingMessage: direction == .outgoing) + reactionContainerView.update(reactions.orderedItems, isOutgoingMessage: direction == .outgoing, showNumbers: thread!.isGroupThread()) } override func layoutSubviews() { From f5572ff6b3cae8bf0969731776efe94b302d3bdb Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 7 Jun 2022 11:01:12 +1000 Subject: [PATCH 031/133] fix long press conficts --- .../Conversations/Message Cells/VisibleMessageCell.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 35b03b8c6..f92f06979 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -1,5 +1,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { + private var isHandlingLongPress: Bool = false private var unloadContent: (() -> Void)? private var previousX: CGFloat = 0 var albumView: MediaAlbumView? @@ -533,6 +534,11 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } @objc func handleLongPress(_ gestureRecognizer: UITapGestureRecognizer) { + if [ .ended, .cancelled, .failed ].contains(gestureRecognizer.state) { + isHandlingLongPress = false + return + } + guard !isHandlingLongPress else { return } guard let viewItem = viewItem else { return } let location = gestureRecognizer.location(in: self) if reactionContainerView.frame.contains(location) { @@ -546,6 +552,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } else { delegate?.handleViewItemLongPressed(viewItem) } + isHandlingLongPress = true } @objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { From d4bca094a17e40b4874ea95ce807353f58217646 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 8 Jun 2022 16:35:45 +1000 Subject: [PATCH 032/133] batch delete emoji reacts --- Session/Conversations/ConversationVC+Interaction.swift | 4 +++- SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 95f180ae7..315885419 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -844,7 +844,9 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func cancelAllReact(reactMessages: [ReactMessage]) { - + guard let groupThread = thread as? TSGroupThread, groupThread.isOpenGroup else { return } + guard let threadId = groupThread.uniqueId, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else { return } + OpenGroupAPIV2.batchDeleteMessages(for: openGroupV2.room, on: openGroupV2.server, messageIds: reactMessages.compactMap{ $0.messageId }) } private func react(_ viewItem: ConversationViewItem, with emoji: String, cancel: Bool) { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift index 97532799d..bbefb9118 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift @@ -368,6 +368,12 @@ public final class OpenGroupAPIV2 : NSObject { } } + public static func batchDeleteMessages(for room: String, on server: String, messageIds: [String]) -> Promise { + let json: JSON = ["ids": messageIds.compactMap{ Int($0) }] + let request = Request(verb: .post, room: room, server: server, endpoint: "delete_messages", parameters: json) + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } + } + private static func parseDeletions(from rawDeletions: [JSON], for room: String, on server: String) -> Promise<[Deletion]> { let storage = SNMessagingKitConfiguration.shared.storage let deletions = rawDeletions.compactMap { Deletion.from($0) } From c36a0187a5803e33c282ac1229f1374c4edc7b2f Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 9 Jun 2022 10:37:45 +1000 Subject: [PATCH 033/133] open group batch delete reactions per emoji --- .../MessageReceiver+Handling.swift | 9 ++++++++- .../Pollers/OpenGroupPollerV2.swift | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 9b72c6c84..b0343fc37 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -386,7 +386,14 @@ extension MessageReceiver { } let reactionMessage = ReactMessage(timestamp: timestamp, authorId: author, emoji: reaction.emoji) reactionMessage.sender = message.sender - if let serverID = message.openGroupServerMessageID { reactionMessage.messageId = "\(serverID)" } + if let serverID = message.openGroupServerMessageID { + reactionMessage.messageId = "\(serverID)" + // Create a lookup between the openGroupServerMessageId and the tsMessage id for easy lookup + // For emoji reacts, the lookup is linking emoji react message server id to the id of the tsMessage that the emoji is reacted to + if let openGroup: OpenGroupV2 = storage.getV2OpenGroup(for: threadID) { + storage.addOpenGroupServerIdLookup(serverID, tsMessageId: tsMessage?.uniqueId, in: openGroup.room, on: openGroup.server, using: transaction) + } + } if let serverHash = message.serverHash { reactionMessage.messageId = serverHash } switch reaction.kind { case .react: diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift index 930361510..15d363cd5 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift @@ -104,8 +104,18 @@ public final class OpenGroupPollerV2 : NSObject { } guard let tsMessage: TSMessage = TSMessage.fetch(uniqueId: messageLookup.tsMessageId, transaction: transaction) else { return } - tsMessage.remove(with: transaction) - storage.removeOpenGroupServerIdLookup(openGroupServerMessageId, in: body.room, on: self.server, using: transaction) + if tsMessage.openGroupServerMessageID == openGroupServerMessageId { // This is the case the deleted message is a tsMessage + tsMessage.remove(with: transaction) + storage.removeOpenGroupServerIdLookup(openGroupServerMessageId, in: body.room, on: self.server, using: transaction) + } else { // Otherwise the deleted message is an emoji react + for reaction in tsMessage.reactions { + if let reactMessage = reaction as? ReactMessage, let messageIdString = reactMessage.messageId, let messageId = UInt64(messageIdString), messageId == openGroupServerMessageId { + tsMessage.removeReaction(reactMessage, transaction: transaction) + storage.removeOpenGroupServerIdLookup(openGroupServerMessageId, in: body.room, on: self.server, using: transaction) + break + } + } + } } } } From e061b14ceb0de6f765c8e730ea1ff0ef53abdac0 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 9 Jun 2022 10:49:46 +1000 Subject: [PATCH 034/133] minor fix --- SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift index bbefb9118..e15d3790d 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift @@ -368,6 +368,7 @@ public final class OpenGroupAPIV2 : NSObject { } } + @discardableResult public static func batchDeleteMessages(for room: String, on server: String, messageIds: [String]) -> Promise { let json: JSON = ["ids": messageIds.compactMap{ Int($0) }] let request = Request(verb: .post, room: room, server: server, endpoint: "delete_messages", parameters: json) From 7fb896fb3096c1bccfd38f9463030973b19615b1 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 9 Jun 2022 14:50:52 +1000 Subject: [PATCH 035/133] WIP: emoji picker --- Session.xcodeproj/project.pbxproj | 8 ++++++++ .../Context Menu/ContextMenuVC+EmojiReactsView.swift | 5 ++++- Session/Conversations/ConversationVC+Interaction.swift | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index d060d7eed..301095b54 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -2083,6 +2083,13 @@ path = Utilities; sourceTree = ""; }; + 7B1B52BD2851ADE1006069F2 /* Emoji Picker */ = { + isa = PBXGroup; + children = ( + ); + path = "Emoji Picker"; + sourceTree = ""; + }; 7B7CB18C270D06350079FF93 /* Views & Modals */ = { isa = PBXGroup; children = ( @@ -2302,6 +2309,7 @@ B835247725C38D190089A44F /* Message Cells */, C328252E25CA54F70062D0A7 /* Context Menu */, B821493625D4D6A7009C0F2A /* Views & Modals */, + 7B1B52BD2851ADE1006069F2 /* Emoji Picker */, C302094625DCDFD3001F572D /* Settings */, ); path = Conversations; diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift index 83adfd966..8b90c0812 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -89,7 +89,10 @@ extension ContextMenuVC { // MARK: Interaction @objc private func handleTap() { dismiss() - work() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { [weak self] in + self?.work() + }) + } } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 315885419..8b74dd596 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -880,6 +880,8 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) { // TODO: to be implemented + print("Ryan Test: showFullEmojiKeyboard") + } func contextMenuDismissed() { From 48ad72b94298d867f10b6c9111e47f0057829dbe Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 10 Jun 2022 16:51:37 +1000 Subject: [PATCH 036/133] WIP: Emoji picker keyboard --- Scripts/EmojiGenerator.swift | 641 ++ Session.xcodeproj/project.pbxproj | 36 + Session/Emoji/Emoji+Available.swift | 111 + Session/Emoji/Emoji+Category.swift | 3776 +++++++++ Session/Emoji/Emoji+Name.swift | 1863 +++++ Session/Emoji/Emoji+SkinTones.swift | 2724 ++++++ Session/Emoji/Emoji.swift | 1863 +++++ Session/Emoji/EmojiWithSkinTones+String.swift | 7269 +++++++++++++++++ Session/Emoji/EmojiWithSkinTones.swift | 73 + .../Utilities/DisplayableText.swift | 2 +- 10 files changed, 18357 insertions(+), 1 deletion(-) create mode 100755 Scripts/EmojiGenerator.swift create mode 100644 Session/Emoji/Emoji+Available.swift create mode 100644 Session/Emoji/Emoji+Category.swift create mode 100644 Session/Emoji/Emoji+Name.swift create mode 100644 Session/Emoji/Emoji+SkinTones.swift create mode 100644 Session/Emoji/Emoji.swift create mode 100644 Session/Emoji/EmojiWithSkinTones+String.swift create mode 100644 Session/Emoji/EmojiWithSkinTones.swift diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift new file mode 100755 index 000000000..c52fedd5d --- /dev/null +++ b/Scripts/EmojiGenerator.swift @@ -0,0 +1,641 @@ +#!/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]? + } + + 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 + 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) + + 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("return nil") + } + 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.rawName)\"") + } + 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 15356b07d..68c1ca8a5 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -158,6 +158,13 @@ 7B93D07327CF19C800811CB6 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D07227CF19C800811CB6 /* MessageRequestsMigration.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 */; }; @@ -1155,6 +1162,13 @@ 7B93D07227CF19C800811CB6 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.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 = ""; }; @@ -2127,6 +2141,20 @@ path = Views; 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 = ( @@ -3665,6 +3693,7 @@ D221A093169C9E5E00537ABF /* Session */ = { isa = PBXGroup; children = ( + 7B9F71CA2852EEE2006DFE7B /* Emoji */, C3F0A58F255C8E3D007BE2A3 /* Meta */, B8B558ED26C4B55F00693325 /* Calls */, C360969C25AD18BA008B62B2 /* Closed Groups */, @@ -4917,6 +4946,7 @@ EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */, 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */, + 7B9F71D82853100A006DFE7B /* EmojiWithSkinTones.swift in Sources */, B84A89BC25DE328A0040017D /* ProfilePictureVC.swift in Sources */, 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */, 451166C01FD86B98000739BA /* AccountManager.swift in Sources */, @@ -4928,6 +4958,7 @@ 340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */, 7BAF54CE27ACCEEC003D12F8 /* Storage+RecentSearchResults.swift in Sources */, 4C090A1B210FD9C7001FD7F9 /* HapticFeedback.swift in Sources */, + 7B9F71D12852EEE2006DFE7B /* EmojiWithSkinTones+String.swift in Sources */, 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, B886B4A92398BA1500211ABE /* QRCode.swift in Sources */, @@ -4996,6 +5027,7 @@ 45F32C222057297A00A300D5 /* MediaDetailViewController.m 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 */, 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */, @@ -5017,14 +5049,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 */, 34D1F0C01F8EC1760066283D /* MessageRecipientStatusUtils.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 */, @@ -5047,6 +5082,7 @@ C31D1DE9252172D4005D4DA8 /* ContactUtilities.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 */, 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..8418c2c4a --- /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 FACE" + case .smiley: return "SMILING FACE WITH OPEN MOUTH" + case .smile: return "SMILING FACE WITH OPEN MOUTH AND SMILING EYES" + case .grin: return "GRINNING FACE WITH SMILING EYES" + case .laughing: return "SMILING FACE WITH OPEN MOUTH AND TIGHTLY-CLOSED EYES" + case .sweatSmile: return "SMILING FACE WITH OPEN MOUTH AND COLD SWEAT" + case .rollingOnTheFloorLaughing: return "ROLLING ON THE FLOOR LAUGHING" + case .joy: return "FACE WITH TEARS OF JOY" + case .slightlySmilingFace: return "SLIGHTLY SMILING FACE" + case .upsideDownFace: return "UPSIDE-DOWN FACE" + case .meltingFace: return "MELTING FACE" + case .wink: return "WINKING FACE" + case .blush: return "SMILING FACE WITH SMILING EYES" + case .innocent: return "SMILING FACE WITH HALO" + case .smilingFaceWith3Hearts: return "SMILING FACE WITH SMILING EYES AND THREE HEARTS" + case .heartEyes: return "SMILING FACE WITH HEART-SHAPED EYES" + case .starStruck: return "GRINNING FACE WITH STAR EYES" + case .kissingHeart: return "FACE THROWING A KISS" + case .kissing: return "KISSING FACE" + case .relaxed: return "WHITE SMILING FACE" + case .kissingClosedEyes: return "KISSING FACE WITH CLOSED EYES" + case .kissingSmilingEyes: return "KISSING FACE WITH SMILING EYES" + case .smilingFaceWithTear: return "SMILING FACE WITH TEAR" + case .yum: return "FACE SAVOURING DELICIOUS FOOD" + case .stuckOutTongue: return "FACE WITH STUCK-OUT TONGUE" + case .stuckOutTongueWinkingEye: return "FACE WITH STUCK-OUT TONGUE AND WINKING EYE" + case .zanyFace: return "GRINNING FACE WITH ONE LARGE AND ONE SMALL EYE" + case .stuckOutTongueClosedEyes: return "FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES" + case .moneyMouthFace: return "MONEY-MOUTH FACE" + case .huggingFace: return "HUGGING FACE" + case .faceWithHandOverMouth: return "SMILING FACE WITH SMILING EYES AND HAND COVERING MOUTH" + case .faceWithOpenEyesAndHandOverMouth: return "FACE WITH OPEN EYES AND HAND OVER MOUTH" + case .faceWithPeekingEye: return "FACE WITH PEEKING EYE" + case .shushingFace: return "FACE WITH FINGER COVERING CLOSED LIPS" + case .thinkingFace: return "THINKING FACE" + case .salutingFace: return "SALUTING FACE" + case .zipperMouthFace: return "ZIPPER-MOUTH FACE" + case .faceWithRaisedEyebrow: return "FACE WITH ONE EYEBROW RAISED" + case .neutralFace: return "NEUTRAL FACE" + case .expressionless: return "EXPRESSIONLESS FACE" + case .noMouth: return "FACE WITHOUT MOUTH" + case .dottedLineFace: return "DOTTED LINE FACE" + case .faceInClouds: return "FACE IN CLOUDS" + case .smirk: return "SMIRKING FACE" + case .unamused: return "UNAMUSED FACE" + case .faceWithRollingEyes: return "FACE WITH ROLLING EYES" + case .grimacing: return "GRIMACING FACE" + case .faceExhaling: return "FACE EXHALING" + case .lyingFace: return "LYING FACE" + case .relieved: return "RELIEVED FACE" + case .pensive: return "PENSIVE FACE" + case .sleepy: return "SLEEPY FACE" + case .droolingFace: return "DROOLING FACE" + case .sleeping: return "SLEEPING FACE" + case .mask: return "FACE WITH MEDICAL MASK" + case .faceWithThermometer: return "FACE WITH THERMOMETER" + case .faceWithHeadBandage: return "FACE WITH HEAD-BANDAGE" + case .nauseatedFace: return "NAUSEATED FACE" + case .faceVomiting: return "FACE WITH OPEN MOUTH VOMITING" + case .sneezingFace: return "SNEEZING FACE" + case .hotFace: return "OVERHEATED FACE" + case .coldFace: return "FREEZING FACE" + case .woozyFace: return "FACE WITH UNEVEN EYES AND WAVY MOUTH" + case .dizzyFace: return "DIZZY FACE" + case .faceWithSpiralEyes: return "FACE WITH SPIRAL EYES" + case .explodingHead: return "SHOCKED FACE WITH EXPLODING HEAD" + case .faceWithCowboyHat: return "FACE WITH COWBOY HAT" + case .partyingFace: return "FACE WITH PARTY HORN AND PARTY HAT" + case .disguisedFace: return "DISGUISED FACE" + case .sunglasses: return "SMILING FACE WITH SUNGLASSES" + case .nerdFace: return "NERD FACE" + case .faceWithMonocle: return "FACE WITH MONOCLE" + case .confused: return "CONFUSED FACE" + case .faceWithDiagonalMouth: return "FACE WITH DIAGONAL MOUTH" + case .worried: return "WORRIED FACE" + case .slightlyFrowningFace: return "SLIGHTLY FROWNING FACE" + case .whiteFrowningFace: return "FROWNING FACE" + case .openMouth: return "FACE WITH OPEN MOUTH" + case .hushed: return "HUSHED FACE" + case .astonished: return "ASTONISHED FACE" + case .flushed: return "FLUSHED FACE" + case .pleadingFace: return "FACE WITH PLEADING EYES" + case .faceHoldingBackTears: return "FACE HOLDING BACK TEARS" + case .frowning: return "FROWNING FACE WITH OPEN MOUTH" + case .anguished: return "ANGUISHED FACE" + case .fearful: return "FEARFUL FACE" + case .coldSweat: return "FACE WITH OPEN MOUTH AND COLD SWEAT" + case .disappointedRelieved: return "DISAPPOINTED BUT RELIEVED FACE" + case .cry: return "CRYING FACE" + case .sob: return "LOUDLY CRYING FACE" + case .scream: return "FACE SCREAMING IN FEAR" + case .confounded: return "CONFOUNDED FACE" + case .persevere: return "PERSEVERING FACE" + case .disappointed: return "DISAPPOINTED FACE" + case .sweat: return "FACE WITH COLD SWEAT" + case .weary: return "WEARY FACE" + case .tiredFace: return "TIRED FACE" + case .yawningFace: return "YAWNING FACE" + case .triumph: return "FACE WITH LOOK OF TRIUMPH" + case .rage: return "POUTING FACE" + case .angry: return "ANGRY FACE" + case .faceWithSymbolsOnMouth: return "SERIOUS FACE WITH SYMBOLS COVERING MOUTH" + case .smilingImp: return "SMILING FACE WITH HORNS" + case .imp: return "IMP" + case .skull: return "SKULL" + case .skullAndCrossbones: return "SKULL AND CROSSBONES" + case .hankey: return "PILE OF POO" + case .clownFace: return "CLOWN FACE" + case .japaneseOgre: return "JAPANESE OGRE" + case .japaneseGoblin: return "JAPANESE GOBLIN" + case .ghost: return "GHOST" + case .alien: return "EXTRATERRESTRIAL ALIEN" + case .spaceInvader: return "ALIEN MONSTER" + case .robotFace: return "ROBOT FACE" + case .smileyCat: return "SMILING CAT FACE WITH OPEN MOUTH" + case .smileCat: return "GRINNING CAT FACE WITH SMILING EYES" + case .joyCat: return "CAT FACE WITH TEARS OF JOY" + case .heartEyesCat: return "SMILING CAT FACE WITH HEART-SHAPED EYES" + case .smirkCat: return "CAT FACE WITH WRY SMILE" + case .kissingCat: return "KISSING CAT FACE WITH CLOSED EYES" + case .screamCat: return "WEARY CAT FACE" + case .cryingCatFace: return "CRYING CAT FACE" + case .poutingCat: return "POUTING CAT FACE" + case .seeNoEvil: return "SEE-NO-EVIL MONKEY" + case .hearNoEvil: return "HEAR-NO-EVIL MONKEY" + case .speakNoEvil: return "SPEAK-NO-EVIL MONKEY" + case .kiss: return "KISS MARK" + case .loveLetter: return "LOVE LETTER" + case .cupid: return "HEART WITH ARROW" + case .giftHeart: return "HEART WITH RIBBON" + case .sparklingHeart: return "SPARKLING HEART" + case .heartpulse: return "GROWING HEART" + case .heartbeat: return "BEATING HEART" + case .revolvingHearts: return "REVOLVING HEARTS" + case .twoHearts: return "TWO HEARTS" + case .heartDecoration: return "HEART DECORATION" + case .heavyHeartExclamationMarkOrnament: return "HEART EXCLAMATION" + case .brokenHeart: return "BROKEN HEART" + case .heartOnFire: return "HEART ON FIRE" + case .mendingHeart: return "MENDING HEART" + case .heart: return "HEAVY BLACK HEART" + case .orangeHeart: return "ORANGE HEART" + case .yellowHeart: return "YELLOW HEART" + case .greenHeart: return "GREEN HEART" + case .blueHeart: return "BLUE HEART" + case .purpleHeart: return "PURPLE HEART" + case .brownHeart: return "BROWN HEART" + case .blackHeart: return "BLACK HEART" + case .whiteHeart: return "WHITE HEART" + case .oneHundred: return "HUNDRED POINTS SYMBOL" + case .anger: return "ANGER SYMBOL" + case .boom: return "COLLISION SYMBOL" + case .dizzy: return "DIZZY SYMBOL" + case .sweatDrops: return "SPLASHING SWEAT SYMBOL" + case .dash: return "DASH SYMBOL" + case .hole: return "HOLE" + case .bomb: return "BOMB" + case .speechBalloon: return "SPEECH BALLOON" + case .eyeInSpeechBubble: return "EYE IN SPEECH BUBBLE" + case .leftSpeechBubble: return "LEFT SPEECH BUBBLE" + case .rightAngerBubble: return "RIGHT ANGER BUBBLE" + case .thoughtBalloon: return "THOUGHT BALLOON" + case .zzz: return "SLEEPING SYMBOL" + case .wave: return "WAVING HAND SIGN" + case .raisedBackOfHand: return "RAISED BACK OF HAND" + case .raisedHandWithFingersSplayed: return "HAND WITH FINGERS SPLAYED" + case .hand: return "RAISED HAND" + case .spockHand: return "RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS" + case .rightwardsHand: return "RIGHTWARDS HAND" + case .leftwardsHand: return "LEFTWARDS HAND" + case .palmDownHand: return "PALM DOWN HAND" + case .palmUpHand: return "PALM UP HAND" + case .okHand: return "OK HAND SIGN" + case .pinchedFingers: return "PINCHED FINGERS" + case .pinchingHand: return "PINCHING HAND" + case .v: return "VICTORY HAND" + case .crossedFingers: return "HAND WITH INDEX AND MIDDLE FINGERS CROSSED" + case .handWithIndexFingerAndThumbCrossed: return "HAND WITH INDEX FINGER AND THUMB CROSSED" + case .iLoveYouHandSign: return "I LOVE YOU HAND SIGN" + case .theHorns: return "SIGN OF THE HORNS" + case .callMeHand: return "CALL ME HAND" + case .pointLeft: return "WHITE LEFT POINTING BACKHAND INDEX" + case .pointRight: return "WHITE RIGHT POINTING BACKHAND INDEX" + case .pointUp2: return "WHITE UP POINTING BACKHAND INDEX" + case .middleFinger: return "REVERSED HAND WITH MIDDLE FINGER EXTENDED" + case .pointDown: return "WHITE DOWN POINTING BACKHAND INDEX" + case .pointUp: return "WHITE UP POINTING INDEX" + case .indexPointingAtTheViewer: return "INDEX POINTING AT THE VIEWER" + case .plusOne: return "THUMBS UP SIGN" + case .negativeOne: return "THUMBS DOWN SIGN" + case .fist: return "RAISED FIST" + case .facepunch: return "FISTED HAND SIGN" + case .leftFacingFist: return "LEFT-FACING FIST" + case .rightFacingFist: return "RIGHT-FACING FIST" + case .clap: return "CLAPPING HANDS SIGN" + case .raisedHands: return "PERSON RAISING BOTH HANDS IN CELEBRATION" + case .heartHands: return "HEART HANDS" + case .openHands: return "OPEN HANDS SIGN" + case .palmsUpTogether: return "PALMS UP TOGETHER" + case .handshake: return "HANDSHAKE" + case .pray: return "PERSON WITH FOLDED HANDS" + case .writingHand: return "WRITING HAND" + case .nailCare: return "NAIL POLISH" + case .selfie: return "SELFIE" + case .muscle: return "FLEXED BICEPS" + case .mechanicalArm: return "MECHANICAL ARM" + case .mechanicalLeg: return "MECHANICAL LEG" + case .leg: return "LEG" + case .foot: return "FOOT" + case .ear: return "EAR" + case .earWithHearingAid: return "EAR WITH HEARING AID" + case .nose: return "NOSE" + case .brain: return "BRAIN" + case .anatomicalHeart: return "ANATOMICAL HEART" + 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" + case .bitingLip: return "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" + case .man: return "MAN" + case .beardedPerson: return "BEARDED PERSON" + case .manWithBeard: return "MAN: BEARD" + case .womanWithBeard: return "WOMAN: BEARD" + case .redHairedMan: return "MAN: RED HAIR" + case .curlyHairedMan: return "MAN: CURLY HAIR" + case .whiteHairedMan: return "MAN: WHITE HAIR" + case .baldMan: return "MAN: BALD" + case .woman: return "WOMAN" + case .redHairedWoman: return "WOMAN: RED HAIR" + case .redHairedPerson: return "PERSON: RED HAIR" + case .curlyHairedWoman: return "WOMAN: CURLY HAIR" + case .curlyHairedPerson: return "PERSON: CURLY HAIR" + case .whiteHairedWoman: return "WOMAN: WHITE HAIR" + case .whiteHairedPerson: return "PERSON: WHITE HAIR" + case .baldWoman: return "WOMAN: BALD" + case .baldPerson: return "PERSON: BALD" + case .blondHairedWoman: return "WOMAN: BLOND HAIR" + case .blondHairedMan: return "MAN: BLOND HAIR" + case .olderAdult: return "OLDER ADULT" + case .olderMan: return "OLDER MAN" + case .olderWoman: return "OLDER WOMAN" + case .personFrowning: return "PERSON FROWNING" + case .manFrowning: return "MAN FROWNING" + case .womanFrowning: return "WOMAN FROWNING" + case .personWithPoutingFace: return "PERSON WITH POUTING FACE" + case .manPouting: return "MAN POUTING" + case .womanPouting: return "WOMAN POUTING" + case .noGood: return "FACE WITH NO GOOD GESTURE" + case .manGesturingNo: return "MAN GESTURING NO" + case .womanGesturingNo: return "WOMAN GESTURING NO" + case .okWoman: return "FACE WITH OK GESTURE" + case .manGesturingOk: return "MAN GESTURING OK" + case .womanGesturingOk: return "WOMAN GESTURING OK" + case .informationDeskPerson: return "INFORMATION DESK PERSON" + case .manTippingHand: return "MAN TIPPING HAND" + case .womanTippingHand: return "WOMAN TIPPING HAND" + case .raisingHand: return "HAPPY PERSON RAISING ONE HAND" + case .manRaisingHand: return "MAN RAISING HAND" + case .womanRaisingHand: return "WOMAN RAISING HAND" + case .deafPerson: return "DEAF PERSON" + case .deafMan: return "DEAF MAN" + case .deafWoman: return "DEAF WOMAN" + case .bow: return "PERSON BOWING DEEPLY" + case .manBowing: return "MAN BOWING" + case .womanBowing: return "WOMAN BOWING" + case .facePalm: return "FACE PALM" + case .manFacepalming: return "MAN FACEPALMING" + case .womanFacepalming: return "WOMAN FACEPALMING" + case .shrug: return "SHRUG" + case .manShrugging: return "MAN SHRUGGING" + case .womanShrugging: return "WOMAN SHRUGGING" + case .healthWorker: return "HEALTH WORKER" + case .maleDoctor: return "MAN HEALTH WORKER" + case .femaleDoctor: return "WOMAN HEALTH WORKER" + case .student: return "STUDENT" + case .maleStudent: return "MAN STUDENT" + case .femaleStudent: return "WOMAN STUDENT" + case .teacher: return "TEACHER" + case .maleTeacher: return "MAN TEACHER" + case .femaleTeacher: return "WOMAN TEACHER" + case .judge: return "JUDGE" + case .maleJudge: return "MAN JUDGE" + case .femaleJudge: return "WOMAN JUDGE" + case .farmer: return "FARMER" + case .maleFarmer: return "MAN FARMER" + case .femaleFarmer: return "WOMAN FARMER" + case .cook: return "COOK" + case .maleCook: return "MAN COOK" + case .femaleCook: return "WOMAN COOK" + case .mechanic: return "MECHANIC" + case .maleMechanic: return "MAN MECHANIC" + case .femaleMechanic: return "WOMAN MECHANIC" + case .factoryWorker: return "FACTORY WORKER" + case .maleFactoryWorker: return "MAN FACTORY WORKER" + case .femaleFactoryWorker: return "WOMAN FACTORY WORKER" + case .officeWorker: return "OFFICE WORKER" + case .maleOfficeWorker: return "MAN OFFICE WORKER" + case .femaleOfficeWorker: return "WOMAN OFFICE WORKER" + case .scientist: return "SCIENTIST" + case .maleScientist: return "MAN SCIENTIST" + case .femaleScientist: return "WOMAN SCIENTIST" + case .technologist: return "TECHNOLOGIST" + case .maleTechnologist: return "MAN TECHNOLOGIST" + case .femaleTechnologist: return "WOMAN TECHNOLOGIST" + case .singer: return "SINGER" + case .maleSinger: return "MAN SINGER" + case .femaleSinger: return "WOMAN SINGER" + case .artist: return "ARTIST" + case .maleArtist: return "MAN ARTIST" + case .femaleArtist: return "WOMAN ARTIST" + case .pilot: return "PILOT" + case .malePilot: return "MAN PILOT" + case .femalePilot: return "WOMAN PILOT" + case .astronaut: return "ASTRONAUT" + case .maleAstronaut: return "MAN ASTRONAUT" + case .femaleAstronaut: return "WOMAN ASTRONAUT" + case .firefighter: return "FIREFIGHTER" + case .maleFirefighter: return "MAN FIREFIGHTER" + case .femaleFirefighter: return "WOMAN FIREFIGHTER" + case .cop: return "POLICE OFFICER" + case .malePoliceOfficer: return "MAN POLICE OFFICER" + case .femalePoliceOfficer: return "WOMAN POLICE OFFICER" + case .sleuthOrSpy: return "DETECTIVE" + case .maleDetective: return "MAN DETECTIVE" + case .femaleDetective: return "WOMAN DETECTIVE" + case .guardsman: return "GUARDSMAN" + case .maleGuard: return "MAN GUARD" + case .femaleGuard: return "WOMAN GUARD" + case .ninja: return "NINJA" + case .constructionWorker: return "CONSTRUCTION WORKER" + case .maleConstructionWorker: return "MAN CONSTRUCTION WORKER" + case .femaleConstructionWorker: return "WOMAN CONSTRUCTION WORKER" + case .personWithCrown: return "PERSON WITH CROWN" + case .prince: return "PRINCE" + case .princess: return "PRINCESS" + case .manWithTurban: return "MAN WITH TURBAN" + case .manWearingTurban: return "MAN WEARING TURBAN" + case .womanWearingTurban: return "WOMAN WEARING TURBAN" + case .manWithGuaPiMao: return "MAN WITH GUA PI MAO" + case .personWithHeadscarf: return "PERSON WITH HEADSCARF" + case .personInTuxedo: return "MAN IN TUXEDO" + case .manInTuxedo: return "MAN IN TUXEDO" + case .womanInTuxedo: return "WOMAN IN TUXEDO" + case .brideWithVeil: return "BRIDE WITH VEIL" + case .manWithVeil: return "MAN WITH VEIL" + case .womanWithVeil: return "WOMAN WITH VEIL" + case .pregnantWoman: return "PREGNANT WOMAN" + case .pregnantMan: return "PREGNANT MAN" + case .pregnantPerson: return "PREGNANT PERSON" + case .breastFeeding: return "BREAST-FEEDING" + case .womanFeedingBaby: return "WOMAN FEEDING BABY" + case .manFeedingBaby: return "MAN FEEDING BABY" + case .personFeedingBaby: return "PERSON FEEDING BABY" + case .angel: return "BABY ANGEL" + case .santa: return "FATHER CHRISTMAS" + case .mrsClaus: return "MOTHER CHRISTMAS" + case .mxClaus: return "MX CLAUS" + case .superhero: return "SUPERHERO" + case .maleSuperhero: return "MAN SUPERHERO" + case .femaleSuperhero: return "WOMAN SUPERHERO" + case .supervillain: return "SUPERVILLAIN" + case .maleSupervillain: return "MAN SUPERVILLAIN" + case .femaleSupervillain: return "WOMAN SUPERVILLAIN" + case .mage: return "MAGE" + case .maleMage: return "MAN MAGE" + case .femaleMage: return "WOMAN MAGE" + case .fairy: return "FAIRY" + case .maleFairy: return "MAN FAIRY" + case .femaleFairy: return "WOMAN FAIRY" + case .vampire: return "VAMPIRE" + case .maleVampire: return "MAN VAMPIRE" + case .femaleVampire: return "WOMAN VAMPIRE" + case .merperson: return "MERPERSON" + case .merman: return "MERMAN" + case .mermaid: return "MERMAID" + case .elf: return "ELF" + case .maleElf: return "MAN ELF" + case .femaleElf: return "WOMAN ELF" + case .genie: return "GENIE" + case .maleGenie: return "MAN GENIE" + case .femaleGenie: return "WOMAN GENIE" + case .zombie: return "ZOMBIE" + case .maleZombie: return "MAN ZOMBIE" + case .femaleZombie: return "WOMAN ZOMBIE" + case .troll: return "TROLL" + case .massage: return "FACE MASSAGE" + case .manGettingMassage: return "MAN GETTING MASSAGE" + case .womanGettingMassage: return "WOMAN GETTING MASSAGE" + case .haircut: return "HAIRCUT" + case .manGettingHaircut: return "MAN GETTING HAIRCUT" + case .womanGettingHaircut: return "WOMAN GETTING HAIRCUT" + case .walking: return "PEDESTRIAN" + case .manWalking: return "MAN WALKING" + case .womanWalking: return "WOMAN WALKING" + case .standingPerson: return "STANDING PERSON" + case .manStanding: return "MAN STANDING" + case .womanStanding: return "WOMAN STANDING" + case .kneelingPerson: return "KNEELING PERSON" + case .manKneeling: return "MAN KNEELING" + case .womanKneeling: return "WOMAN KNEELING" + case .personWithProbingCane: return "PERSON WITH WHITE CANE" + case .manWithProbingCane: return "MAN WITH WHITE CANE" + case .womanWithProbingCane: return "WOMAN WITH WHITE CANE" + case .personInMotorizedWheelchair: return "PERSON IN MOTORIZED WHEELCHAIR" + case .manInMotorizedWheelchair: return "MAN IN MOTORIZED WHEELCHAIR" + case .womanInMotorizedWheelchair: return "WOMAN IN MOTORIZED WHEELCHAIR" + case .personInManualWheelchair: return "PERSON IN MANUAL WHEELCHAIR" + case .manInManualWheelchair: return "MAN IN MANUAL WHEELCHAIR" + case .womanInManualWheelchair: return "WOMAN IN MANUAL WHEELCHAIR" + case .runner: return "RUNNER" + case .manRunning: return "MAN RUNNING" + case .womanRunning: return "WOMAN RUNNING" + case .dancer: return "DANCER" + case .manDancing: return "MAN DANCING" + case .manInBusinessSuitLevitating: return "PERSON IN SUIT LEVITATING" + case .dancers: return "WOMAN WITH BUNNY EARS" + case .menWithBunnyEarsPartying: return "MEN WITH BUNNY EARS" + case .womenWithBunnyEarsPartying: return "WOMEN WITH BUNNY EARS" + case .personInSteamyRoom: return "PERSON IN STEAMY ROOM" + case .manInSteamyRoom: return "MAN IN STEAMY ROOM" + case .womanInSteamyRoom: return "WOMAN IN STEAMY ROOM" + case .personClimbing: return "PERSON CLIMBING" + case .manClimbing: return "MAN CLIMBING" + case .womanClimbing: return "WOMAN CLIMBING" + case .fencer: return "FENCER" + case .horseRacing: return "HORSE RACING" + case .skier: return "SKIER" + case .snowboarder: return "SNOWBOARDER" + case .golfer: return "PERSON GOLFING" + case .manGolfing: return "MAN GOLFING" + case .womanGolfing: return "WOMAN GOLFING" + case .surfer: return "SURFER" + case .manSurfing: return "MAN SURFING" + case .womanSurfing: return "WOMAN SURFING" + case .rowboat: return "ROWBOAT" + case .manRowingBoat: return "MAN ROWING BOAT" + case .womanRowingBoat: return "WOMAN ROWING BOAT" + case .swimmer: return "SWIMMER" + case .manSwimming: return "MAN SWIMMING" + case .womanSwimming: return "WOMAN SWIMMING" + case .personWithBall: return "PERSON BOUNCING BALL" + case .manBouncingBall: return "MAN BOUNCING BALL" + case .womanBouncingBall: return "WOMAN BOUNCING BALL" + case .weightLifter: return "PERSON LIFTING WEIGHTS" + case .manLiftingWeights: return "MAN LIFTING WEIGHTS" + case .womanLiftingWeights: return "WOMAN LIFTING WEIGHTS" + case .bicyclist: return "BICYCLIST" + case .manBiking: return "MAN BIKING" + case .womanBiking: return "WOMAN BIKING" + case .mountainBicyclist: return "MOUNTAIN BICYCLIST" + case .manMountainBiking: return "MAN MOUNTAIN BIKING" + case .womanMountainBiking: return "WOMAN MOUNTAIN BIKING" + case .personDoingCartwheel: return "PERSON DOING CARTWHEEL" + case .manCartwheeling: return "MAN CARTWHEELING" + case .womanCartwheeling: return "WOMAN CARTWHEELING" + case .wrestlers: return "WRESTLERS" + case .manWrestling: return "MEN WRESTLING" + case .womanWrestling: return "WOMEN WRESTLING" + case .waterPolo: return "WATER POLO" + case .manPlayingWaterPolo: return "MAN PLAYING WATER POLO" + case .womanPlayingWaterPolo: return "WOMAN PLAYING WATER POLO" + case .handball: return "HANDBALL" + case .manPlayingHandball: return "MAN PLAYING HANDBALL" + case .womanPlayingHandball: return "WOMAN PLAYING HANDBALL" + case .juggling: return "JUGGLING" + case .manJuggling: return "MAN JUGGLING" + case .womanJuggling: return "WOMAN JUGGLING" + case .personInLotusPosition: return "PERSON IN LOTUS POSITION" + case .manInLotusPosition: return "MAN IN LOTUS POSITION" + case .womanInLotusPosition: return "WOMAN IN LOTUS POSITION" + case .bath: return "BATH" + case .sleepingAccommodation: return "SLEEPING ACCOMMODATION" + case .peopleHoldingHands: return "PEOPLE HOLDING HANDS" + case .twoWomenHoldingHands: return "TWO WOMEN HOLDING HANDS" + case .manAndWomanHoldingHands: return "MAN AND WOMAN HOLDING HANDS" + case .twoMenHoldingHands: return "TWO MEN HOLDING HANDS" + case .personKissPerson: return "KISS" + case .womanKissMan: return "KISS: WOMAN, MAN" + case .manKissMan: return "KISS: MAN, MAN" + case .womanKissWoman: return "KISS: WOMAN, WOMAN" + case .personHeartPerson: return "COUPLE WITH HEART" + case .womanHeartMan: return "COUPLE WITH HEART: WOMAN, MAN" + case .manHeartMan: return "COUPLE WITH HEART: MAN, MAN" + case .womanHeartWoman: return "COUPLE WITH HEART: WOMAN, WOMAN" + case .family: return "FAMILY" + case .manWomanBoy: return "FAMILY: MAN, WOMAN, BOY" + case .manWomanGirl: return "FAMILY: MAN, WOMAN, GIRL" + case .manWomanGirlBoy: return "FAMILY: MAN, WOMAN, GIRL, BOY" + case .manWomanBoyBoy: return "FAMILY: MAN, WOMAN, BOY, BOY" + case .manWomanGirlGirl: return "FAMILY: MAN, WOMAN, GIRL, GIRL" + case .manManBoy: return "FAMILY: MAN, MAN, BOY" + case .manManGirl: return "FAMILY: MAN, MAN, GIRL" + case .manManGirlBoy: return "FAMILY: MAN, MAN, GIRL, BOY" + case .manManBoyBoy: return "FAMILY: MAN, MAN, BOY, BOY" + case .manManGirlGirl: return "FAMILY: MAN, MAN, GIRL, GIRL" + case .womanWomanBoy: return "FAMILY: WOMAN, WOMAN, BOY" + case .womanWomanGirl: return "FAMILY: WOMAN, WOMAN, GIRL" + case .womanWomanGirlBoy: return "FAMILY: WOMAN, WOMAN, GIRL, BOY" + case .womanWomanBoyBoy: return "FAMILY: WOMAN, WOMAN, BOY, BOY" + case .womanWomanGirlGirl: return "FAMILY: WOMAN, WOMAN, GIRL, GIRL" + case .manBoy: return "FAMILY: MAN, BOY" + case .manBoyBoy: return "FAMILY: MAN, BOY, BOY" + case .manGirl: return "FAMILY: MAN, GIRL" + case .manGirlBoy: return "FAMILY: MAN, GIRL, BOY" + case .manGirlGirl: return "FAMILY: MAN, GIRL, GIRL" + case .womanBoy: return "FAMILY: WOMAN, BOY" + case .womanBoyBoy: return "FAMILY: WOMAN, BOY, BOY" + case .womanGirl: return "FAMILY: WOMAN, GIRL" + case .womanGirlBoy: return "FAMILY: WOMAN, GIRL, BOY" + case .womanGirlGirl: return "FAMILY: WOMAN, GIRL, GIRL" + case .speakingHeadInSilhouette: return "SPEAKING HEAD" + case .bustInSilhouette: return "BUST IN SILHOUETTE" + case .bustsInSilhouette: return "BUSTS IN SILHOUETTE" + case .peopleHugging: return "PEOPLE HUGGING" + case .footprints: return "FOOTPRINTS" + case .skinTone2: return "EMOJI MODIFIER FITZPATRICK TYPE-1-2" + case .skinTone3: return "EMOJI MODIFIER FITZPATRICK TYPE-3" + case .skinTone4: return "EMOJI MODIFIER FITZPATRICK TYPE-4" + case .skinTone5: return "EMOJI MODIFIER FITZPATRICK TYPE-5" + case .skinTone6: return "EMOJI MODIFIER FITZPATRICK TYPE-6" + case .monkeyFace: return "MONKEY FACE" + case .monkey: return "MONKEY" + case .gorilla: return "GORILLA" + case .orangutan: return "ORANGUTAN" + case .dog: return "DOG FACE" + case .dog2: return "DOG" + case .guideDog: return "GUIDE DOG" + case .serviceDog: return "SERVICE DOG" + case .poodle: return "POODLE" + case .wolf: return "WOLF FACE" + case .foxFace: return "FOX FACE" + case .raccoon: return "RACCOON" + case .cat: return "CAT FACE" + case .cat2: return "CAT" + case .blackCat: return "BLACK CAT" + case .lionFace: return "LION FACE" + case .tiger: return "TIGER FACE" + case .tiger2: return "TIGER" + case .leopard: return "LEOPARD" + case .horse: return "HORSE FACE" + case .racehorse: return "HORSE" + case .unicornFace: return "UNICORN FACE" + case .zebraFace: return "ZEBRA FACE" + case .deer: return "DEER" + case .bison: return "BISON" + case .cow: return "COW FACE" + case .ox: return "OX" + case .waterBuffalo: return "WATER BUFFALO" + case .cow2: return "COW" + case .pig: return "PIG FACE" + case .pig2: return "PIG" + case .boar: return "BOAR" + case .pigNose: return "PIG NOSE" + case .ram: return "RAM" + case .sheep: return "SHEEP" + case .goat: return "GOAT" + case .dromedaryCamel: return "DROMEDARY CAMEL" + case .camel: return "BACTRIAN CAMEL" + case .llama: return "LLAMA" + case .giraffeFace: return "GIRAFFE FACE" + case .elephant: return "ELEPHANT" + case .mammoth: return "MAMMOTH" + case .rhinoceros: return "RHINOCEROS" + case .hippopotamus: return "HIPPOPOTAMUS" + case .mouse: return "MOUSE FACE" + case .mouse2: return "MOUSE" + case .rat: return "RAT" + case .hamster: return "HAMSTER FACE" + case .rabbit: return "RABBIT FACE" + case .rabbit2: return "RABBIT" + case .chipmunk: return "CHIPMUNK" + case .beaver: return "BEAVER" + case .hedgehog: return "HEDGEHOG" + case .bat: return "BAT" + case .bear: return "BEAR FACE" + case .polarBear: return "POLAR BEAR" + case .koala: return "KOALA" + case .pandaFace: return "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" + case .turkey: return "TURKEY" + case .chicken: return "CHICKEN" + case .rooster: return "ROOSTER" + case .hatchingChick: return "HATCHING CHICK" + case .babyChick: return "BABY CHICK" + case .hatchedChick: return "FRONT-FACING BABY CHICK" + case .bird: return "BIRD" + case .penguin: return "PENGUIN" + case .doveOfPeace: return "DOVE" + 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 FACE" + case .crocodile: return "CROCODILE" + case .turtle: return "TURTLE" + case .lizard: return "LIZARD" + case .snake: return "SNAKE" + case .dragonFace: return "DRAGON FACE" + case .dragon: return "DRAGON" + case .sauropod: return "SAUROPOD" + case .tRex: return "T-REX" + case .whale: return "SPOUTING WHALE" + case .whale2: return "WHALE" + case .dolphin: return "DOLPHIN" + case .seal: return "SEAL" + case .fish: return "FISH" + case .tropicalFish: return "TROPICAL FISH" + case .blowfish: return "BLOWFISH" + case .shark: return "SHARK" + case .octopus: return "OCTOPUS" + case .shell: return "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 "HONEYBEE" + case .beetle: return "BEETLE" + case .ladybug: return "LADY BEETLE" + case .cricket: return "CRICKET" + case .cockroach: return "COCKROACH" + case .spider: return "SPIDER" + case .spiderWeb: return "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 "CHERRY BLOSSOM" + case .whiteFlower: return "WHITE FLOWER" + case .lotus: return "LOTUS" + case .rosette: return "ROSETTE" + case .rose: return "ROSE" + case .wiltedFlower: return "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" + case .evergreenTree: return "EVERGREEN TREE" + case .deciduousTree: return "DECIDUOUS TREE" + case .palmTree: return "PALM TREE" + case .cactus: return "CACTUS" + case .earOfRice: return "EAR OF RICE" + case .herb: return "HERB" + case .shamrock: return "SHAMROCK" + case .fourLeafClover: return "FOUR LEAF CLOVER" + case .mapleLeaf: return "MAPLE LEAF" + case .fallenLeaf: return "FALLEN LEAF" + case .leaves: return "LEAF FLUTTERING IN WIND" + case .emptyNest: return "EMPTY NEST" + case .nestWithEggs: return "NEST WITH EGGS" + 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" + case .greenApple: return "GREEN APPLE" + 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" + case .potato: return "POTATO" + case .carrot: return "CARROT" + case .corn: return "EAR OF MAIZE" + case .hotPepper: return "HOT PEPPER" + case .bellPepper: return "BELL PEPPER" + case .cucumber: return "CUCUMBER" + case .leafyGreen: return "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" + case .flatbread: return "FLATBREAD" + case .pretzel: return "PRETZEL" + case .bagel: return "BAGEL" + case .pancakes: return "PANCAKES" + case .waffle: return "WAFFLE" + case .cheeseWedge: return "CHEESE WEDGE" + case .meatOnBone: return "MEAT ON BONE" + case .poultryLeg: return "POULTRY LEG" + case .cutOfMeat: return "CUT OF MEAT" + case .bacon: return "BACON" + case .hamburger: return "HAMBURGER" + case .fries: return "FRENCH FRIES" + case .pizza: return "SLICE OF PIZZA" + case .hotdog: return "HOT DOG" + case .sandwich: return "SANDWICH" + case .taco: return "TACO" + case .burrito: return "BURRITO" + case .tamale: return "TAMALE" + case .stuffedFlatbread: return "STUFFED FLATBREAD" + case .falafel: return "FALAFEL" + case .egg: return "EGG" + case .friedEgg: return "COOKING" + case .shallowPanOfFood: return "SHALLOW PAN OF FOOD" + case .stew: return "POT OF FOOD" + case .fondue: return "FONDUE" + case .bowlWithSpoon: return "BOWL WITH SPOON" + case .greenSalad: return "GREEN SALAD" + case .popcorn: return "POPCORN" + case .butter: return "BUTTER" + case .salt: return "SALT SHAKER" + case .cannedFood: return "CANNED FOOD" + case .bento: return "BENTO BOX" + case .riceCracker: return "RICE CRACKER" + case .riceBall: return "RICE BALL" + case .rice: return "COOKED RICE" + case .curry: return "CURRY AND RICE" + case .ramen: return "STEAMING BOWL" + case .spaghetti: return "SPAGHETTI" + case .sweetPotato: return "ROASTED SWEET POTATO" + case .oden: return "ODEN" + case .sushi: return "SUSHI" + case .friedShrimp: return "FRIED SHRIMP" + case .fishCake: return "FISH CAKE WITH SWIRL DESIGN" + case .moonCake: return "MOON CAKE" + case .dango: return "DANGO" + case .dumpling: return "DUMPLING" + case .fortuneCookie: return "FORTUNE COOKIE" + case .takeoutBox: return "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 "SOFT ICE CREAM" + case .shavedIce: return "SHAVED ICE" + case .iceCream: return "ICE CREAM" + case .doughnut: return "DOUGHNUT" + case .cookie: return "COOKIE" + case .birthday: return "BIRTHDAY CAKE" + case .cake: return "SHORTCAKE" + case .cupcake: return "CUPCAKE" + case .pie: return "PIE" + case .chocolateBar: return "CHOCOLATE BAR" + case .candy: return "CANDY" + case .lollipop: return "LOLLIPOP" + case .custard: return "CUSTARD" + case .honeyPot: return "HONEY POT" + case .babyBottle: return "BABY BOTTLE" + case .glassOfMilk: return "GLASS OF MILK" + case .coffee: return "HOT BEVERAGE" + case .teapot: return "TEAPOT" + case .tea: return "TEACUP WITHOUT HANDLE" + case .sake: return "SAKE BOTTLE AND CUP" + case .champagne: return "BOTTLE WITH POPPING CORK" + case .wineGlass: return "WINE GLASS" + case .cocktail: return "COCKTAIL GLASS" + case .tropicalDrink: return "TROPICAL DRINK" + case .beer: return "BEER MUG" + case .beers: return "CLINKING BEER MUGS" + case .clinkingGlasses: return "CLINKING GLASSES" + case .tumblerGlass: return "TUMBLER GLASS" + case .pouringLiquid: return "POURING LIQUID" + case .cupWithStraw: return "CUP WITH STRAW" + case .bubbleTea: return "BUBBLE TEA" + case .beverageBox: return "BEVERAGE BOX" + case .mateDrink: return "MATE DRINK" + case .iceCube: return "ICE CUBE" + case .chopsticks: return "CHOPSTICKS" + case .knifeForkPlate: return "FORK AND KNIFE WITH PLATE" + case .forkAndKnife: return "FORK AND KNIFE" + case .spoon: return "SPOON" + case .hocho: return "HOCHO" + case .jar: return "JAR" + case .amphora: return "AMPHORA" + case .earthAfrica: return "EARTH GLOBE EUROPE-AFRICA" + case .earthAmericas: return "EARTH GLOBE AMERICAS" + case .earthAsia: return "EARTH GLOBE ASIA-AUSTRALIA" + case .globeWithMeridians: return "GLOBE WITH MERIDIANS" + case .worldMap: return "WORLD MAP" + case .japan: return "SILHOUETTE OF JAPAN" + case .compass: return "COMPASS" + case .snowCappedMountain: return "SNOW-CAPPED MOUNTAIN" + case .mountain: return "MOUNTAIN" + case .volcano: return "VOLCANO" + case .mountFuji: return "MOUNT FUJI" + case .camping: return "CAMPING" + case .beachWithUmbrella: return "BEACH WITH UMBRELLA" + case .desert: return "DESERT" + case .desertIsland: return "DESERT ISLAND" + case .nationalPark: return "NATIONAL PARK" + case .stadium: return "STADIUM" + case .classicalBuilding: return "CLASSICAL BUILDING" + case .buildingConstruction: return "BUILDING CONSTRUCTION" + case .bricks: return "BRICK" + case .rock: return "ROCK" + case .wood: return "WOOD" + case .hut: return "HUT" + case .houseBuildings: return "HOUSES" + case .derelictHouseBuilding: return "DERELICT HOUSE" + case .house: return "HOUSE BUILDING" + case .houseWithGarden: return "HOUSE WITH GARDEN" + case .office: return "OFFICE BUILDING" + case .postOffice: return "JAPANESE POST OFFICE" + case .europeanPostOffice: return "EUROPEAN POST OFFICE" + case .hospital: return "HOSPITAL" + case .bank: return "BANK" + case .hotel: return "HOTEL" + case .loveHotel: return "LOVE HOTEL" + case .convenienceStore: return "CONVENIENCE STORE" + case .school: return "SCHOOL" + case .departmentStore: return "DEPARTMENT STORE" + case .factory: return "FACTORY" + case .japaneseCastle: return "JAPANESE CASTLE" + case .europeanCastle: return "EUROPEAN CASTLE" + case .wedding: return "WEDDING" + case .tokyoTower: return "TOKYO TOWER" + case .statueOfLiberty: return "STATUE OF LIBERTY" + case .church: return "CHURCH" + case .mosque: return "MOSQUE" + case .hinduTemple: return "HINDU TEMPLE" + case .synagogue: return "SYNAGOGUE" + case .shintoShrine: return "SHINTO SHRINE" + case .kaaba: return "KAABA" + case .fountain: return "FOUNTAIN" + case .tent: return "TENT" + case .foggy: return "FOGGY" + case .nightWithStars: return "NIGHT WITH STARS" + case .cityscape: return "CITYSCAPE" + case .sunriseOverMountains: return "SUNRISE OVER MOUNTAINS" + case .sunrise: return "SUNRISE" + case .citySunset: return "CITYSCAPE AT DUSK" + case .citySunrise: return "SUNSET OVER BUILDINGS" + case .bridgeAtNight: return "BRIDGE AT NIGHT" + case .hotsprings: return "HOT SPRINGS" + case .carouselHorse: return "CAROUSEL HORSE" + case .playgroundSlide: return "PLAYGROUND SLIDE" + case .ferrisWheel: return "FERRIS WHEEL" + case .rollerCoaster: return "ROLLER COASTER" + case .barber: return "BARBER POLE" + case .circusTent: return "CIRCUS TENT" + case .steamLocomotive: return "STEAM LOCOMOTIVE" + case .railwayCar: return "RAILWAY CAR" + case .bullettrainSide: return "HIGH-SPEED TRAIN" + case .bullettrainFront: return "HIGH-SPEED TRAIN WITH BULLET NOSE" + case .train2: return "TRAIN" + case .metro: return "METRO" + case .lightRail: return "LIGHT RAIL" + case .station: return "STATION" + case .tram: return "TRAM" + case .monorail: return "MONORAIL" + case .mountainRailway: return "MOUNTAIN RAILWAY" + case .train: return "TRAM CAR" + case .bus: return "BUS" + case .oncomingBus: return "ONCOMING BUS" + case .trolleybus: return "TROLLEYBUS" + case .minibus: return "MINIBUS" + case .ambulance: return "AMBULANCE" + case .fireEngine: return "FIRE ENGINE" + case .policeCar: return "POLICE CAR" + case .oncomingPoliceCar: return "ONCOMING POLICE CAR" + case .taxi: return "TAXI" + case .oncomingTaxi: return "ONCOMING TAXI" + case .car: return "AUTOMOBILE" + case .oncomingAutomobile: return "ONCOMING AUTOMOBILE" + case .blueCar: return "RECREATIONAL VEHICLE" + case .pickupTruck: return "PICKUP TRUCK" + case .truck: return "DELIVERY TRUCK" + case .articulatedLorry: return "ARTICULATED LORRY" + case .tractor: return "TRACTOR" + case .racingCar: return "RACING CAR" + case .racingMotorcycle: return "MOTORCYCLE" + case .motorScooter: return "MOTOR SCOOTER" + case .manualWheelchair: return "MANUAL WHEELCHAIR" + case .motorizedWheelchair: return "MOTORIZED WHEELCHAIR" + case .autoRickshaw: return "AUTO RICKSHAW" + case .bike: return "BICYCLE" + case .scooter: return "SCOOTER" + case .skateboard: return "SKATEBOARD" + case .rollerSkate: return "ROLLER SKATE" + case .busstop: return "BUS STOP" + case .motorway: return "MOTORWAY" + case .railwayTrack: return "RAILWAY TRACK" + case .oilDrum: return "OIL DRUM" + case .fuelpump: return "FUEL PUMP" + case .wheel: return "WHEEL" + case .rotatingLight: return "POLICE CARS REVOLVING LIGHT" + case .trafficLight: return "HORIZONTAL TRAFFIC LIGHT" + case .verticalTrafficLight: return "VERTICAL TRAFFIC LIGHT" + case .octagonalSign: return "OCTAGONAL SIGN" + case .construction: return "CONSTRUCTION SIGN" + case .anchor: return "ANCHOR" + case .ringBuoy: return "RING BUOY" + case .boat: return "SAILBOAT" + case .canoe: return "CANOE" + case .speedboat: return "SPEEDBOAT" + case .passengerShip: return "PASSENGER SHIP" + case .ferry: return "FERRY" + case .motorBoat: return "MOTOR BOAT" + case .ship: return "SHIP" + case .airplane: return "AIRPLANE" + case .smallAirplane: return "SMALL AIRPLANE" + case .airplaneDeparture: return "AIRPLANE DEPARTURE" + case .airplaneArriving: return "AIRPLANE ARRIVING" + case .parachute: return "PARACHUTE" + case .seat: return "SEAT" + case .helicopter: return "HELICOPTER" + case .suspensionRailway: return "SUSPENSION RAILWAY" + case .mountainCableway: return "MOUNTAIN CABLEWAY" + case .aerialTramway: return "AERIAL TRAMWAY" + case .satellite: return "SATELLITE" + case .rocket: return "ROCKET" + case .flyingSaucer: return "FLYING SAUCER" + case .bellhopBell: return "BELLHOP BELL" + case .luggage: return "LUGGAGE" + case .hourglass: return "HOURGLASS" + case .hourglassFlowingSand: return "HOURGLASS WITH FLOWING SAND" + case .watch: return "WATCH" + case .alarmClock: return "ALARM CLOCK" + case .stopwatch: return "STOPWATCH" + case .timerClock: return "TIMER CLOCK" + case .mantelpieceClock: return "MANTELPIECE CLOCK" + case .clock12: return "CLOCK FACE TWELVE OCLOCK" + case .clock1230: return "CLOCK FACE TWELVE-THIRTY" + case .clock1: return "CLOCK FACE ONE OCLOCK" + case .clock130: return "CLOCK FACE ONE-THIRTY" + case .clock2: return "CLOCK FACE TWO OCLOCK" + case .clock230: return "CLOCK FACE TWO-THIRTY" + case .clock3: return "CLOCK FACE THREE OCLOCK" + case .clock330: return "CLOCK FACE THREE-THIRTY" + case .clock4: return "CLOCK FACE FOUR OCLOCK" + case .clock430: return "CLOCK FACE FOUR-THIRTY" + case .clock5: return "CLOCK FACE FIVE OCLOCK" + case .clock530: return "CLOCK FACE FIVE-THIRTY" + case .clock6: return "CLOCK FACE SIX OCLOCK" + case .clock630: return "CLOCK FACE SIX-THIRTY" + case .clock7: return "CLOCK FACE SEVEN OCLOCK" + case .clock730: return "CLOCK FACE SEVEN-THIRTY" + case .clock8: return "CLOCK FACE EIGHT OCLOCK" + case .clock830: return "CLOCK FACE EIGHT-THIRTY" + case .clock9: return "CLOCK FACE NINE OCLOCK" + case .clock930: return "CLOCK FACE NINE-THIRTY" + case .clock10: return "CLOCK FACE TEN OCLOCK" + case .clock1030: return "CLOCK FACE TEN-THIRTY" + case .clock11: return "CLOCK FACE ELEVEN OCLOCK" + case .clock1130: return "CLOCK FACE ELEVEN-THIRTY" + case .newMoon: return "NEW MOON SYMBOL" + case .waxingCrescentMoon: return "WAXING CRESCENT MOON SYMBOL" + case .firstQuarterMoon: return "FIRST QUARTER MOON SYMBOL" + case .moon: return "WAXING GIBBOUS MOON SYMBOL" + case .fullMoon: return "FULL MOON SYMBOL" + case .waningGibbousMoon: return "WANING GIBBOUS MOON SYMBOL" + case .lastQuarterMoon: return "LAST QUARTER MOON SYMBOL" + case .waningCrescentMoon: return "WANING CRESCENT MOON SYMBOL" + case .crescentMoon: return "CRESCENT MOON" + case .newMoonWithFace: return "NEW MOON WITH FACE" + case .firstQuarterMoonWithFace: return "FIRST QUARTER MOON WITH FACE" + case .lastQuarterMoonWithFace: return "LAST QUARTER MOON WITH FACE" + case .thermometer: return "THERMOMETER" + case .sunny: return "BLACK SUN WITH RAYS" + case .fullMoonWithFace: return "FULL MOON WITH FACE" + case .sunWithFace: return "SUN WITH FACE" + case .ringedPlanet: return "RINGED PLANET" + case .star: return "WHITE MEDIUM STAR" + case .star2: return "GLOWING STAR" + case .stars: return "SHOOTING STAR" + case .milkyWay: return "MILKY WAY" + case .cloud: return "CLOUD" + case .partlySunny: return "SUN BEHIND CLOUD" + case .thunderCloudAndRain: return "CLOUD WITH LIGHTNING AND RAIN" + case .mostlySunny: return "SUN BEHIND SMALL CLOUD" + case .barelySunny: return "SUN BEHIND LARGE CLOUD" + case .partlySunnyRain: return "SUN BEHIND RAIN CLOUD" + case .rainCloud: return "CLOUD WITH RAIN" + case .snowCloud: return "CLOUD WITH SNOW" + case .lightning: return "CLOUD WITH LIGHTNING" + case .tornado: return "TORNADO" + case .fog: return "FOG" + case .windBlowingFace: return "WIND FACE" + case .cyclone: return "CYCLONE" + case .rainbow: return "RAINBOW" + case .closedUmbrella: return "CLOSED UMBRELLA" + case .umbrella: return "UMBRELLA" + case .umbrellaWithRainDrops: return "UMBRELLA WITH RAIN DROPS" + case .umbrellaOnGround: return "UMBRELLA ON GROUND" + case .zap: return "HIGH VOLTAGE SIGN" + case .snowflake: return "SNOWFLAKE" + case .snowman: return "SNOWMAN" + case .snowmanWithoutSnow: return "SNOWMAN WITHOUT SNOW" + case .comet: return "COMET" + case .fire: return "FIRE" + case .droplet: return "DROPLET" + case .ocean: return "WATER WAVE" + case .jackOLantern: return "JACK-O-LANTERN" + case .christmasTree: return "CHRISTMAS TREE" + case .fireworks: return "FIREWORKS" + case .sparkler: return "FIREWORK SPARKLER" + case .firecracker: return "FIRECRACKER" + case .sparkles: return "SPARKLES" + case .balloon: return "BALLOON" + case .tada: return "PARTY POPPER" + case .confettiBall: return "CONFETTI BALL" + case .tanabataTree: return "TANABATA TREE" + case .bamboo: return "PINE DECORATION" + case .dolls: return "JAPANESE DOLLS" + case .flags: return "CARP STREAMER" + case .windChime: return "WIND CHIME" + case .riceScene: return "MOON VIEWING CEREMONY" + case .redEnvelope: return "RED GIFT ENVELOPE" + case .ribbon: return "RIBBON" + case .gift: return "WRAPPED PRESENT" + case .reminderRibbon: return "REMINDER RIBBON" + case .admissionTickets: return "ADMISSION TICKETS" + case .ticket: return "TICKET" + case .medal: return "MILITARY MEDAL" + case .trophy: return "TROPHY" + case .sportsMedal: return "SPORTS MEDAL" + case .firstPlaceMedal: return "FIRST PLACE MEDAL" + case .secondPlaceMedal: return "SECOND PLACE MEDAL" + case .thirdPlaceMedal: return "THIRD PLACE MEDAL" + case .soccer: return "SOCCER BALL" + case .baseball: return "BASEBALL" + case .softball: return "SOFTBALL" + case .basketball: return "BASKETBALL AND HOOP" + case .volleyball: return "VOLLEYBALL" + case .football: return "AMERICAN FOOTBALL" + case .rugbyFootball: return "RUGBY FOOTBALL" + case .tennis: return "TENNIS RACQUET AND BALL" + case .flyingDisc: return "FLYING DISC" + case .bowling: return "BOWLING" + case .cricketBatAndBall: return "CRICKET BAT AND BALL" + case .fieldHockeyStickAndBall: return "FIELD HOCKEY STICK AND BALL" + case .iceHockeyStickAndPuck: return "ICE HOCKEY STICK AND PUCK" + case .lacrosse: return "LACROSSE STICK AND BALL" + case .tableTennisPaddleAndBall: return "TABLE TENNIS PADDLE AND BALL" + case .badmintonRacquetAndShuttlecock: return "BADMINTON RACQUET AND SHUTTLECOCK" + case .boxingGlove: return "BOXING GLOVE" + case .martialArtsUniform: return "MARTIAL ARTS UNIFORM" + case .goalNet: return "GOAL NET" + case .golf: return "FLAG IN HOLE" + case .iceSkate: return "ICE SKATE" + case .fishingPoleAndFish: return "FISHING POLE AND FISH" + case .divingMask: return "DIVING MASK" + case .runningShirtWithSash: return "RUNNING SHIRT WITH SASH" + case .ski: return "SKI AND SKI BOOT" + case .sled: return "SLED" + case .curlingStone: return "CURLING STONE" + case .dart: return "DIRECT HIT" + case .yoYo: return "YO-YO" + case .kite: return "KITE" + case .eightBall: return "BILLIARDS" + case .crystalBall: return "CRYSTAL BALL" + case .magicWand: return "MAGIC WAND" + case .nazarAmulet: return "NAZAR AMULET" + case .hamsa: return "HAMSA" + case .videoGame: return "VIDEO GAME" + case .joystick: return "JOYSTICK" + case .slotMachine: return "SLOT MACHINE" + case .gameDie: return "GAME DIE" + case .jigsaw: return "JIGSAW PUZZLE PIECE" + case .teddyBear: return "TEDDY BEAR" + case .pinata: return "PINATA" + case .mirrorBall: return "MIRROR BALL" + case .nestingDolls: return "NESTING DOLLS" + case .spades: return "BLACK SPADE SUIT" + case .hearts: return "BLACK HEART SUIT" + case .diamonds: return "BLACK DIAMOND SUIT" + case .clubs: return "BLACK CLUB SUIT" + case .chessPawn: return "CHESS PAWN" + case .blackJoker: return "PLAYING CARD BLACK JOKER" + case .mahjong: return "MAHJONG TILE RED DRAGON" + case .flowerPlayingCards: return "FLOWER PLAYING CARDS" + case .performingArts: return "PERFORMING ARTS" + case .frameWithPicture: return "FRAMED PICTURE" + case .art: return "ARTIST PALETTE" + case .thread: return "SPOOL OF THREAD" + case .sewingNeedle: return "SEWING NEEDLE" + case .yarn: return "BALL OF YARN" + case .knot: return "KNOT" + case .eyeglasses: return "EYEGLASSES" + case .darkSunglasses: return "SUNGLASSES" + case .goggles: return "GOGGLES" + case .labCoat: return "LAB COAT" + case .safetyVest: return "SAFETY VEST" + case .necktie: return "NECKTIE" + case .shirt: return "T-SHIRT" + 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" + case .briefs: return "BRIEFS" + case .shorts: return "SHORTS" + case .bikini: return "BIKINI" + case .womansClothes: return "WOMANS CLOTHES" + case .purse: return "PURSE" + case .handbag: return "HANDBAG" + case .pouch: return "POUCH" + case .shoppingBags: return "SHOPPING BAGS" + case .schoolSatchel: return "SCHOOL SATCHEL" + case .thongSandal: return "THONG SANDAL" + case .mansShoe: return "MANS SHOE" + case .athleticShoe: return "ATHLETIC SHOE" + case .hikingBoot: return "HIKING BOOT" + case .womansFlatShoe: return "FLAT SHOE" + case .highHeel: return "HIGH-HEELED SHOE" + case .sandal: return "WOMANS SANDAL" + case .balletShoes: return "BALLET SHOES" + case .boot: return "WOMANS BOOTS" + case .crown: return "CROWN" + case .womansHat: return "WOMANS HAT" + case .tophat: return "TOP HAT" + case .mortarBoard: return "GRADUATION CAP" + case .billedCap: return "BILLED CAP" + case .militaryHelmet: return "MILITARY HELMET" + case .helmetWithWhiteCross: return "RESCUE WORKERโ€™S HELMET" + case .prayerBeads: return "PRAYER BEADS" + case .lipstick: return "LIPSTICK" + case .ring: return "RING" + case .gem: return "GEM STONE" + case .mute: return "SPEAKER WITH CANCELLATION STROKE" + case .speaker: return "SPEAKER" + case .sound: return "SPEAKER WITH ONE SOUND WAVE" + case .loudSound: return "SPEAKER WITH THREE SOUND WAVES" + case .loudspeaker: return "PUBLIC ADDRESS LOUDSPEAKER" + case .mega: return "CHEERING MEGAPHONE" + case .postalHorn: return "POSTAL HORN" + case .bell: return "BELL" + case .noBell: return "BELL WITH CANCELLATION STROKE" + case .musicalScore: return "MUSICAL SCORE" + case .musicalNote: return "MUSICAL NOTE" + case .notes: return "MULTIPLE MUSICAL NOTES" + case .studioMicrophone: return "STUDIO MICROPHONE" + case .levelSlider: return "LEVEL SLIDER" + case .controlKnobs: return "CONTROL KNOBS" + case .microphone: return "MICROPHONE" + case .headphones: return "HEADPHONE" + case .radio: return "RADIO" + case .saxophone: return "SAXOPHONE" + case .accordion: return "ACCORDION" + case .guitar: return "GUITAR" + case .musicalKeyboard: return "MUSICAL KEYBOARD" + case .trumpet: return "TRUMPET" + case .violin: return "VIOLIN" + case .banjo: return "BANJO" + case .drumWithDrumsticks: return "DRUM WITH DRUMSTICKS" + case .longDrum: return "LONG DRUM" + case .iphone: return "MOBILE PHONE" + case .calling: return "MOBILE PHONE WITH RIGHTWARDS ARROW AT LEFT" + case .phone: return "BLACK TELEPHONE" + case .telephoneReceiver: return "TELEPHONE RECEIVER" + case .pager: return "PAGER" + case .fax: return "FAX MACHINE" + case .battery: return "BATTERY" + case .lowBattery: return "LOW BATTERY" + case .electricPlug: return "ELECTRIC PLUG" + case .computer: return "PERSONAL COMPUTER" + case .desktopComputer: return "DESKTOP COMPUTER" + case .printer: return "PRINTER" + case .keyboard: return "KEYBOARD" + case .threeButtonMouse: return "COMPUTER MOUSE" + case .trackball: return "TRACKBALL" + case .minidisc: return "MINIDISC" + case .floppyDisk: return "FLOPPY DISK" + case .cd: return "OPTICAL DISC" + case .dvd: return "DVD" + case .abacus: return "ABACUS" + case .movieCamera: return "MOVIE CAMERA" + case .filmFrames: return "FILM FRAMES" + case .filmProjector: return "FILM PROJECTOR" + case .clapper: return "CLAPPER BOARD" + case .tv: return "TELEVISION" + case .camera: return "CAMERA" + case .cameraWithFlash: return "CAMERA WITH FLASH" + case .videoCamera: return "VIDEO CAMERA" + case .vhs: return "VIDEOCASSETTE" + case .mag: return "LEFT-POINTING MAGNIFYING GLASS" + case .magRight: return "RIGHT-POINTING MAGNIFYING GLASS" + case .candle: return "CANDLE" + case .bulb: return "ELECTRIC LIGHT BULB" + case .flashlight: return "ELECTRIC TORCH" + case .izakayaLantern: return "IZAKAYA LANTERN" + case .diyaLamp: return "DIYA LAMP" + case .notebookWithDecorativeCover: return "NOTEBOOK WITH DECORATIVE COVER" + case .closedBook: return "CLOSED BOOK" + case .book: return "OPEN BOOK" + case .greenBook: return "GREEN BOOK" + case .blueBook: return "BLUE BOOK" + case .orangeBook: return "ORANGE BOOK" + case .books: return "BOOKS" + case .notebook: return "NOTEBOOK" + case .ledger: return "LEDGER" + case .pageWithCurl: return "PAGE WITH CURL" + case .scroll: return "SCROLL" + case .pageFacingUp: return "PAGE FACING UP" + case .newspaper: return "NEWSPAPER" + case .rolledUpNewspaper: return "ROLLED-UP NEWSPAPER" + case .bookmarkTabs: return "BOOKMARK TABS" + case .bookmark: return "BOOKMARK" + case .label: return "LABEL" + case .moneybag: return "MONEY BAG" + case .coin: return "COIN" + case .yen: return "BANKNOTE WITH YEN SIGN" + case .dollar: return "BANKNOTE WITH DOLLAR SIGN" + case .euro: return "BANKNOTE WITH EURO SIGN" + case .pound: return "BANKNOTE WITH POUND SIGN" + case .moneyWithWings: return "MONEY WITH WINGS" + case .creditCard: return "CREDIT CARD" + case .receipt: return "RECEIPT" + case .chart: return "CHART WITH UPWARDS TREND AND YEN SIGN" + case .email: return "ENVELOPE" + case .eMail: return "E-MAIL SYMBOL" + case .incomingEnvelope: return "INCOMING ENVELOPE" + case .envelopeWithArrow: return "ENVELOPE WITH DOWNWARDS ARROW ABOVE" + case .outboxTray: return "OUTBOX TRAY" + case .inboxTray: return "INBOX TRAY" + case .package: return "PACKAGE" + case .mailbox: return "CLOSED MAILBOX WITH RAISED FLAG" + case .mailboxClosed: return "CLOSED MAILBOX WITH LOWERED FLAG" + case .mailboxWithMail: return "OPEN MAILBOX WITH RAISED FLAG" + case .mailboxWithNoMail: return "OPEN MAILBOX WITH LOWERED FLAG" + case .postbox: return "POSTBOX" + case .ballotBoxWithBallot: return "BALLOT BOX WITH BALLOT" + case .pencil2: return "PENCIL" + case .blackNib: return "BLACK NIB" + case .lowerLeftFountainPen: return "FOUNTAIN PEN" + case .lowerLeftBallpointPen: return "PEN" + case .lowerLeftPaintbrush: return "PAINTBRUSH" + case .lowerLeftCrayon: return "CRAYON" + case .memo: return "MEMO" + case .briefcase: return "BRIEFCASE" + case .fileFolder: return "FILE FOLDER" + case .openFileFolder: return "OPEN FILE FOLDER" + case .cardIndexDividers: return "CARD INDEX DIVIDERS" + case .date: return "CALENDAR" + case .calendar: return "TEAR-OFF CALENDAR" + case .spiralNotePad: return "SPIRAL NOTEPAD" + case .spiralCalendarPad: return "SPIRAL CALENDAR" + case .cardIndex: return "CARD INDEX" + case .chartWithUpwardsTrend: return "CHART WITH UPWARDS TREND" + case .chartWithDownwardsTrend: return "CHART WITH DOWNWARDS TREND" + case .barChart: return "BAR CHART" + case .clipboard: return "CLIPBOARD" + case .pushpin: return "PUSHPIN" + case .roundPushpin: return "ROUND PUSHPIN" + case .paperclip: return "PAPERCLIP" + case .linkedPaperclips: return "LINKED PAPERCLIPS" + case .straightRuler: return "STRAIGHT RULER" + case .triangularRuler: return "TRIANGULAR RULER" + case .scissors: return "BLACK SCISSORS" + case .cardFileBox: return "CARD FILE BOX" + case .fileCabinet: return "FILE CABINET" + case .wastebasket: return "WASTEBASKET" + case .lock: return "LOCK" + case .unlock: return "OPEN LOCK" + case .lockWithInkPen: return "LOCK WITH INK PEN" + case .closedLockWithKey: return "CLOSED LOCK WITH KEY" + case .key: return "KEY" + case .oldKey: return "OLD KEY" + case .hammer: return "HAMMER" + case .axe: return "AXE" + case .pick: return "PICK" + case .hammerAndPick: return "HAMMER AND PICK" + case .hammerAndWrench: return "HAMMER AND WRENCH" + case .daggerKnife: return "DAGGER" + case .crossedSwords: return "CROSSED SWORDS" + case .gun: return "PISTOL" + case .boomerang: return "BOOMERANG" + case .bowAndArrow: return "BOW AND ARROW" + case .shield: return "SHIELD" + case .carpentrySaw: return "CARPENTRY SAW" + case .wrench: return "WRENCH" + case .screwdriver: return "SCREWDRIVER" + case .nutAndBolt: return "NUT AND BOLT" + case .gear: return "GEAR" + case .compression: return "CLAMP" + case .scales: return "BALANCE SCALE" + case .probingCane: return "PROBING CANE" + case .link: return "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" + case .petriDish: return "PETRI DISH" + case .dna: return "DNA DOUBLE HELIX" + case .microscope: return "MICROSCOPE" + case .telescope: return "TELESCOPE" + case .satelliteAntenna: return "SATELLITE ANTENNA" + case .syringe: return "SYRINGE" + case .dropOfBlood: return "DROP OF BLOOD" + case .pill: return "PILL" + case .adhesiveBandage: return "ADHESIVE BANDAGE" + case .crutch: return "CRUTCH" + case .stethoscope: return "STETHOSCOPE" + case .xRay: return "X-RAY" + 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" + case .chair: return "CHAIR" + case .toilet: return "TOILET" + case .plunger: return "PLUNGER" + case .shower: return "SHOWER" + case .bathtub: return "BATHTUB" + case .mouseTrap: return "MOUSE TRAP" + case .razor: return "RAZOR" + case .lotionBottle: return "LOTION BOTTLE" + case .safetyPin: return "SAFETY PIN" + case .broom: return "BROOM" + case .basket: return "BASKET" + case .rollOfPaper: return "ROLL OF PAPER" + case .bucket: return "BUCKET" + case .soap: return "BAR OF SOAP" + case .bubbles: return "BUBBLES" + case .toothbrush: return "TOOTHBRUSH" + case .sponge: return "SPONGE" + case .fireExtinguisher: return "FIRE EXTINGUISHER" + case .shoppingTrolley: return "SHOPPING TROLLEY" + case .smoking: return "SMOKING SYMBOL" + case .coffin: return "COFFIN" + case .headstone: return "HEADSTONE" + case .funeralUrn: return "FUNERAL URN" + case .moyai: return "MOYAI" + case .placard: return "PLACARD" + case .identificationCard: return "IDENTIFICATION CARD" + case .atm: return "AUTOMATED TELLER MACHINE" + case .putLitterInItsPlace: return "PUT LITTER IN ITS PLACE SYMBOL" + case .potableWater: return "POTABLE WATER SYMBOL" + case .wheelchair: return "WHEELCHAIR SYMBOL" + case .mens: return "MENS SYMBOL" + case .womens: return "WOMENS SYMBOL" + case .restroom: return "RESTROOM" + case .babySymbol: return "BABY SYMBOL" + case .wc: return "WATER CLOSET" + case .passportControl: return "PASSPORT CONTROL" + case .customs: return "CUSTOMS" + case .baggageClaim: return "BAGGAGE CLAIM" + case .leftLuggage: return "LEFT LUGGAGE" + case .warning: return "WARNING SIGN" + case .childrenCrossing: return "CHILDREN CROSSING" + case .noEntry: return "NO ENTRY" + case .noEntrySign: return "NO ENTRY SIGN" + case .noBicycles: return "NO BICYCLES" + case .noSmoking: return "NO SMOKING SYMBOL" + case .doNotLitter: return "DO NOT LITTER SYMBOL" + case .nonPotableWater: return "NON-POTABLE WATER SYMBOL" + case .noPedestrians: return "NO PEDESTRIANS" + case .noMobilePhones: return "NO MOBILE PHONES" + case .underage: return "NO ONE UNDER EIGHTEEN SYMBOL" + case .radioactiveSign: return "RADIOACTIVE" + case .biohazardSign: return "BIOHAZARD" + case .arrowUp: return "UPWARDS BLACK ARROW" + case .arrowUpperRight: return "NORTH EAST ARROW" + case .arrowRight: return "BLACK RIGHTWARDS ARROW" + case .arrowLowerRight: return "SOUTH EAST ARROW" + case .arrowDown: return "DOWNWARDS BLACK ARROW" + case .arrowLowerLeft: return "SOUTH WEST ARROW" + case .arrowLeft: return "LEFTWARDS BLACK ARROW" + case .arrowUpperLeft: return "NORTH WEST ARROW" + case .arrowUpDown: return "UP DOWN ARROW" + case .leftRightArrow: return "LEFT RIGHT ARROW" + case .leftwardsArrowWithHook: return "LEFTWARDS ARROW WITH HOOK" + case .arrowRightHook: return "RIGHTWARDS ARROW WITH HOOK" + case .arrowHeadingUp: return "ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS" + case .arrowHeadingDown: return "ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS" + case .arrowsClockwise: return "CLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS" + case .arrowsCounterclockwise: return "ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS" + case .back: return "BACK WITH LEFTWARDS ARROW ABOVE" + case .end: return "END WITH LEFTWARDS ARROW ABOVE" + case .on: return "ON WITH EXCLAMATION MARK WITH LEFT RIGHT ARROW ABOVE" + case .soon: return "SOON WITH RIGHTWARDS ARROW ABOVE" + case .top: return "TOP WITH UPWARDS ARROW ABOVE" + case .placeOfWorship: return "PLACE OF WORSHIP" + case .atomSymbol: return "ATOM SYMBOL" + case .omSymbol: return "OM" + case .starOfDavid: return "STAR OF DAVID" + case .wheelOfDharma: return "WHEEL OF DHARMA" + case .yinYang: return "YIN YANG" + case .latinCross: return "LATIN CROSS" + case .orthodoxCross: return "ORTHODOX CROSS" + case .starAndCrescent: return "STAR AND CRESCENT" + case .peaceSymbol: return "PEACE SYMBOL" + case .menorahWithNineBranches: return "MENORAH WITH NINE BRANCHES" + case .sixPointedStar: return "SIX POINTED STAR WITH MIDDLE DOT" + 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" + case .`repeat`: return "CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS" + case .repeatOne: return "CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY" + case .arrowForward: return "BLACK RIGHT-POINTING TRIANGLE" + case .fastForward: return "BLACK RIGHT-POINTING DOUBLE TRIANGLE" + case .blackRightPointingDoubleTriangleWithVerticalBar: return "NEXT TRACK BUTTON" + case .blackRightPointingTriangleWithDoubleVerticalBar: return "PLAY OR PAUSE BUTTON" + case .arrowBackward: return "BLACK LEFT-POINTING TRIANGLE" + case .rewind: return "BLACK LEFT-POINTING DOUBLE TRIANGLE" + case .blackLeftPointingDoubleTriangleWithVerticalBar: return "LAST TRACK BUTTON" + case .arrowUpSmall: return "UP-POINTING SMALL RED TRIANGLE" + case .arrowDoubleUp: return "BLACK UP-POINTING DOUBLE TRIANGLE" + case .arrowDownSmall: return "DOWN-POINTING SMALL RED TRIANGLE" + case .arrowDoubleDown: return "BLACK DOWN-POINTING DOUBLE TRIANGLE" + case .doubleVerticalBar: return "PAUSE BUTTON" + case .blackSquareForStop: return "STOP BUTTON" + case .blackCircleForRecord: return "RECORD BUTTON" + case .eject: return "EJECT BUTTON" + case .cinema: return "CINEMA" + case .lowBrightness: return "LOW BRIGHTNESS SYMBOL" + case .highBrightness: return "HIGH BRIGHTNESS SYMBOL" + case .signalStrength: return "ANTENNA WITH BARS" + case .vibrationMode: return "VIBRATION MODE" + case .mobilePhoneOff: return "MOBILE PHONE OFF" + case .femaleSign: return "FEMALE SIGN" + case .maleSign: return "MALE SIGN" + case .transgenderSymbol: return "TRANSGENDER SYMBOL" + case .heavyMultiplicationX: return "HEAVY MULTIPLICATION X" + case .heavyPlusSign: return "HEAVY PLUS SIGN" + case .heavyMinusSign: return "HEAVY MINUS SIGN" + case .heavyDivisionSign: return "HEAVY DIVISION SIGN" + case .heavyEqualsSign: return "HEAVY EQUALS SIGN" + case .infinity: return "INFINITY" + case .bangbang: return "DOUBLE EXCLAMATION MARK" + case .interrobang: return "EXCLAMATION QUESTION MARK" + case .question: return "BLACK QUESTION MARK ORNAMENT" + case .greyQuestion: return "WHITE QUESTION MARK ORNAMENT" + case .greyExclamation: return "WHITE EXCLAMATION MARK ORNAMENT" + case .exclamation: return "HEAVY EXCLAMATION MARK SYMBOL" + case .wavyDash: return "WAVY DASH" + case .currencyExchange: return "CURRENCY EXCHANGE" + case .heavyDollarSign: return "HEAVY DOLLAR SIGN" + case .medicalSymbol: return "MEDICAL SYMBOL" + case .recycle: return "BLACK UNIVERSAL RECYCLING SYMBOL" + case .fleurDeLis: return "FLEUR-DE-LIS" + case .trident: return "TRIDENT EMBLEM" + case .nameBadge: return "NAME BADGE" + case .beginner: return "JAPANESE SYMBOL FOR BEGINNER" + case .o: return "HEAVY LARGE CIRCLE" + case .whiteCheckMark: return "WHITE HEAVY CHECK MARK" + case .ballotBoxWithCheck: return "BALLOT BOX WITH CHECK" + case .heavyCheckMark: return "HEAVY CHECK MARK" + case .x: return "CROSS MARK" + case .negativeSquaredCrossMark: return "NEGATIVE SQUARED CROSS MARK" + case .curlyLoop: return "CURLY LOOP" + case .loop: return "DOUBLE CURLY LOOP" + case .partAlternationMark: return "PART ALTERNATION MARK" + case .eightSpokedAsterisk: return "EIGHT SPOKED ASTERISK" + case .eightPointedBlackStar: return "EIGHT POINTED BLACK STAR" + case .sparkle: return "SPARKLE" + case .copyright: return "COPYRIGHT SIGN" + case .registered: return "REGISTERED SIGN" + case .tm: return "TRADE MARK SIGN" + case .hash: return "HASH KEY" + case .keycapStar: return "KEYCAP: *" + case .zero: return "KEYCAP 0" + case .one: return "KEYCAP 1" + case .two: return "KEYCAP 2" + case .three: return "KEYCAP 3" + case .four: return "KEYCAP 4" + case .five: return "KEYCAP 5" + case .six: return "KEYCAP 6" + case .seven: return "KEYCAP 7" + case .eight: return "KEYCAP 8" + case .nine: return "KEYCAP 9" + case .keycapTen: return "KEYCAP TEN" + case .capitalAbcd: return "INPUT SYMBOL FOR LATIN CAPITAL LETTERS" + case .abcd: return "INPUT SYMBOL FOR LATIN SMALL LETTERS" + case .oneTwoThreeFour: return "INPUT SYMBOL FOR NUMBERS" + case .symbols: return "INPUT SYMBOL FOR SYMBOLS" + case .abc: return "INPUT SYMBOL FOR LATIN LETTERS" + case .a: return "NEGATIVE SQUARED LATIN CAPITAL LETTER A" + case .ab: return "NEGATIVE SQUARED AB" + case .b: return "NEGATIVE SQUARED LATIN CAPITAL LETTER B" + case .cl: return "SQUARED CL" + case .cool: return "SQUARED COOL" + case .free: return "SQUARED FREE" + case .informationSource: return "INFORMATION SOURCE" + case .id: return "SQUARED ID" + case .m: return "CIRCLED LATIN CAPITAL LETTER M" + case .new: return "SQUARED NEW" + case .ng: return "SQUARED NG" + case .o2: return "NEGATIVE SQUARED LATIN CAPITAL LETTER O" + case .ok: return "SQUARED OK" + case .parking: return "NEGATIVE SQUARED LATIN CAPITAL LETTER P" + case .sos: return "SQUARED SOS" + case .up: return "SQUARED UP WITH EXCLAMATION MARK" + case .vs: return "SQUARED VS" + case .koko: return "SQUARED KATAKANA KOKO" + case .sa: return "SQUARED KATAKANA SA" + case .u6708: return "SQUARED CJK UNIFIED IDEOGRAPH-6708" + case .u6709: return "SQUARED CJK UNIFIED IDEOGRAPH-6709" + case .u6307: return "SQUARED CJK UNIFIED IDEOGRAPH-6307" + case .ideographAdvantage: return "CIRCLED IDEOGRAPH ADVANTAGE" + case .u5272: return "SQUARED CJK UNIFIED IDEOGRAPH-5272" + case .u7121: return "SQUARED CJK UNIFIED IDEOGRAPH-7121" + case .u7981: return "SQUARED CJK UNIFIED IDEOGRAPH-7981" + case .accept: return "CIRCLED IDEOGRAPH ACCEPT" + case .u7533: return "SQUARED CJK UNIFIED IDEOGRAPH-7533" + case .u5408: return "SQUARED CJK UNIFIED IDEOGRAPH-5408" + case .u7a7a: return "SQUARED CJK UNIFIED IDEOGRAPH-7A7A" + case .congratulations: return "CIRCLED IDEOGRAPH CONGRATULATION" + case .secret: return "CIRCLED IDEOGRAPH SECRET" + case .u55b6: return "SQUARED CJK UNIFIED IDEOGRAPH-55B6" + case .u6e80: return "SQUARED CJK UNIFIED IDEOGRAPH-6E80" + case .redCircle: return "LARGE RED CIRCLE" + case .largeOrangeCircle: return "LARGE ORANGE CIRCLE" + case .largeYellowCircle: return "LARGE YELLOW CIRCLE" + case .largeGreenCircle: return "LARGE GREEN CIRCLE" + case .largeBlueCircle: return "LARGE BLUE CIRCLE" + case .largePurpleCircle: return "LARGE PURPLE CIRCLE" + case .largeBrownCircle: return "LARGE BROWN CIRCLE" + case .blackCircle: return "MEDIUM BLACK CIRCLE" + case .whiteCircle: return "MEDIUM WHITE CIRCLE" + case .largeRedSquare: return "LARGE RED SQUARE" + case .largeOrangeSquare: return "LARGE ORANGE SQUARE" + case .largeYellowSquare: return "LARGE YELLOW SQUARE" + case .largeGreenSquare: return "LARGE GREEN SQUARE" + case .largeBlueSquare: return "LARGE BLUE SQUARE" + case .largePurpleSquare: return "LARGE PURPLE SQUARE" + case .largeBrownSquare: return "LARGE BROWN SQUARE" + case .blackLargeSquare: return "BLACK LARGE SQUARE" + case .whiteLargeSquare: return "WHITE LARGE SQUARE" + case .blackMediumSquare: return "BLACK MEDIUM SQUARE" + case .whiteMediumSquare: return "WHITE MEDIUM SQUARE" + case .blackMediumSmallSquare: return "BLACK MEDIUM SMALL SQUARE" + case .whiteMediumSmallSquare: return "WHITE MEDIUM SMALL SQUARE" + case .blackSmallSquare: return "BLACK SMALL SQUARE" + case .whiteSmallSquare: return "WHITE SMALL SQUARE" + case .largeOrangeDiamond: return "LARGE ORANGE DIAMOND" + case .largeBlueDiamond: return "LARGE BLUE DIAMOND" + case .smallOrangeDiamond: return "SMALL ORANGE DIAMOND" + case .smallBlueDiamond: return "SMALL BLUE DIAMOND" + case .smallRedTriangle: return "UP-POINTING RED TRIANGLE" + case .smallRedTriangleDown: return "DOWN-POINTING RED TRIANGLE" + case .diamondShapeWithADotInside: return "DIAMOND SHAPE WITH A DOT INSIDE" + case .radioButton: return "RADIO BUTTON" + case .whiteSquareButton: return "WHITE SQUARE BUTTON" + case .blackSquareButton: return "BLACK SQUARE BUTTON" + case .checkeredFlag: return "CHEQUERED FLAG" + case .triangularFlagOnPost: return "TRIANGULAR FLAG ON POST" + case .crossedFlags: return "CROSSED FLAGS" + case .wavingBlackFlag: return "WAVING BLACK FLAG" + case .wavingWhiteFlag: return "WHITE FLAG" + case .rainbowFlag: return "RAINBOW FLAG" + case .transgenderFlag: return "TRANSGENDER FLAG" + case .pirateFlag: return "PIRATE FLAG" + case .flagAc: return "Ascension Island Flag" + case .flagAd: return "Andorra Flag" + case .flagAe: return "United Arab Emirates Flag" + case .flagAf: return "Afghanistan Flag" + case .flagAg: return "Antigua & Barbuda Flag" + case .flagAi: return "Anguilla Flag" + case .flagAl: return "Albania Flag" + case .flagAm: return "Armenia Flag" + case .flagAo: return "Angola Flag" + case .flagAq: return "Antarctica Flag" + case .flagAr: return "Argentina Flag" + case .flagAs: return "American Samoa Flag" + case .flagAt: return "Austria Flag" + case .flagAu: return "Australia Flag" + case .flagAw: return "Aruba Flag" + case .flagAx: return "ร…land Islands Flag" + case .flagAz: return "Azerbaijan Flag" + case .flagBa: return "Bosnia & Herzegovina Flag" + case .flagBb: return "Barbados Flag" + case .flagBd: return "Bangladesh Flag" + case .flagBe: return "Belgium Flag" + case .flagBf: return "Burkina Faso Flag" + case .flagBg: return "Bulgaria Flag" + case .flagBh: return "Bahrain Flag" + case .flagBi: return "Burundi Flag" + case .flagBj: return "Benin Flag" + case .flagBl: return "St. Barthรฉlemy Flag" + case .flagBm: return "Bermuda Flag" + case .flagBn: return "Brunei Flag" + case .flagBo: return "Bolivia Flag" + case .flagBq: return "Caribbean Netherlands Flag" + case .flagBr: return "Brazil Flag" + case .flagBs: return "Bahamas Flag" + case .flagBt: return "Bhutan Flag" + case .flagBv: return "Bouvet Island Flag" + case .flagBw: return "Botswana Flag" + case .flagBy: return "Belarus Flag" + case .flagBz: return "Belize Flag" + case .flagCa: return "Canada Flag" + case .flagCc: return "Cocos (Keeling) Islands Flag" + case .flagCd: return "Congo - Kinshasa Flag" + case .flagCf: return "Central African Republic Flag" + case .flagCg: return "Congo - Brazzaville Flag" + case .flagCh: return "Switzerland Flag" + case .flagCi: return "Cรดte dโ€™Ivoire Flag" + case .flagCk: return "Cook Islands Flag" + case .flagCl: return "Chile Flag" + case .flagCm: return "Cameroon Flag" + case .cn: return "China Flag" + case .flagCo: return "Colombia Flag" + case .flagCp: return "Clipperton Island Flag" + case .flagCr: return "Costa Rica Flag" + case .flagCu: return "Cuba Flag" + case .flagCv: return "Cape Verde Flag" + case .flagCw: return "Curaรงao Flag" + case .flagCx: return "Christmas Island Flag" + case .flagCy: return "Cyprus Flag" + case .flagCz: return "Czechia Flag" + case .de: return "Germany Flag" + case .flagDg: return "Diego Garcia Flag" + case .flagDj: return "Djibouti Flag" + case .flagDk: return "Denmark Flag" + case .flagDm: return "Dominica Flag" + case .flagDo: return "Dominican Republic Flag" + case .flagDz: return "Algeria Flag" + case .flagEa: return "Ceuta & Melilla Flag" + case .flagEc: return "Ecuador Flag" + case .flagEe: return "Estonia Flag" + case .flagEg: return "Egypt Flag" + case .flagEh: return "Western Sahara Flag" + case .flagEr: return "Eritrea Flag" + case .es: return "Spain Flag" + case .flagEt: return "Ethiopia Flag" + case .flagEu: return "European Union Flag" + case .flagFi: return "Finland Flag" + case .flagFj: return "Fiji Flag" + case .flagFk: return "Falkland Islands Flag" + case .flagFm: return "Micronesia Flag" + case .flagFo: return "Faroe Islands Flag" + case .fr: return "France Flag" + case .flagGa: return "Gabon Flag" + case .gb: return "United Kingdom Flag" + case .flagGd: return "Grenada Flag" + case .flagGe: return "Georgia Flag" + case .flagGf: return "French Guiana Flag" + case .flagGg: return "Guernsey Flag" + case .flagGh: return "Ghana Flag" + case .flagGi: return "Gibraltar Flag" + case .flagGl: return "Greenland Flag" + case .flagGm: return "Gambia Flag" + case .flagGn: return "Guinea Flag" + case .flagGp: return "Guadeloupe Flag" + case .flagGq: return "Equatorial Guinea Flag" + case .flagGr: return "Greece Flag" + case .flagGs: return "South Georgia & South Sandwich Islands Flag" + case .flagGt: return "Guatemala Flag" + case .flagGu: return "Guam Flag" + case .flagGw: return "Guinea-Bissau Flag" + case .flagGy: return "Guyana Flag" + case .flagHk: return "Hong Kong SAR China Flag" + case .flagHm: return "Heard & McDonald Islands Flag" + case .flagHn: return "Honduras Flag" + case .flagHr: return "Croatia Flag" + case .flagHt: return "Haiti Flag" + case .flagHu: return "Hungary Flag" + case .flagIc: return "Canary Islands Flag" + case .flagId: return "Indonesia Flag" + case .flagIe: return "Ireland Flag" + case .flagIl: return "Israel Flag" + case .flagIm: return "Isle of Man Flag" + case .flagIn: return "India Flag" + case .flagIo: return "British Indian Ocean Territory Flag" + case .flagIq: return "Iraq Flag" + case .flagIr: return "Iran Flag" + case .flagIs: return "Iceland Flag" + case .it: return "Italy Flag" + case .flagJe: return "Jersey Flag" + case .flagJm: return "Jamaica Flag" + case .flagJo: return "Jordan Flag" + case .jp: return "Japan Flag" + case .flagKe: return "Kenya Flag" + case .flagKg: return "Kyrgyzstan Flag" + case .flagKh: return "Cambodia Flag" + case .flagKi: return "Kiribati Flag" + case .flagKm: return "Comoros Flag" + case .flagKn: return "St. Kitts & Nevis Flag" + case .flagKp: return "North Korea Flag" + case .kr: return "South Korea Flag" + case .flagKw: return "Kuwait Flag" + case .flagKy: return "Cayman Islands Flag" + case .flagKz: return "Kazakhstan Flag" + case .flagLa: return "Laos Flag" + case .flagLb: return "Lebanon Flag" + case .flagLc: return "St. Lucia Flag" + case .flagLi: return "Liechtenstein Flag" + case .flagLk: return "Sri Lanka Flag" + case .flagLr: return "Liberia Flag" + case .flagLs: return "Lesotho Flag" + case .flagLt: return "Lithuania Flag" + case .flagLu: return "Luxembourg Flag" + case .flagLv: return "Latvia Flag" + case .flagLy: return "Libya Flag" + case .flagMa: return "Morocco Flag" + case .flagMc: return "Monaco Flag" + case .flagMd: return "Moldova Flag" + case .flagMe: return "Montenegro Flag" + case .flagMf: return "St. Martin Flag" + case .flagMg: return "Madagascar Flag" + case .flagMh: return "Marshall Islands Flag" + case .flagMk: return "North Macedonia Flag" + case .flagMl: return "Mali Flag" + case .flagMm: return "Myanmar (Burma) Flag" + case .flagMn: return "Mongolia Flag" + case .flagMo: return "Macao SAR China Flag" + case .flagMp: return "Northern Mariana Islands Flag" + case .flagMq: return "Martinique Flag" + case .flagMr: return "Mauritania Flag" + case .flagMs: return "Montserrat Flag" + case .flagMt: return "Malta Flag" + case .flagMu: return "Mauritius Flag" + case .flagMv: return "Maldives Flag" + case .flagMw: return "Malawi Flag" + case .flagMx: return "Mexico Flag" + case .flagMy: return "Malaysia Flag" + case .flagMz: return "Mozambique Flag" + case .flagNa: return "Namibia Flag" + case .flagNc: return "New Caledonia Flag" + case .flagNe: return "Niger Flag" + case .flagNf: return "Norfolk Island Flag" + case .flagNg: return "Nigeria Flag" + case .flagNi: return "Nicaragua Flag" + case .flagNl: return "Netherlands Flag" + case .flagNo: return "Norway Flag" + case .flagNp: return "Nepal Flag" + case .flagNr: return "Nauru Flag" + case .flagNu: return "Niue Flag" + case .flagNz: return "New Zealand Flag" + case .flagOm: return "Oman Flag" + case .flagPa: return "Panama Flag" + case .flagPe: return "Peru Flag" + case .flagPf: return "French Polynesia Flag" + case .flagPg: return "Papua New Guinea Flag" + case .flagPh: return "Philippines Flag" + case .flagPk: return "Pakistan Flag" + case .flagPl: return "Poland Flag" + case .flagPm: return "St. Pierre & Miquelon Flag" + case .flagPn: return "Pitcairn Islands Flag" + case .flagPr: return "Puerto Rico Flag" + case .flagPs: return "Palestinian Territories Flag" + case .flagPt: return "Portugal Flag" + case .flagPw: return "Palau Flag" + case .flagPy: return "Paraguay Flag" + case .flagQa: return "Qatar Flag" + case .flagRe: return "Rรฉunion Flag" + case .flagRo: return "Romania Flag" + case .flagRs: return "Serbia Flag" + case .ru: return "Russia Flag" + case .flagRw: return "Rwanda Flag" + case .flagSa: return "Saudi Arabia Flag" + case .flagSb: return "Solomon Islands Flag" + case .flagSc: return "Seychelles Flag" + case .flagSd: return "Sudan Flag" + case .flagSe: return "Sweden Flag" + case .flagSg: return "Singapore Flag" + case .flagSh: return "St. Helena Flag" + case .flagSi: return "Slovenia Flag" + case .flagSj: return "Svalbard & Jan Mayen Flag" + case .flagSk: return "Slovakia Flag" + case .flagSl: return "Sierra Leone Flag" + case .flagSm: return "San Marino Flag" + case .flagSn: return "Senegal Flag" + case .flagSo: return "Somalia Flag" + case .flagSr: return "Suriname Flag" + case .flagSs: return "South Sudan Flag" + case .flagSt: return "Sรฃo Tomรฉ & Prรญncipe Flag" + case .flagSv: return "El Salvador Flag" + case .flagSx: return "Sint Maarten Flag" + case .flagSy: return "Syria Flag" + case .flagSz: return "Eswatini Flag" + case .flagTa: return "Tristan da Cunha Flag" + case .flagTc: return "Turks & Caicos Islands Flag" + case .flagTd: return "Chad Flag" + case .flagTf: return "French Southern Territories Flag" + case .flagTg: return "Togo Flag" + case .flagTh: return "Thailand Flag" + case .flagTj: return "Tajikistan Flag" + case .flagTk: return "Tokelau Flag" + case .flagTl: return "Timor-Leste Flag" + case .flagTm: return "Turkmenistan Flag" + case .flagTn: return "Tunisia Flag" + case .flagTo: return "Tonga Flag" + case .flagTr: return "Turkey Flag" + case .flagTt: return "Trinidad & Tobago Flag" + case .flagTv: return "Tuvalu Flag" + case .flagTw: return "Taiwan Flag" + case .flagTz: return "Tanzania Flag" + case .flagUa: return "Ukraine Flag" + case .flagUg: return "Uganda Flag" + case .flagUm: return "U.S. Outlying Islands Flag" + case .flagUn: return "United Nations Flag" + case .us: return "United States Flag" + case .flagUy: return "Uruguay Flag" + case .flagUz: return "Uzbekistan Flag" + case .flagVa: return "Vatican City Flag" + case .flagVc: return "St. Vincent & Grenadines Flag" + case .flagVe: return "Venezuela Flag" + case .flagVg: return "British Virgin Islands Flag" + case .flagVi: return "U.S. Virgin Islands Flag" + case .flagVn: return "Vietnam Flag" + case .flagVu: return "Vanuatu Flag" + case .flagWf: return "Wallis & Futuna Flag" + case .flagWs: return "Samoa Flag" + case .flagXk: return "Kosovo Flag" + case .flagYe: return "Yemen Flag" + case .flagYt: return "Mayotte Flag" + case .flagZa: return "South Africa Flag" + case .flagZm: return "Zambia Flag" + case .flagZw: return "Zimbabwe Flag" + case .flagEngland: return "England Flag" + case .flagScotland: return "Scotland Flag" + case .flagWales: return "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..9eeac6495 --- /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 { + return nil + } + } +} diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift new file mode 100644 index 000000000..3e7cd02a9 --- /dev/null +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -0,0 +1,73 @@ +// +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// + +public struct EmojiWithSkinTones: Hashable { + let baseEmoji: Emoji + let skinTones: [Emoji.SkinTone]? + + 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) + } + } + + var rawValue: String { + if let skinTones = skinTones { + return baseEmoji.emojiPerSkinTonePermutation?[skinTones] ?? baseEmoji.rawValue + } else { + return baseEmoji.rawValue + } + } +} + +extension Emoji { + private static let keyValueStore = SDSKeyValueStore(collection: "Emoji+PreferredSkinTonePermutation") + + static func allSendableEmojiByCategoryWithPreferredSkinTones(transaction: YapDatabaseReadTransaction) -> [Category: [EmojiWithSkinTones]] { + return Category.allCases.reduce(into: [Category: [EmojiWithSkinTones]]()) { result, category in + result[category] = category.normalizedEmoji.filter { $0.available }.map { $0.withPreferredSkinTones(transaction: transaction) } + } + } + + func withPreferredSkinTones(transaction: YapDatabaseReadTransaction) -> EmojiWithSkinTones { + guard let rawSkinTones = Self.keyValueStore.getObject(forKey: rawValue, transaction: transaction) as? [String] else { + return EmojiWithSkinTones(baseEmoji: self, skinTones: nil) + } + + return EmojiWithSkinTones(baseEmoji: self, skinTones: rawSkinTones.compactMap { SkinTone(rawValue: $0) }) + } + + func setPreferredSkinTones(_ preferredSkinTonePermutation: [SkinTone]?, transaction: YapDatabaseReadWriteTransaction) { + if let preferredSkinTonePermutation = preferredSkinTonePermutation { + Self.keyValueStore.setObject(preferredSkinTonePermutation.map { $0.rawValue }, key: rawValue, transaction: transaction) + } else { + Self.keyValueStore.removeValue(forKey: rawValue, transaction: transaction) + } + } + + init?(_ string: String) { + guard let emojiWithSkinTonePermutation = EmojiWithSkinTones(rawValue: string) else { return nil } + self = emojiWithSkinTonePermutation.baseEmoji + } +} + +// 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/SignalUtilitiesKit/Utilities/DisplayableText.swift b/SignalUtilitiesKit/Utilities/DisplayableText.swift index d63caad7a..4afe3e5f0 100644 --- a/SignalUtilitiesKit/Utilities/DisplayableText.swift +++ b/SignalUtilitiesKit/Utilities/DisplayableText.swift @@ -131,7 +131,7 @@ extension String { return CTLineGetGlyphCount(line) } - var isSingleEmoji: Bool { + public var isSingleEmoji: Bool { return glyphCount == 1 && containsEmoji } From 5f4758d36a692d2d1ff946ea3f3251a55698d444 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Tue, 14 Jun 2022 09:47:11 +1000 Subject: [PATCH 037/133] fix: use yapdatabase transaction for skin tones --- Session/Emoji/EmojiWithSkinTones.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift index 3e7cd02a9..26b1a14be 100644 --- a/Session/Emoji/EmojiWithSkinTones.swift +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -29,7 +29,7 @@ public struct EmojiWithSkinTones: Hashable { } extension Emoji { - private static let keyValueStore = SDSKeyValueStore(collection: "Emoji+PreferredSkinTonePermutation") + private static let emojiWithPreferredSkinToneCollection = "Emoji+PreferredSkinTonePermutation" static func allSendableEmojiByCategoryWithPreferredSkinTones(transaction: YapDatabaseReadTransaction) -> [Category: [EmojiWithSkinTones]] { return Category.allCases.reduce(into: [Category: [EmojiWithSkinTones]]()) { result, category in @@ -38,7 +38,7 @@ extension Emoji { } func withPreferredSkinTones(transaction: YapDatabaseReadTransaction) -> EmojiWithSkinTones { - guard let rawSkinTones = Self.keyValueStore.getObject(forKey: rawValue, transaction: transaction) as? [String] else { + guard let rawSkinTones = transaction.object(forKey: rawValue, inCollection: Self.emojiWithPreferredSkinToneCollection) as? [String] else { return EmojiWithSkinTones(baseEmoji: self, skinTones: nil) } @@ -47,9 +47,9 @@ extension Emoji { func setPreferredSkinTones(_ preferredSkinTonePermutation: [SkinTone]?, transaction: YapDatabaseReadWriteTransaction) { if let preferredSkinTonePermutation = preferredSkinTonePermutation { - Self.keyValueStore.setObject(preferredSkinTonePermutation.map { $0.rawValue }, key: rawValue, transaction: transaction) + transaction.setObject(preferredSkinTonePermutation.map { $0.rawValue }, forKey: rawValue, inCollection: Self.emojiWithPreferredSkinToneCollection) } else { - Self.keyValueStore.removeValue(forKey: rawValue, transaction: transaction) + transaction.removeObject(forKey: rawValue, inCollection: Self.emojiWithPreferredSkinToneCollection) } } From 913939616e594e9194a4297df34d1a9423f530a8 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Tue, 14 Jun 2022 15:11:17 +1000 Subject: [PATCH 038/133] feat: emoji picker view --- Session.xcodeproj/project.pbxproj | 12 + .../ContextMenuVC+EmojiReactsView.swift | 2 +- .../ConversationVC+Interaction.swift | 27 +- .../EmojiPickerCollectionView.swift | 395 ++++++++++++++++++ .../Emoji Picker/EmojiPickerSheet.swift | 128 ++++++ .../Emoji Picker/EmojiSkinTonePicker.swift | 305 ++++++++++++++ .../Translations/en.lproj/Localizable.strings | 18 + 7 files changed, 878 insertions(+), 9 deletions(-) create mode 100644 Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift create mode 100644 Session/Conversations/Emoji Picker/EmojiPickerSheet.swift create mode 100644 Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 68c1ca8a5..c7953fdac 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -134,6 +134,9 @@ 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 */; }; @@ -1138,6 +1141,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 = ""; }; @@ -2100,6 +2106,9 @@ 7B1B52BD2851ADE1006069F2 /* Emoji Picker */ = { isa = PBXGroup; children = ( + 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */, + 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */, + 7B1B52D728580C6D006069F2 /* EmojiPickerSheet.swift */, ); path = "Emoji Picker"; sourceTree = ""; @@ -4970,7 +4979,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 */, @@ -5095,6 +5106,7 @@ B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */, 7B93D06D27CF175800811CB6 /* MessageRequestsCell.swift in Sources */, 7B93D07727CF1A8A00811CB6 /* MockDataGenerator.swift in Sources */, + 7B1B52D828580C6D006069F2 /* EmojiPickerSheet.swift in Sources */, 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */, B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */, 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift index 8b90c0812..031c216e2 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -89,7 +89,7 @@ extension ContextMenuVC { // MARK: Interaction @objc private func handleTap() { dismiss() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { [weak self] in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: { [weak self] in self?.work() }) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 8b74dd596..b16e8871b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -51,8 +51,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let call = SessionCall(for: contactSessionID, uuid: UUID().uuidString.lowercased(), mode: .offer, outgoing: true) let callVC = CallVC(for: call) callVC.conversationVC = self - self.inputAccessoryView?.isHidden = true - self.inputAccessoryView?.alpha = 0 + hideInputAccessoryView() present(callVC, animated: true, completion: nil) } else { let callPermissionRequestModal = CallPermissionRequestModal() @@ -497,6 +496,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc self.oldText = newText } + func hideInputAccessoryView() { + self.inputAccessoryView?.isHidden = true + self.inputAccessoryView?.alpha = 0 + } + func showInputAccessoryView() { UIView.animate(withDuration: 0.25, animations: { self.inputAccessoryView?.isHidden = false @@ -742,8 +746,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } alertVC.addAction(cancelAction) - self.inputAccessoryView?.isHidden = true - self.inputAccessoryView?.alpha = 0 + hideInputAccessoryView() self.presentAlert(alertVC) } else { deleteLocally(viewItem) @@ -878,10 +881,18 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) { - // TODO: to be implemented - - print("Ryan Test: showFullEmojiKeyboard") - + hideInputAccessoryView() + let emojiPicker = EmojiPickerSheet( + completionHandler: { emoji in + if let emoji = emoji { + self.react(viewItem, with: emoji.rawValue) + } + }, + dismissHandler: { + self.showInputAccessoryView() + }) + emojiPicker.modalPresentationStyle = .overFullScreen + present(emojiPicker, animated: true, completion: nil) } func contextMenuDismissed() { diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift new file mode 100644 index 000000000..68bd7061b --- /dev/null +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -0,0 +1,395 @@ + +protocol EmojiPickerCollectionViewDelegate: AnyObject { + func emojiPicker(_ emojiPicker: EmojiPickerCollectionView, didSelectEmoji emoji: EmojiWithSkinTones) + func emojiPickerWillBeginDragging(_ emojiPicker: EmojiPickerCollectionView) +} + +class EmojiPickerCollectionView: UICollectionView { + let layout: UICollectionViewFlowLayout + + private static let emojiPickerCollection = "EmojiPickerCollection" + private static let recentEmojiKey = "recentEmoji" + + 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)) + + 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 = isDarkMode ? .ows_gray90 : .ows_white + + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + panGestureRecognizer.require(toFail: longPressGesture) + addGestureRecognizer(longPressGesture) + + addGestureRecognizer(tapGestureRecognizer) + tapGestureRecognizer.delegate = self + + Storage.read { transaction in + let rawRecentEmoji = transaction.object( + forKey: EmojiPickerCollectionView.recentEmojiKey, + inCollection: EmojiPickerCollectionView.emojiPickerCollection + ) as? [String] ?? [] + + self.recentEmoji = rawRecentEmoji.compactMap { EmojiWithSkinTones(rawValue: $0) } + + // 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. + for (idx, emoji) in self.recentEmoji.enumerated().reversed() { + if !emoji.isNormalized { + if self.recentEmoji.contains(emoji.normalized) { + self.recentEmoji.remove(at: idx) + } else { + self.recentEmoji[idx] = emoji.normalized + } + } + } + + self.allSendableEmojiByCategory = Emoji.allSendableEmojiByCategoryWithPreferredSkinTones(transaction: transaction) + } + } + + 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 + } + + func recordRecentEmoji(_ emoji: EmojiWithSkinTones, transaction: YapDatabaseReadWriteTransaction) { + guard recentEmoji.first != emoji else { return } + guard emoji.isNormalized else { + recordRecentEmoji(emoji.normalized, transaction: transaction) + return + } + + var newRecentEmoji = recentEmoji + + // Remove any existing entries for this emoji + newRecentEmoji.removeAll { emoji == $0 } + // Insert the selected emoji at the start of the list + newRecentEmoji.insert(emoji, at: 0) + // Truncate the recent emoji list to a maximum of 50 stored + newRecentEmoji = Array(newRecentEmoji[0.. 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)") + } + + Storage.write { transaction in + self.recordRecentEmoji(emoji, transaction: transaction) + } + + 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 + } +} + +fileprivate extension EmojiWithSkinTones { + + var normalized: EmojiWithSkinTones { + switch (baseEmoji, skinTones) { + case (let base, nil) where base.normalized != base: + return EmojiWithSkinTones(baseEmoji: base.normalized) + default: + return self + } + } + + var isNormalized: Bool { self == normalized } + +} diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift new file mode 100644 index 000000000..64a692d48 --- /dev/null +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -0,0 +1,128 @@ +// 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 = isDarkMode ? .ows_gray90 : .ows_white + 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 + } +} + diff --git a/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift b/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift new file mode 100644 index 000000000..a6efa3361 --- /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 emoji.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 = isDarkMode ? .ows_gray75 : .ows_white + referenceOverlay.layer.cornerRadius = 9 + addSubview(referenceOverlay) + + containerView.layoutMargins = UIEdgeInsets(top: 9, leading: 16, bottom: 9, trailing: 16) + containerView.backgroundColor = isDarkMode ? .ows_gray75 : .ows_white + 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/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 62c842bb6..cae2d7c49 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -651,3 +651,21 @@ "modal_call_permission_request_explanation" = "You can enable the 'Voice and video calls' permission in the Privacy Settings."; "DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred"; "DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later"; +/* 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"; From 02b0d607f7138981878a631b008ee5b0f7500caf Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Tue, 14 Jun 2022 17:10:03 +1000 Subject: [PATCH 039/133] WIP: notification --- Session/Notifications/AppNotifications.swift | 36 ++++++++++++++ .../MessageReceiver+Handling.swift | 15 +++--- .../Notifications/NotificationsProtocol.h | 9 +++- .../NSENotificationPresenter.swift | 47 +++++++++---------- .../Utilities/NoopNotificationsManager.swift | 4 ++ 5 files changed, 76 insertions(+), 35 deletions(-) diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index a39574cce..1b33d1b0b 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -302,6 +302,42 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { ) } } + + public func notifyUser(forReaction reactMessage: ReactMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) { + guard !thread.isMuted else { return } + guard !thread.isGroupThread() else { return } // We do NOT notify emoji reacts in groups + guard let sender = reactMessage.sender, let emoji = reactMessage.emoji else { return } + guard let threadId = thread.uniqueId else { return } + + let context = Contact.context(for: thread) + let senderName = Storage.shared.getContact(with: sender, using: transaction)?.displayName(for: context) ?? sender + + let notificationTitle = "Sesion" + var notificationBody = "\(senderName) reacts to your message with \(emoji)." + switch previewType { + case .namePreview: break + default: notificationBody = NotificationStrings.incomingMessageBody + } + + let category = AppNotificationCategory.incomingMessage + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId + ] + + DispatchQueue.main.async { + let sound = self.requestSound(thread: thread) + + self.adaptee.notify( + category: category, + title: notificationTitle, + body: notificationBody, + userInfo: userInfo, + sound: sound, + replacingIdentifier: UUID().uuidString + ) + } + } public func notifyForFailedSend(inThread thread: TSThread) { let notificationTitle: String? diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index b0343fc37..5ef9b24c2 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -377,32 +377,33 @@ extension MessageReceiver { // Get or create thread guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread } // Handle emoji reacts first - if let reaction = message.reaction, proto.dataMessage?.reaction != nil, let author = reaction.publicKey, let timestamp = reaction.timestamp { + if let reaction = message.reaction, proto.dataMessage?.reaction != nil, let author = reaction.publicKey, let timestamp = reaction.timestamp, let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) { var tsMessage: TSMessage? if author == getUserHexEncodedPublicKey() { tsMessage = TSOutgoingMessage.find(withTimestamp: timestamp) } else { tsMessage = TSIncomingMessage.find(withAuthorId: author, timestamp: timestamp, transaction: transaction) } - let reactionMessage = ReactMessage(timestamp: timestamp, authorId: author, emoji: reaction.emoji) - reactionMessage.sender = message.sender + let reactMessage = ReactMessage(timestamp: timestamp, authorId: author, emoji: reaction.emoji) + reactMessage.sender = message.sender if let serverID = message.openGroupServerMessageID { - reactionMessage.messageId = "\(serverID)" + reactMessage.messageId = "\(serverID)" // Create a lookup between the openGroupServerMessageId and the tsMessage id for easy lookup // For emoji reacts, the lookup is linking emoji react message server id to the id of the tsMessage that the emoji is reacted to if let openGroup: OpenGroupV2 = storage.getV2OpenGroup(for: threadID) { storage.addOpenGroupServerIdLookup(serverID, tsMessageId: tsMessage?.uniqueId, in: openGroup.room, on: openGroup.server, using: transaction) } } - if let serverHash = message.serverHash { reactionMessage.messageId = serverHash } + if let serverHash = message.serverHash { reactMessage.messageId = serverHash } switch reaction.kind { case .react: - tsMessage?.addReaction(reactionMessage, transaction: transaction) + tsMessage?.addReaction(reactMessage, transaction: transaction) case .remove: - tsMessage?.removeReaction(reactionMessage, transaction: transaction) + tsMessage?.removeReaction(reactMessage, transaction: transaction) case .none: break } + SSKEnvironment.shared.notificationsManager?.notifyUser(forReaction: reactMessage, in: thread, transaction: transaction) return "" } // Parse quote if needed diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h index 576adb737..7ec8970a7 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.h @@ -6,6 +6,7 @@ NS_ASSUME_NONNULL_BEGIN +@class SNReactMessage; @class TSErrorMessage; @class TSIncomingMessage; @class TSInfoMessage; @@ -22,8 +23,12 @@ NS_ASSUME_NONNULL_BEGIN transaction:(YapDatabaseReadTransaction *)transaction; - (void)notifyUserForIncomingCall:(TSInfoMessage *)callInfoMessage - inThread:(TSThread *)thread - transaction:(YapDatabaseReadTransaction *)transaction; + inThread:(TSThread *)thread + transaction:(YapDatabaseReadTransaction *)transaction; + +- (void)notifyUserForReaction:(SNReactMessage *)reactMessage + inThread:(TSThread *)thread + transaction:(YapDatabaseReadTransaction *)transaction; - (void)cancelNotification:(NSString *)identifier; - (void)clearAllNotifications; diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index c19184a5d..b7b747db1 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -110,19 +110,8 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { } else { trigger = nil } - - 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(forIncomingCall callInfoMessage: TSInfoMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) { @@ -149,19 +138,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationContent.body = String(format: "modal_call_missed_tips_explanation".localized(), thread.name(with: transaction)) } - // Add request - let identifier = UUID().uuidString - let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: nil) - 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") + addNotifcationRequest(identifier: UUID().uuidString, notificationContent: notificationContent, trigger: nil) + } + + public func notifyUser(forReaction reactMessage: ReactMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) { + } public func cancelNotification(_ identifier: String) { @@ -175,6 +156,20 @@ 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: nil) + 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/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift b/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift index 86b11b0b9..4dab041f0 100644 --- a/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift +++ b/SignalUtilitiesKit/Utilities/NoopNotificationsManager.swift @@ -13,6 +13,10 @@ public class NoopNotificationsManager: NSObject, NotificationsProtocol { owsFailDebug("") } + public func notifyUser(forReaction reactMessage: ReactMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) { + owsFailDebug("") + } + public func cancelNotification(_ identifier: String) { owsFailDebug("") } From 3a3ffc4289468265d33da6270ab03a8516f56a6a Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Tue, 14 Jun 2022 17:13:54 +1000 Subject: [PATCH 040/133] minor fix --- Session/Notifications/AppNotifications.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 1b33d1b0b..8d061325e 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -313,7 +313,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { let senderName = Storage.shared.getContact(with: sender, using: transaction)?.displayName(for: context) ?? sender let notificationTitle = "Sesion" - var notificationBody = "\(senderName) reacts to your message with \(emoji)." + var notificationBody = "\(senderName) reacts to your message with \(emoji)" switch previewType { case .namePreview: break default: notificationBody = NotificationStrings.incomingMessageBody From b9d5009c3df216e0dca05f6a449e01ed5c010541 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Tue, 14 Jun 2022 17:15:02 +1000 Subject: [PATCH 041/133] fix typo --- Session/Notifications/AppNotifications.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 8d061325e..85ec828ec 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -312,8 +312,8 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { let context = Contact.context(for: thread) let senderName = Storage.shared.getContact(with: sender, using: transaction)?.displayName(for: context) ?? sender - let notificationTitle = "Sesion" - var notificationBody = "\(senderName) reacts to your message with \(emoji)" + let notificationTitle = "Session" + var notificationBody = "\(senderName) reacts to a message with \(emoji)" switch previewType { case .namePreview: break default: notificationBody = NotificationStrings.incomingMessageBody From 0aed17c6b540f30b0497e5acebe171cb522a79a1 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 17 Jun 2022 10:58:45 +1000 Subject: [PATCH 042/133] feat: localise emoji reacts notification --- Session/Meta/Translations/en.lproj/Localizable.strings | 1 + Session/Notifications/AppNotifications.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index cae2d7c49..affc443b3 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -669,3 +669,4 @@ "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 %@"; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 85ec828ec..0fb20102d 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -313,7 +313,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { let senderName = Storage.shared.getContact(with: sender, using: transaction)?.displayName(for: context) ?? sender let notificationTitle = "Session" - var notificationBody = "\(senderName) reacts to a message with \(emoji)" + var notificationBody = String(format: "EMOJI_REACTS_NOTIFICATION".localized(), senderName, emoji) switch previewType { case .namePreview: break default: notificationBody = NotificationStrings.incomingMessageBody From 447d24898a84ce2663298e3f6d6594cf8ca38e27 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 17 Jun 2022 14:26:23 +1000 Subject: [PATCH 043/133] feat: Notification --- Session/Notifications/AppNotifications.swift | 1 + .../NSENotificationPresenter.swift | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 0fb20102d..b26198a89 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -306,6 +306,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { public func notifyUser(forReaction reactMessage: ReactMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) { guard !thread.isMuted else { return } guard !thread.isGroupThread() else { return } // We do NOT notify emoji reacts in groups + guard !thread.isMessageRequest(using: transaction) else { return } guard let sender = reactMessage.sender, let emoji = reactMessage.emoji else { return } guard let threadId = thread.uniqueId else { return } diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index b7b747db1..69f75660f 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -142,7 +142,33 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { } public func notifyUser(forReaction reactMessage: ReactMessage, in thread: TSThread, transaction: YapDatabaseReadTransaction) { + guard !thread.isMuted else { return } + guard !thread.isGroupThread() else { return } // We do NOT notify emoji reacts in groups + guard !thread.isMessageRequest(using: transaction) else { return } + guard let sender = reactMessage.sender, let emoji = reactMessage.emoji else { return } + guard let threadID = thread.uniqueId else { return } + let context = Contact.context(for: thread) + let senderName = Storage.shared.getContact(with: sender, using: transaction)?.displayName(for: context) ?? sender + + let notificationTitle = "Session" + var notificationBody = String(format: "EMOJI_REACTS_NOTIFICATION".localized(), senderName, emoji) + let notificationsPreference = Environment.shared.preferences!.notificationPreviewType() + switch notificationsPreference { + case .namePreview: break + default: notificationBody = NotificationStrings.incomingMessageBody + } + + var userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] + userInfo[NotificationServiceExtension.threadIdKey] = threadID + + let notificationContent = UNMutableNotificationContent() + notificationContent.userInfo = userInfo + notificationContent.sound = OWSSounds.notificationSound(for: thread).notificationSound(isQuiet: false) + notificationContent.title = notificationTitle + notificationContent.body = notificationBody + + addNotifcationRequest(identifier: UUID().uuidString, notificationContent: notificationContent, trigger: nil) } public func cancelNotification(_ identifier: String) { From 8fe2e2d1d90f5d2db7d6953eee1307f3f16c8bf1 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Jun 2022 11:12:21 +1000 Subject: [PATCH 044/133] ui: emoji picker background color fix --- .../Emoji Picker/EmojiPickerCollectionView.swift | 2 +- Session/Conversations/Emoji Picker/EmojiPickerSheet.swift | 2 +- Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index 68bd7061b..5769e7916 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -60,7 +60,7 @@ class EmojiPickerCollectionView: UICollectionView { withReuseIdentifier: EmojiSectionHeader.reuseIdentifier ) - backgroundColor = isDarkMode ? .ows_gray90 : .ows_white + backgroundColor = .clear let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) panGestureRecognizer.require(toFail: longPressGesture) diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift index 64a692d48..f1e14845d 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -22,7 +22,7 @@ class EmojiPickerSheet: BaseVC { private lazy var searchBar: SearchBar = { let result = SearchBar() result.tintColor = Colors.text - result.backgroundColor = isDarkMode ? .ows_gray90 : .ows_white + result.backgroundColor = .clear result.delegate = self return result }() diff --git a/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift b/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift index a6efa3361..ac7b5b34f 100644 --- a/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift +++ b/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift @@ -116,12 +116,12 @@ class EmojiSkinTonePicker: UIView { layer.shadowOpacity = 0.25 layer.shadowRadius = 4 - referenceOverlay.backgroundColor = isDarkMode ? .ows_gray75 : .ows_white + referenceOverlay.backgroundColor = Colors.modalBackground referenceOverlay.layer.cornerRadius = 9 addSubview(referenceOverlay) containerView.layoutMargins = UIEdgeInsets(top: 9, leading: 16, bottom: 9, trailing: 16) - containerView.backgroundColor = isDarkMode ? .ows_gray75 : .ows_white + containerView.backgroundColor = Colors.modalBackground containerView.layer.cornerRadius = 11 addSubview(containerView) containerView.autoPinWidthToSuperview() From 220a9ac4a1623a61a3fbf3d8392b3fe3e4c1658b Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Jun 2022 11:24:04 +1000 Subject: [PATCH 045/133] ui: fix in context menu vc emoji bar may be out of screen --- Session/Conversations/Context Menu/ContextMenuVC.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 7aee8bdc5..45c2c9f12 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -139,11 +139,11 @@ final class ContextMenuVC : UIViewController { let spacing = Values.smallSpacing let margin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing) if frame.maxY + spacing + menuHeight > UIScreen.main.bounds.height - margin { - menuView.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) - emojiBar.pin(.top, to: .bottom, of: snapshot, withInset: spacing) - } else { - menuView.pin(.top, to: .bottom, of: snapshot, withInset: spacing) emojiBar.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) + menuView.pin(.bottom, to: .top, of: emojiBar, withInset: -spacing) + } else { + emojiBar.pin(.top, to: .bottom, of: snapshot, withInset: spacing) + menuView.pin(.top, to: .bottom, of: emojiBar, withInset: spacing) } switch viewItem.interaction.interactionType() { case .outgoingMessage: From c91bdb3aebc341d5556c721e9f2f2e34b97db7cd Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Jun 2022 15:31:54 +1000 Subject: [PATCH 046/133] refactor: persist recent used emojis --- Session.xcodeproj/project.pbxproj | 4 ++ .../Context Menu/ContextMenuVC+Action.swift | 2 +- .../ContextMenuVC+EmojiReactsView.swift | 6 +-- .../Context Menu/ContextMenuVC.swift | 7 ++- .../ConversationVC+Interaction.swift | 18 ++++--- .../EmojiPickerCollectionView.swift | 52 ++----------------- .../Content Views/ReactionContainerView.swift | 12 ++--- .../Content Views/ReactionView.swift | 12 ++--- .../Message Cells/MessageCell.swift | 2 +- .../Message Cells/VisibleMessageCell.swift | 4 +- .../Views & Modals/ReactionListSheet.swift | 14 ++--- Session/Emoji/EmojiWithSkinTones.swift | 11 ++++ Session/Emoji/Storage+Emoji.swift | 36 +++++++++++++ .../General/SNUserDefaults.swift | 29 ----------- 14 files changed, 96 insertions(+), 113 deletions(-) create mode 100644 Session/Emoji/Storage+Emoji.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c7953fdac..46215da68 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -137,6 +137,7 @@ 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 */; }; + 7B1B52E2286030DF006069F2 /* Storage+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52E1286030DF006069F2 /* Storage+Emoji.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 */; }; @@ -1144,6 +1145,7 @@ 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 = ""; }; + 7B1B52E1286030DF006069F2 /* Storage+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Emoji.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 = ""; }; @@ -2160,6 +2162,7 @@ 7B9F71CD2852EEE2006DFE7B /* Emoji+SkinTones.swift */, 7B9F71CE2852EEE2006DFE7B /* Emoji.swift */, 7B9F71CF2852EEE2006DFE7B /* Emoji+Name.swift */, + 7B1B52E1286030DF006069F2 /* Storage+Emoji.swift */, ); path = Emoji; sourceTree = ""; @@ -5092,6 +5095,7 @@ C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */, C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, + 7B1B52E2286030DF006069F2 /* Storage+Emoji.swift in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, 7B9F71D22852EEE2006DFE7B /* Emoji+SkinTones.swift in Sources */, 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */, diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 4b324c522..bbd8e53f4 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -95,7 +95,7 @@ protocol ContextMenuActionDelegate : AnyObject { func save(_ viewItem: ConversationViewItem) func ban(_ viewItem: ConversationViewItem) func banAndDeleteAllMessages(_ viewItem: ConversationViewItem) - func react(_ viewItem: ConversationViewItem, with emoji: String) + func react(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones) func showFullEmojiKeyboard(_ viewItem: ConversationViewItem) func contextMenuDismissed() } diff --git a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift index 031c216e2..9c1e00d0b 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+EmojiReactsView.swift @@ -2,7 +2,7 @@ extension ContextMenuVC { final class EmojiReactsView: UIView { - private let emoji: String + private let emoji: EmojiWithSkinTones private let dismiss: () -> Void private let work: () -> Void @@ -10,7 +10,7 @@ extension ContextMenuVC { private static let size: CGFloat = 40 // MARK: Lifecycle - init(for emoji: String, dismiss: @escaping () -> Void, work: @escaping () -> Void) { + init(for emoji: EmojiWithSkinTones, dismiss: @escaping () -> Void, work: @escaping () -> Void) { self.emoji = emoji self.dismiss = dismiss self.work = work @@ -28,7 +28,7 @@ extension ContextMenuVC { private func setUpViewHierarchy() { let emojiLabel = UILabel() - emojiLabel.text = self.emoji + emojiLabel.text = self.emoji.rawValue emojiLabel.font = .systemFont(ofSize: Values.veryLargeFontSize) emojiLabel.set(.height, to: ContextMenuVC.EmojiReactsView.size) addSubview(emojiLabel) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 45c2c9f12..206901b28 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -5,6 +5,8 @@ final class ContextMenuVC : UIViewController { private let frame: CGRect private let dismiss: () -> Void private weak var delegate: ContextMenuActionDelegate? + + private var recentEmoji: [EmojiWithSkinTones] = [] // MARK: UI Components private lazy var blurView = UIVisualEffectView(effect: nil) @@ -58,6 +60,9 @@ final class ContextMenuVC : UIViewController { self.delegate = delegate self.dismiss = dismiss super.init(nibName: nil, bundle: nil) + Storage.read { transaction in + self.recentEmoji = Array(Storage.shared.getRecentEmoji(transaction: transaction)[...5]) + } } override init(nibName: String?, bundle: Bundle?) { @@ -106,7 +111,7 @@ final class ContextMenuVC : UIViewController { emojiPlusButton.pin(.right, to: .right, of: emojiBar, withInset: -Values.smallSpacing) emojiPlusButton.center(.vertical, in: emojiBar) - let emojiLabels = UserDefaults.standard.getRecentlyUsedEmojis().map { emoji -> EmojiReactsView in + let emojiLabels = recentEmoji.map { emoji -> EmojiReactsView in EmojiReactsView(for: emoji, dismiss: snDismiss) { self.delegate?.react(self.viewItem, with: emoji) } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index b16e8871b..e97fbb2dd 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -822,7 +822,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc presentAlert(alert) } - func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?) { + func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: EmojiWithSkinTones?) { guard let thread = thread as? TSGroupThread else { return } guard let message = viewItem.interaction as? TSMessage, message.reactions.count > 0 else { return } let reactionListSheet = ReactionListSheet(for: viewItem, thread: thread) @@ -833,17 +833,19 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc present(reactionListSheet, animated: true, completion: nil) } - func react(_ viewItem: ConversationViewItem, with emoji: String) { - UserDefaults.standard.addNewRecentlyUsedEmoji(emoji) - react(viewItem, with: emoji, cancel: false) + func react(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones) { + Storage.write { transaction in + Storage.shared.recordRecentEmoji(emoji, transaction: transaction) + } + react(viewItem, with: emoji.rawValue, cancel: false) } - func quickReact(_ viewItem: ConversationViewItem, with emoji: String) { + func quickReact(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones) { react(viewItem, with: emoji) } - func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) { - react(viewItem, with: emoji, cancel: true) + func cancelReact(_ viewItem: ConversationViewItem, for emoji: EmojiWithSkinTones) { + react(viewItem, with: emoji.rawValue, cancel: true) } func cancelAllReact(reactMessages: [ReactMessage]) { @@ -885,7 +887,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let emojiPicker = EmojiPickerSheet( completionHandler: { emoji in if let emoji = emoji { - self.react(viewItem, with: emoji.rawValue) + self.react(viewItem, with: emoji) } }, dismissHandler: { diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index 5769e7916..6eea609b1 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -7,9 +7,6 @@ protocol EmojiPickerCollectionViewDelegate: AnyObject { class EmojiPickerCollectionView: UICollectionView { let layout: UICollectionViewFlowLayout - private static let emojiPickerCollection = "EmojiPickerCollection" - private static let recentEmojiKey = "recentEmoji" - weak var pickerDelegate: EmojiPickerCollectionViewDelegate? private var recentEmoji: [EmojiWithSkinTones] = [] @@ -70,12 +67,7 @@ class EmojiPickerCollectionView: UICollectionView { tapGestureRecognizer.delegate = self Storage.read { transaction in - let rawRecentEmoji = transaction.object( - forKey: EmojiPickerCollectionView.recentEmojiKey, - inCollection: EmojiPickerCollectionView.emojiPickerCollection - ) as? [String] ?? [] - - self.recentEmoji = rawRecentEmoji.compactMap { EmojiWithSkinTones(rawValue: $0) } + self.recentEmoji = Storage.shared.getRecentEmoji(transaction: transaction) // 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 @@ -140,29 +132,6 @@ class EmojiPickerCollectionView: UICollectionView { return category.localizedName } - func recordRecentEmoji(_ emoji: EmojiWithSkinTones, transaction: YapDatabaseReadWriteTransaction) { - guard recentEmoji.first != emoji else { return } - guard emoji.isNormalized else { - recordRecentEmoji(emoji.normalized, transaction: transaction) - return - } - - var newRecentEmoji = recentEmoji - - // Remove any existing entries for this emoji - newRecentEmoji.removeAll { emoji == $0 } - // Insert the selected emoji at the start of the list - newRecentEmoji.insert(emoji, at: 0) - // Truncate the recent emoji list to a maximum of 50 stored - newRecentEmoji = Array(newRecentEmoji[0.. maxEmojisPerLine { displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)]) @@ -111,7 +111,7 @@ final class ReactionContainerView : UIView { private func updateAllReactions() { var reactions = self.reactions while reactions.count > 0 { - var line: [(String, (Int, Bool))] = [] + var line: [(EmojiWithSkinTones, (Int, Bool))] = [] while reactions.count > 0 && line.count < maxEmojisPerLine { line.append(reactions.removeFirst()) } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 3c1c6cd8d..1276d5c09 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -1,7 +1,7 @@ import UIKit final class ReactionButton : UIView { - let emoji: String + let emoji: EmojiWithSkinTones let number: Int let showBorder: Bool let showNumber: Bool @@ -13,7 +13,7 @@ final class ReactionButton : UIView { private var spacing: CGFloat = Values.verySmallSpacing // MARK: Lifecycle - init(emoji: String, value: Int, showBorder: Bool = false, showNumber: Bool = true) { + init(emoji: EmojiWithSkinTones, value: Int, showBorder: Bool = false, showNumber: Bool = true) { self.emoji = emoji self.number = value self.showBorder = showBorder @@ -32,7 +32,7 @@ final class ReactionButton : UIView { private func setUpViewHierarchy() { let emojiLabel = UILabel() - emojiLabel.text = emoji + emojiLabel.text = emoji.rawValue emojiLabel.font = .systemFont(ofSize: fontSize) let stackView = UIStackView(arrangedSubviews: [ emojiLabel ]) @@ -63,14 +63,14 @@ final class ReactionButton : UIView { } final class ExpandingReactionButton: UIView { - private let emojis: [String] + private let emojis: [EmojiWithSkinTones] // MARK: Settings private let size: CGFloat = 22 private let margin: CGFloat = 15 // MARK: Lifecycle - init(emojis: [String]) { + init(emojis: [EmojiWithSkinTones]) { self.emojis = emojis super.init(frame: CGRect.zero) setUpViewHierarchy() @@ -96,7 +96,7 @@ final class ExpandingReactionButton: UIView { container.layer.borderColor = isDarkMode ? UIColor.black.cgColor : UIColor.white.cgColor let emojiLabel = UILabel() - emojiLabel.text = emoji + emojiLabel.text = emoji.rawValue emojiLabel.font = .systemFont(ofSize: Values.verySmallFontSize) container.addSubview(emojiLabel) diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index 03bac3d15..f8a5169a0 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -80,6 +80,6 @@ protocol MessageCellDelegate : ReactionDelegate { func openURL(_ url: URL) func handleReplyButtonTapped(for viewItem: ConversationViewItem) func showUserDetails(for sessionID: String) - func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: String?) + func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: EmojiWithSkinTones?) func needsLayout() } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index f92f06979..3e33d4c4a 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -457,9 +457,9 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } private func populateReaction(for viewItem: ConversationViewItem, message: TSMessage) { - let reactions: OrderedDictionary = OrderedDictionary() + let reactions: OrderedDictionary = OrderedDictionary() for reaction in message.reactions { - if let reactMessage = reaction as? ReactMessage, let emoji = reactMessage.emoji { + if let reactMessage = reaction as? ReactMessage, let rawEmoji = reactMessage.emoji, let emoji = EmojiWithSkinTones(rawValue: rawEmoji) { let isSelfSend = (reactMessage.sender! == getUserHexEncodedPublicKey()) if let value = reactions.value(forKey: emoji) { reactions.replace(key: emoji, value: (value.0 + 1, value.1 || isSelfSend)) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index fb688a698..e407a23e1 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -3,8 +3,8 @@ final class ReactionListSheet : BaseVC { private let thread: TSGroupThread private let viewItem: ConversationViewItem private var reactions: [ReactMessage] = [] - private var reactionMap: OrderedDictionary = OrderedDictionary() - var selectedReaction: String? + private var reactionMap: OrderedDictionary = OrderedDictionary() + var selectedReaction: EmojiWithSkinTones? var delegate: ReactionDelegate? // MARK: Components @@ -151,7 +151,7 @@ final class ReactionListSheet : BaseVC { self.reactions = message.reactions as! [ReactMessage] } for reaction in reactions { - if let emoji = reaction.emoji { + if let rawEmoji = reaction.emoji, let emoji = EmojiWithSkinTones(rawValue: rawEmoji) { if !reactionMap.hasValue(forKey: emoji) { reactionMap.append(key: emoji, value: []) } var value = reactionMap.value(forKey: emoji)! if reaction.sender == getUserHexEncodedPublicKey() { @@ -170,7 +170,7 @@ final class ReactionListSheet : BaseVC { private func reloadData() { reactionContainer.reloadData() let seletedData = reactionMap.value(forKey: selectedReaction!)! - detailInfoLabel.text = "\(selectedReaction!) ยท \(seletedData.count)" + detailInfoLabel.text = "\(selectedReaction!.rawValue) ยท \(seletedData.count)" if thread.isOpenGroup, let threadId = thread.uniqueId, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) { let isUserModerator = OpenGroupAPIV2.isUserModerator(getUserHexEncodedPublicKey(), for: openGroupV2.room, on: openGroupV2.server) clearAllButton.isHidden = !isUserModerator @@ -227,7 +227,7 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell let item = reactionMap.orderedItems[indexPath.item] - cell.data = (item.0, item.1.count) + cell.data = (item.0.rawValue, item.1.count) cell.isCurrentSelection = item.0 == selectedReaction! return cell } @@ -350,8 +350,8 @@ extension ReactionListSheet { protocol ReactionDelegate : AnyObject { - func quickReact(_ viewItem: ConversationViewItem, with emoji: String) - func cancelReact(_ viewItem: ConversationViewItem, for emoji: String) + func quickReact(_ viewItem: ConversationViewItem, with emoji: EmojiWithSkinTones) + func cancelReact(_ viewItem: ConversationViewItem, for emoji: EmojiWithSkinTones) func cancelAllReact(reactMessages: [ReactMessage]) } diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift index 26b1a14be..6c62ff255 100644 --- a/Session/Emoji/EmojiWithSkinTones.swift +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -26,6 +26,17 @@ public struct EmojiWithSkinTones: Hashable { return baseEmoji.rawValue } } + + var normalized: EmojiWithSkinTones { + switch (baseEmoji, skinTones) { + case (let base, nil) where base.normalized != base: + return EmojiWithSkinTones(baseEmoji: base.normalized) + default: + return self + } + } + + var isNormalized: Bool { self == normalized } } extension Emoji { diff --git a/Session/Emoji/Storage+Emoji.swift b/Session/Emoji/Storage+Emoji.swift new file mode 100644 index 000000000..35db167db --- /dev/null +++ b/Session/Emoji/Storage+Emoji.swift @@ -0,0 +1,36 @@ +extension Storage { + + private static let emojiPickerCollection = "EmojiPickerCollection" + private static let recentEmojiKey = "recentEmoji" + + func getRecentEmoji(transaction: YapDatabaseReadTransaction) -> [EmojiWithSkinTones] { + var rawRecentEmoji = transaction.object(forKey: Self.recentEmojiKey, inCollection: Self.emojiPickerCollection) as? [String] ?? [] + let defaultEmoji = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"].filter{ !rawRecentEmoji.contains($0) } + + if rawRecentEmoji.count < 6 { + rawRecentEmoji.append(contentsOf: defaultEmoji[..<(defaultEmoji.count - rawRecentEmoji.count)]) + } + + return rawRecentEmoji.compactMap { EmojiWithSkinTones(rawValue: $0) } + } + + func recordRecentEmoji(_ emoji: EmojiWithSkinTones, transaction: YapDatabaseReadWriteTransaction) { + let recentEmoji = getRecentEmoji(transaction: transaction) + guard recentEmoji.first != emoji else { return } + guard emoji.isNormalized else { + recordRecentEmoji(emoji.normalized, transaction: transaction) + return + } + + var newRecentEmoji = recentEmoji + + // Remove any existing entries for this emoji + newRecentEmoji.removeAll { emoji == $0 } + // Insert the selected emoji at the start of the list + newRecentEmoji.insert(emoji, at: 0) + // Truncate the recent emoji list to a maximum of 50 stored + newRecentEmoji = Array(newRecentEmoji[0.. [String] { - get { return self.stringArray(forKey: array.rawValue) ?? []} - set { set(newValue, forKey: array.rawValue) } - } - - func getRecentlyUsedEmojis() -> [String] { - let result = self[.recentlyUsedEmojis] - if result.isEmpty { - return ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"] - } - return result - } - - func addNewRecentlyUsedEmoji(_ emoji: String) { - var recentlyUsedEmojis = getRecentlyUsedEmojis() - if let index = recentlyUsedEmojis.firstIndex(of: emoji) { - recentlyUsedEmojis.remove(at: index) - } - if recentlyUsedEmojis.count >= 6 { - recentlyUsedEmojis.remove(at: 5) - } - recentlyUsedEmojis.insert(emoji, at: 0) - self[.recentlyUsedEmojis] = recentlyUsedEmojis - } } From 4c011f14e87a5bb8f26716ee975a61f93268a6e7 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Jun 2022 15:36:01 +1000 Subject: [PATCH 047/133] fix: remove default emojis from recently used --- Session/Conversations/Context Menu/ContextMenuVC.swift | 2 +- .../Emoji Picker/EmojiPickerCollectionView.swift | 2 +- Session/Emoji/Storage+Emoji.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 206901b28..524d923bd 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -61,7 +61,7 @@ final class ContextMenuVC : UIViewController { self.dismiss = dismiss super.init(nibName: nil, bundle: nil) Storage.read { transaction in - self.recentEmoji = Array(Storage.shared.getRecentEmoji(transaction: transaction)[...5]) + self.recentEmoji = Array(Storage.shared.getRecentEmoji(withDefaultEmoji: true, transaction: transaction)[...5]) } } diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index 6eea609b1..7bb1f358b 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -67,7 +67,7 @@ class EmojiPickerCollectionView: UICollectionView { tapGestureRecognizer.delegate = self Storage.read { transaction in - self.recentEmoji = Storage.shared.getRecentEmoji(transaction: transaction) + self.recentEmoji = Storage.shared.getRecentEmoji(withDefaultEmoji: false, transaction: transaction) // 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 diff --git a/Session/Emoji/Storage+Emoji.swift b/Session/Emoji/Storage+Emoji.swift index 35db167db..69cf017b7 100644 --- a/Session/Emoji/Storage+Emoji.swift +++ b/Session/Emoji/Storage+Emoji.swift @@ -3,11 +3,11 @@ extension Storage { private static let emojiPickerCollection = "EmojiPickerCollection" private static let recentEmojiKey = "recentEmoji" - func getRecentEmoji(transaction: YapDatabaseReadTransaction) -> [EmojiWithSkinTones] { + func getRecentEmoji(withDefaultEmoji: Bool, transaction: YapDatabaseReadTransaction) -> [EmojiWithSkinTones] { var rawRecentEmoji = transaction.object(forKey: Self.recentEmojiKey, inCollection: Self.emojiPickerCollection) as? [String] ?? [] let defaultEmoji = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"].filter{ !rawRecentEmoji.contains($0) } - if rawRecentEmoji.count < 6 { + if rawRecentEmoji.count < 6 && withDefaultEmoji { rawRecentEmoji.append(contentsOf: defaultEmoji[..<(defaultEmoji.count - rawRecentEmoji.count)]) } @@ -15,7 +15,7 @@ extension Storage { } func recordRecentEmoji(_ emoji: EmojiWithSkinTones, transaction: YapDatabaseReadWriteTransaction) { - let recentEmoji = getRecentEmoji(transaction: transaction) + let recentEmoji = getRecentEmoji(withDefaultEmoji: false, transaction: transaction) guard recentEmoji.first != emoji else { return } guard emoji.isNormalized else { recordRecentEmoji(emoji.normalized, transaction: transaction) From 841d34d3ef2ee4daea26844811e6dfce4a840399 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Jun 2022 16:09:04 +1000 Subject: [PATCH 048/133] fix: tap and long press emoji below message bubble --- Session/Conversations/Message Cells/VisibleMessageCell.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 3e33d4c4a..ab697e160 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -544,7 +544,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { if reactionContainerView.frame.contains(location) { let convertedLocation = reactionContainerView.convert(location, from: self) for reactionView in reactionContainerView.reactionViews { - if reactionView.frame.contains(convertedLocation) { + if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { delegate?.showReactionList(viewItem, selectedReaction: reactionView.emoji) break } @@ -568,7 +568,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } else if reactionContainerView.frame.contains(location) { let convertedLocation = reactionContainerView.convert(location, from: self) for reactionView in reactionContainerView.reactionViews { - if reactionView.frame.contains(convertedLocation) { + if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { if reactionView.showBorder { delegate?.cancelReact(viewItem, for: reactionView.emoji) } else { From 30ebb3f53fa556235e26c9d0b72f82de04ffe8bf Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Jun 2022 16:41:45 +1000 Subject: [PATCH 049/133] ui: fix reaction container view --- .../Message Cells/Content Views/ReactionContainerView.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 16e5f3707..3113d5fe4 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -110,14 +110,18 @@ final class ReactionContainerView : UIView { private func updateAllReactions() { var reactions = self.reactions + var numberOfLines = 0 while reactions.count > 0 { var line: [(EmojiWithSkinTones, (Int, Bool))] = [] while reactions.count > 0 && line.count < maxEmojisPerLine { line.append(reactions.removeFirst()) } updateCollapsedReactions(line) + numberOfLines += 1 + } + if numberOfLines > 1 { + mainStackView.addArrangedSubview(collapseButton) } - mainStackView.addArrangedSubview(collapseButton) } private func prepareForUpdate() { From ab05b5ecf4fd6488ec4f3d7eabd04bcefd8bed22 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Jun 2022 16:50:36 +1000 Subject: [PATCH 050/133] minor fix --- .../Message Cells/Content Views/ReactionContainerView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 3113d5fe4..d9eaeeba1 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -121,6 +121,8 @@ final class ReactionContainerView : UIView { } if numberOfLines > 1 { mainStackView.addArrangedSubview(collapseButton) + } else { + showingAllReactions = false } } From 4934714eae65e1d6e4aa82894f1903bb0df3ee1d Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 22 Jun 2022 13:51:36 +1000 Subject: [PATCH 051/133] feat: rate limit --- .../Conversations/ConversationVC+Interaction.swift | 11 +++++++++++ SessionMessagingKit/Utilities/General.swift | 1 + 2 files changed, 12 insertions(+) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index e97fbb2dd..a4869382f 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -862,6 +862,17 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc reactMessage.sender = getUserHexEncodedPublicKey() let thread = self.thread let sentTimestamp: UInt64 = NSDate.millisecondTimestamp() + General.Cache.recentReactionTimestamps.mutate{ $0.append(sentTimestamp) } + // Rate Limit + if General.Cache.recentReactionTimestamps.wrappedValue.count > 20 { + let firstTimestamp = General.Cache.recentReactionTimestamps.wrappedValue.first! + if sentTimestamp - firstTimestamp < 60 * 1000 { + General.Cache.recentReactionTimestamps.mutate{ $0.removeLast() } + return + } else { + General.Cache.recentReactionTimestamps.mutate{ $0.removeFirst() } + } + } let visibleMessage = VisibleMessage() visibleMessage.sentTimestamp = sentTimestamp visibleMessage.reaction = .from(reactMessage) diff --git a/SessionMessagingKit/Utilities/General.swift b/SessionMessagingKit/Utilities/General.swift index d2e4e0c96..a9d4873ff 100644 --- a/SessionMessagingKit/Utilities/General.swift +++ b/SessionMessagingKit/Utilities/General.swift @@ -3,6 +3,7 @@ import Foundation public enum General { public enum Cache { public static var cachedEncodedPublicKey: Atomic = Atomic(nil) + public static var recentReactionTimestamps: Atomic<[UInt64]> = Atomic([]) } } From d83100e3a033e9c2a940a1330efad5d1f5043423 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 22 Jun 2022 13:56:32 +1000 Subject: [PATCH 052/133] add comments --- Session/Conversations/ConversationVC+Interaction.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index a4869382f..98158a46e 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -868,7 +868,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let firstTimestamp = General.Cache.recentReactionTimestamps.wrappedValue.first! if sentTimestamp - firstTimestamp < 60 * 1000 { General.Cache.recentReactionTimestamps.mutate{ $0.removeLast() } - return + return // Reach the limit 20 reacts per 60s } else { General.Cache.recentReactionTimestamps.mutate{ $0.removeFirst() } } From b6b99fac694a04c96d896394d656ea0531f7bb6e Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 23 Jun 2022 10:49:01 +1000 Subject: [PATCH 053/133] refactor: use view model for reaction button --- .../Content Views/ReactionContainerView.swift | 14 +++++----- .../Content Views/ReactionView.swift | 26 ++++++++++++------- .../Message Cells/VisibleMessageCell.swift | 10 +++---- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index d9eaeeba1..5ba01339b 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -21,7 +21,7 @@ final class ReactionContainerView : UIView { private var showNumbers = true private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 - var reactions: [(EmojiWithSkinTones, (Int, Bool))] = [] + var reactions: [ReactionViewModel] = [] var reactionViews: [ReactionButton] = [] var expandButton: ExpandingReactionButton? var collapseButton: UIStackView = { @@ -58,7 +58,7 @@ final class ReactionContainerView : UIView { mainStackView.pin(to: self) } - public func update(_ reactions: [(EmojiWithSkinTones, (Int, Bool))], isOutgoingMessage: Bool, showNumbers: Bool) { + public func update(_ reactions: [ReactionViewModel], isOutgoingMessage: Bool, showNumbers: Bool) { self.reactions = reactions self.isOutgoingMessage = isOutgoingMessage self.showNumbers = showNumbers @@ -70,7 +70,7 @@ final class ReactionContainerView : UIView { } } - private func updateCollapsedReactions(_ reactions: [(EmojiWithSkinTones, (Int, Bool))]) { + private func updateCollapsedReactions(_ reactions: [ReactionViewModel]) { let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = Values.smallSpacing @@ -83,19 +83,19 @@ final class ReactionContainerView : UIView { reactionContainerView.semanticContentAttribute = .unspecified } - var displayedReactions: [(EmojiWithSkinTones, (Int, Bool))] + var displayedReactions: [ReactionViewModel] var expandButtonReactions: [EmojiWithSkinTones] if reactions.count > maxEmojisPerLine { displayedReactions = Array(reactions[0...(maxEmojisPerLine - 3)]) - expandButtonReactions = Array(reactions[(maxEmojisPerLine - 2)...maxEmojisPerLine]).map{ $0.0 } + expandButtonReactions = Array(reactions[(maxEmojisPerLine - 2)...maxEmojisPerLine]).map{ $0.emoji } } else { displayedReactions = reactions expandButtonReactions = [] } for reaction in displayedReactions { - let reactionView = ReactionButton(emoji: reaction.0, value: reaction.1.0, showBorder: reaction.1.1, showNumber: showNumbers) + let reactionView = ReactionButton(viewModel: reaction, showNumber: showNumbers) stackView.addArrangedSubview(reactionView) reactionViews.append(reactionView) } @@ -112,7 +112,7 @@ final class ReactionContainerView : UIView { var reactions = self.reactions var numberOfLines = 0 while reactions.count > 0 { - var line: [(EmojiWithSkinTones, (Int, Bool))] = [] + var line: [ReactionViewModel] = [] while reactions.count > 0 && line.count < maxEmojisPerLine { line.append(reactions.removeFirst()) } diff --git a/Session/Conversations/Message Cells/Content Views/ReactionView.swift b/Session/Conversations/Message Cells/Content Views/ReactionView.swift index 1276d5c09..16ec21b32 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionView.swift @@ -1,9 +1,19 @@ import UIKit -final class ReactionButton : UIView { +public struct ReactionViewModel: Hashable { let emoji: EmojiWithSkinTones let number: Int let showBorder: Bool + + init(emoji: EmojiWithSkinTones, value: Int, showBorder: Bool) { + self.emoji = emoji + self.number = value + self.showBorder = showBorder + } +} + +final class ReactionButton: UIView { + let viewModel: ReactionViewModel let showNumber: Bool // MARK: Settings @@ -13,10 +23,8 @@ final class ReactionButton : UIView { private var spacing: CGFloat = Values.verySmallSpacing // MARK: Lifecycle - init(emoji: EmojiWithSkinTones, value: Int, showBorder: Bool = false, showNumber: Bool = true) { - self.emoji = emoji - self.number = value - self.showBorder = showBorder + init(viewModel: ReactionViewModel, showNumber: Bool = true) { + self.viewModel = viewModel self.showNumber = showNumber super.init(frame: CGRect.zero) setUpViewHierarchy() @@ -32,7 +40,7 @@ final class ReactionButton : UIView { private func setUpViewHierarchy() { let emojiLabel = UILabel() - emojiLabel.text = emoji.rawValue + emojiLabel.text = viewModel.emoji.rawValue emojiLabel.font = .systemFont(ofSize: fontSize) let stackView = UIStackView(arrangedSubviews: [ emojiLabel ]) @@ -48,13 +56,13 @@ final class ReactionButton : UIView { backgroundColor = Colors.receivedMessageBackground layer.cornerRadius = self.height / 2 - if showBorder { + if viewModel.showBorder { self.addBorder(with: Colors.accent) } - if showNumber || self.number > 1 { + if showNumber || viewModel.number > 1 { let numberLabel = UILabel() - numberLabel.text = self.number < 1000 ? "\(number)" : String(format: "%.1f", Float(number) / 1000) + "k" + 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) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index ab697e160..4e805ecf9 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -468,7 +468,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } } - reactionContainerView.update(reactions.orderedItems, isOutgoingMessage: direction == .outgoing, showNumbers: thread!.isGroupThread()) + reactionContainerView.update(reactions.orderedItems.map { ReactionViewModel(emoji: $0.0, value: $0.1.0, showBorder:$0.1.1 )}, isOutgoingMessage: direction == .outgoing, showNumbers: thread!.isGroupThread()) } override func layoutSubviews() { @@ -545,7 +545,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let convertedLocation = reactionContainerView.convert(location, from: self) for reactionView in reactionContainerView.reactionViews { if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { - delegate?.showReactionList(viewItem, selectedReaction: reactionView.emoji) + delegate?.showReactionList(viewItem, selectedReaction: reactionView.viewModel.emoji) break } } @@ -569,10 +569,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let convertedLocation = reactionContainerView.convert(location, from: self) for reactionView in reactionContainerView.reactionViews { if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) { - if reactionView.showBorder { - delegate?.cancelReact(viewItem, for: reactionView.emoji) + if reactionView.viewModel.showBorder { + delegate?.cancelReact(viewItem, for: reactionView.viewModel.emoji) } else { - delegate?.quickReact(viewItem, with: reactionView.emoji) + delegate?.quickReact(viewItem, with: reactionView.viewModel.emoji) } return } From ee9d856e3c61d1d535b7ef3cba74769bbab4d735 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 23 Jun 2022 16:21:02 +1000 Subject: [PATCH 054/133] ui: fix reaction views automatically collapse --- Session/Conversations/ConversationVC.swift | 11 ++++++++++- Session/Conversations/ConversationViewItem.h | 2 ++ Session/Conversations/ConversationViewItem.m | 8 ++++++++ .../Content Views/ReactionContainerView.swift | 2 +- Session/Conversations/Message Cells/MessageCell.swift | 2 +- .../Message Cells/VisibleMessageCell.swift | 5 +++-- 6 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index d01ebfa9f..122acf7da 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -39,6 +39,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat var baselineKeyboardHeight: CGFloat = 0 // Reaction var showingReactionListForMessageId: String? + var reactionExpandedMessageIds: Set = [] var audioSession: OWSAudioSession { Environment.shared.audioSession } var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection } @@ -454,6 +455,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let viewItem = viewItems[indexPath.row] + viewItem.reactionShouldExpanded = reactionExpandedMessageIds.contains(viewItem.interaction.uniqueId ?? "") let cell = tableView.dequeueReusableCell(withIdentifier: MessageCell.getCellType(for: viewItem).identifier) as! MessageCell cell.delegate = self cell.thread = thread @@ -767,11 +769,18 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat return UITableView.automaticDimension } - func needsLayout() { + func needsLayout(for viewItem: ConversationViewItem, expandingReactions: Bool) { UIView.setAnimationsEnabled(false) messagesTableView.beginUpdates() messagesTableView.endUpdates() UIView.setAnimationsEnabled(true) + if let messageId = viewItem.interaction.uniqueId { + if expandingReactions { + reactionExpandedMessageIds.insert(messageId) + } else { + reactionExpandedMessageIds.remove(messageId) + } + } } func getMediaCache() -> NSCache { diff --git a/Session/Conversations/ConversationViewItem.h b/Session/Conversations/ConversationViewItem.h index 46ba1edaa..58877cb23 100644 --- a/Session/Conversations/ConversationViewItem.h +++ b/Session/Conversations/ConversationViewItem.h @@ -121,6 +121,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic, readonly, nullable) NSString *systemMessageText; +@property (nonatomic) BOOL reactionShouldExpanded; + // NOTE: This property is only set for incoming messages. @property (nonatomic, readonly, nullable) NSString *authorConversationColorName; diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations/ConversationViewItem.m index d39057a01..575a7bcdd 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations/ConversationViewItem.m @@ -121,6 +121,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @synthesize lastAudioMessageView = _lastAudioMessageView; @synthesize senderName = _senderName; @synthesize shouldHideFooter = _shouldHideFooter; +@synthesize reactionShouldExpanded = _reactionShouldExpanded; - (instancetype)initWithInteraction:(TSInteraction *)interaction isGroupThread:(BOOL)isGroupThread @@ -295,6 +296,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self clearCachedLayoutState]; } +- (void)setReactionShouldExpanded:(BOOL)reactionShouldExpanded +{ + _reactionShouldExpanded = reactionShouldExpanded; + + [self clearCachedLayoutState]; +} + - (void)clearCachedLayoutState { self.cachedCellSize = nil; diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 5ba01339b..70041c6eb 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -16,7 +16,7 @@ final class ReactionContainerView : UIView { return result }() - private var showingAllReactions = false + var showingAllReactions = false private var isOutgoingMessage = false private var showNumbers = true private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 diff --git a/Session/Conversations/Message Cells/MessageCell.swift b/Session/Conversations/Message Cells/MessageCell.swift index f8a5169a0..dd3190c94 100644 --- a/Session/Conversations/Message Cells/MessageCell.swift +++ b/Session/Conversations/Message Cells/MessageCell.swift @@ -81,5 +81,5 @@ protocol MessageCellDelegate : ReactionDelegate { func handleReplyButtonTapped(for viewItem: ConversationViewItem) func showUserDetails(for sessionID: String) func showReactionList(_ viewItem: ConversationViewItem, selectedReaction: EmojiWithSkinTones?) - func needsLayout() + func needsLayout(for viewItem: ConversationViewItem, expandingReactions: Bool) } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 4e805ecf9..cc14c4285 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -468,6 +468,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } } + reactionContainerView.showingAllReactions = viewItem.reactionShouldExpanded reactionContainerView.update(reactions.orderedItems.map { ReactionViewModel(emoji: $0.0, value: $0.1.0, showBorder:$0.1.1 )}, isOutgoingMessage: direction == .outgoing, showNumbers: thread!.isGroupThread()) } @@ -579,11 +580,11 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } if let expandButton = reactionContainerView.expandButton, expandButton.frame.contains(convertedLocation) { reactionContainerView.showAllEmojis() - delegate?.needsLayout() + delegate?.needsLayout(for: viewItem, expandingReactions: true) } if reactionContainerView.collapseButton.frame.contains(convertedLocation) { reactionContainerView.showLessEmojis() - delegate?.needsLayout() + delegate?.needsLayout(for: viewItem, expandingReactions: false) } } else { delegate?.handleViewItemTapped(viewItem, gestureRecognizer: gestureRecognizer) From 42fd617c348a23f71e8618088c9519d46d172bc2 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 27 Jun 2022 10:16:38 +1000 Subject: [PATCH 055/133] fix: self send emoji reacts notification --- .../Sending & Receiving/MessageReceiver+Handling.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 5ef9b24c2..ec1f78b17 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -403,7 +403,9 @@ extension MessageReceiver { case .none: break } - SSKEnvironment.shared.notificationsManager?.notifyUser(forReaction: reactMessage, in: thread, transaction: transaction) + if message.sender != getUserHexEncodedPublicKey() { + SSKEnvironment.shared.notificationsManager?.notifyUser(forReaction: reactMessage, in: thread, transaction: transaction) + } return "" } // Parse quote if needed From 45998a3066fa9cd08b11b2a7345fa7c8436bdaf3 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 27 Jun 2022 17:11:02 +1000 Subject: [PATCH 056/133] WIP: fix long press on a message which is too tall to show --- .../Context Menu/ContextMenuVC.swift | 41 +++++++++++++------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 524d923bd..d96e2b8f3 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -1,3 +1,5 @@ +import CoreGraphics +import UIKit final class ContextMenuVC : UIViewController { private let snapshot: UIView @@ -86,10 +88,6 @@ final class ContextMenuVC : UIViewController { snapshot.layer.shadowOpacity = 0.4 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) @@ -142,14 +140,13 @@ final class ContextMenuVC : UIViewController { // Constrains let menuHeight = CGFloat(actionViews.count) * ContextMenuVC.actionViewHeight let spacing = Values.smallSpacing - let margin = max(UIApplication.shared.keyWindow!.safeAreaInsets.bottom, Values.mediumSpacing) - if frame.maxY + spacing + menuHeight > UIScreen.main.bounds.height - margin { - emojiBar.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) - menuView.pin(.bottom, to: .top, of: emojiBar, withInset: -spacing) - } else { - emojiBar.pin(.top, to: .bottom, of: snapshot, withInset: spacing) - menuView.pin(.top, to: .bottom, of: emojiBar, withInset: spacing) - } + let frame = calculateFrame(menuHeight: menuHeight, spacing: spacing) + 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) + emojiBar.pin(.bottom, to: .top, of: snapshot, withInset: -spacing) + menuView.pin(.top, to: .bottom, of: snapshot, withInset: spacing) switch viewItem.interaction.interactionType() { case .outgoingMessage: menuView.pin(.right, to: .right, of: snapshot) @@ -171,6 +168,26 @@ final class ContextMenuVC : UIViewController { self.menuView.alpha = 1 } } + + func calculateFrame(menuHeight: CGFloat, spacing: CGFloat) -> CGRect { + var finalFrame = frame + let ratio = frame.width / frame.height + 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 { + finalFrame.size.height -= diffY + let newWidth = ratio * finalFrame.size.height + if viewItem.interaction.interactionType() == .outgoingMessage { + 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 { + finalFrame.origin.y = (UIScreen.main.bounds.height - finalFrame.size.height) / 2 + } + return finalFrame + } // MARK: Updating override func viewDidLayoutSubviews() { From 23563467b61bcdea3066e3d1a30869e0e368798f Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 29 Jun 2022 15:03:51 +1000 Subject: [PATCH 057/133] fix a crash when there is not enough 6 recently used emojis --- Session/Emoji/Storage+Emoji.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Emoji/Storage+Emoji.swift b/Session/Emoji/Storage+Emoji.swift index 69cf017b7..bd1ccf34a 100644 --- a/Session/Emoji/Storage+Emoji.swift +++ b/Session/Emoji/Storage+Emoji.swift @@ -8,7 +8,7 @@ extension Storage { let defaultEmoji = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"].filter{ !rawRecentEmoji.contains($0) } if rawRecentEmoji.count < 6 && withDefaultEmoji { - rawRecentEmoji.append(contentsOf: defaultEmoji[..<(defaultEmoji.count - rawRecentEmoji.count)]) + rawRecentEmoji.append(contentsOf: defaultEmoji[..<(defaultEmoji.count - rawRecentEmoji.count + 1)]) } return rawRecentEmoji.compactMap { EmojiWithSkinTones(rawValue: $0) } From e8518188ac0574de79758496748debf5de7dc232 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 30 Jun 2022 17:20:11 +1000 Subject: [PATCH 058/133] WIP: fix snapshot --- .../ConversationVC+Interaction.swift | 2 +- .../Message Cells/VisibleMessageCell.swift | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 98158a46e..de40e565a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -513,7 +513,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc // Show the context menu if applicable guard let index = viewItems.firstIndex(where: { $0 === viewItem }), let cell = messagesTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell, - let snapshot = cell.bubbleView.snapshotView(afterScreenUpdates: false), contextMenuWindow == nil, + let snapshot = cell.snapshot(afterScreenUpdates: false), contextMenuWindow == nil, !ContextMenuVC.actions(for: viewItem, delegate: self).isEmpty else { return } UIImpactFeedbackGenerator(style: .heavy).impactOccurred() let frame = cell.convert(cell.bubbleView.frame, to: UIApplication.shared.keyWindow!) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index cc14c4285..3a859edc0 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -1,3 +1,4 @@ +import UIKit final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { private var isHandlingLongPress: Bool = false @@ -806,3 +807,19 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { return result } } + +extension VisibleMessageCell { + public func snapshot(afterScreenUpdates afterUpdates: Bool) -> UIView? { + let labelForRendering = UILabel() + labelForRendering.numberOfLines = 0 + labelForRendering.backgroundColor = self.bubbleView.backgroundColor + if let bodyTextView = self.bodyTextView { + labelForRendering.attributedText = bodyTextView.attributedText + self.snContentView.addSubview(labelForRendering) + labelForRendering.frame = self.snContentView.convert(bodyTextView.frame, to: self.snContentView) + } + let snapshot = self.bubbleView.snapshotView(afterScreenUpdates: true) + labelForRendering.removeFromSuperview() + return snapshot + } +} From 1d9e4c88c27bd3234ed9ce1ea302ff32e49ee736 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 1 Jul 2022 16:42:55 +1000 Subject: [PATCH 059/133] fix snapshot not completed issue --- Session.xcodeproj/project.pbxproj | 4 + .../ConversationVC+Interaction.swift | 2 +- .../Conversations/Input View/InputView.swift | 4 + .../Content Views/LinkPreviewView.swift | 5 +- .../Message Cells/VisibleMessageCell.swift | 77 +++++++++------ SessionUIKit/Components/TappableLabel.swift | 99 +++++++++++++++++++ 6 files changed, 159 insertions(+), 32 deletions(-) create mode 100644 SessionUIKit/Components/TappableLabel.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 46215da68..28670dec8 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ 7BAF54D927ACD0E3003D12F8 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */; }; 7BAF54DA27ACD0E3003D12F8 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D727ACD0E3003D12F8 /* UITableView+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 */; }; @@ -1191,6 +1192,7 @@ 7BAF54D627ACD0E3003D12F8 /* String+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; 7BAF54D727ACD0E3003D12F8 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+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 = ""; }; @@ -2916,6 +2918,7 @@ B8CCF638239721E20091D419 /* TabBar.swift */, B8BB82B423947F2D00BA5194 /* TextField.swift */, C3C3CF8824D8EED300E1CCE7 /* TextView.swift */, + 7BBBDC43286EAD2D00747E59 /* TappableLabel.swift */, ); path = Components; sourceTree = ""; @@ -4533,6 +4536,7 @@ buildActionMask = 2147483647; files = ( C331FF972558FA6B00070591 /* Fonts.swift in Sources */, + 7BBBDC44286EAD2D00747E59 /* TappableLabel.swift in Sources */, C331FF9B2558FA6B00070591 /* Gradients.swift in Sources */, C331FFB82558FA8D00070591 /* DeviceUtilities.swift in Sources */, C331FFE72558FB0000070591 /* TextField.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index de40e565a..946f2324a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -513,7 +513,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc // Show the context menu if applicable guard let index = viewItems.firstIndex(where: { $0 === viewItem }), let cell = messagesTableView.cellForRow(at: IndexPath(row: index, section: 0)) as? VisibleMessageCell, - let snapshot = cell.snapshot(afterScreenUpdates: false), contextMenuWindow == nil, + let snapshot = cell.bubbleView.snapshotView(afterScreenUpdates: false), contextMenuWindow == nil, !ContextMenuVC.actions(for: viewItem, delegate: self).isEmpty else { return } UIImpactFeedbackGenerator(style: .heavy).impactOccurred() let frame = cell.convert(cell.bubbleView.frame, to: UIApplication.shared.keyWindow!) diff --git a/Session/Conversations/Input View/InputView.swift b/Session/Conversations/Input View/InputView.swift index cb7d4779b..dc3ba7d42 100644 --- a/Session/Conversations/Input View/InputView.swift +++ b/Session/Conversations/Input View/InputView.swift @@ -398,6 +398,10 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate, func handleMentionSelected(_ mention: Mention, from view: MentionSelectionView) { delegate?.handleMentionSelected(mention, from: view) } + + func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange) { + // Do nothing + } // MARK: Convenience private func container(for button: InputViewButton) -> UIView { diff --git a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift index 086adece9..8d76a588d 100644 --- a/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift +++ b/Session/Conversations/Message Cells/Content Views/LinkPreviewView.swift @@ -1,4 +1,5 @@ import NVActivityIndicatorView +import SessionUIKit final class LinkPreviewView : UIView { private let viewItem: ConversationViewItem? @@ -59,7 +60,7 @@ final class LinkPreviewView : UIView { return result }() - var bodyTextView: UITextView? + var bodyTextView: TappableLabel? // MARK: Settings private static let loaderSize: CGFloat = 24 @@ -163,7 +164,7 @@ final class LinkPreviewView : UIView { } // MARK: Delegate -protocol LinkPreviewViewDelegate : UITextViewDelegate & BodyTextViewDelegate { +protocol LinkPreviewViewDelegate : TappableLabelDelegate { var lastSearchedText: String? { get } func handleLinkPreviewCanceled() diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 3a859edc0..e50b8aeda 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -1,11 +1,12 @@ import UIKit +import SessionUIKit final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { private var isHandlingLongPress: Bool = false private var unloadContent: (() -> Void)? private var previousX: CGFloat = 0 var albumView: MediaAlbumView? - var bodyTextView: UITextView? + var bodyTextView: TappableLabel? // Constraints private lazy var headerViewTopConstraint = headerView.pin(.top, to: .top, of: self, withInset: 1) private lazy var authorLabelHeightConstraint = authorLabel.set(.height, to: 0) @@ -631,9 +632,10 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { } } - func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { - delegate?.openURL(URL) - return false + func tapableLabel(_ label: TappableLabel, didTapUrl url: String, atRange range: NSRange) { + if let URL = URL(string: url) { + delegate?.openURL(URL) + } } private func resetReply() { @@ -776,50 +778,67 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { return isGroupThread && viewItem.shouldShowSenderProfilePicture && senderSessionID != nil } - static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, delegate: UITextViewDelegate & BodyTextViewDelegate) -> UITextView { + static func getBodyTextView(for viewItem: ConversationViewItem, with availableWidth: CGFloat, textColor: UIColor, delegate: TappableLabelDelegate) -> TappableLabel { // Take care of: // โ€ข Highlighting mentions // โ€ข Linkification // โ€ข Highlighting search results + + func detectLinks(body: String?) -> [String: NSRange] { + var links: [String: NSRange] = [:] + guard let body = body else { return links } + let detector: NSDataDetector + do { + detector = try NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + } catch { + return [:] + } + 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 + } + guard let message = viewItem.interaction as? TSMessage else { preconditionFailure() } let isOutgoing = (message.interactionType() == .outgoingMessage) - let result = BodyTextView(snDelegate: delegate) - result.isEditable = false let attributes: [NSAttributedString.Key:Any] = [ .foregroundColor : textColor, .font : UIFont.systemFont(ofSize: getFontSize(for: viewItem)) ] let attributedText = NSMutableAttributedString(attributedString: MentionUtilities.highlightMentions(in: message.body ?? "", isOutgoingMessage: isOutgoing, threadID: viewItem.interaction.uniqueThreadId, attributes: attributes)) + let links = detectLinks(body: message.body) + for (urlString, range) in links { + let linkCustomAttributes: [NSAttributedString.Key : Any] = [ + .font: UIFont.systemFont(ofSize: getFontSize(for: viewItem)), + .foregroundColor: textColor, + .underlineColor: textColor, + .underlineStyle: NSUnderlineStyle.single.rawValue, + .attachment: URL(string: urlString)!] + attributedText.addAttributes(linkCustomAttributes, range: range) + } + + let result = TappableLabel() 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) result.set(.height, to: size.height) return result } } - -extension VisibleMessageCell { - public func snapshot(afterScreenUpdates afterUpdates: Bool) -> UIView? { - let labelForRendering = UILabel() - labelForRendering.numberOfLines = 0 - labelForRendering.backgroundColor = self.bubbleView.backgroundColor - if let bodyTextView = self.bodyTextView { - labelForRendering.attributedText = bodyTextView.attributedText - self.snContentView.addSubview(labelForRendering) - labelForRendering.frame = self.snContentView.convert(bodyTextView.frame, to: self.snContentView) - } - let snapshot = self.bubbleView.snapshotView(afterScreenUpdates: true) - labelForRendering.removeFromSuperview() - return snapshot - } -} diff --git a/SessionUIKit/Components/TappableLabel.swift b/SessionUIKit/Components/TappableLabel.swift new file mode 100644 index 000000000..a37eea2d9 --- /dev/null +++ b/SessionUIKit/Components/TappableLabel.swift @@ -0,0 +1,99 @@ +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(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 { + if let attributedText = attributedText { + textStorage = NSTextStorage(attributedString: attributedText) + findLinksAndRange(attributeString: attributedText) + } else { + textStorage = NSTextStorage() + links = [:] + } + } + } + + public override var lineBreakMode: NSLineBreakMode { + didSet { + textContainer.lineBreakMode = lineBreakMode + } + } + + public override var numberOfLines: Int { + didSet { + textContainer.maximumNumberOfLines = numberOfLines + } + } + + 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 + } + + public override func layoutSubviews() { + super.layoutSubviews() + textContainer.size = bounds.size + } + + 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 + } + } +} From ec5504c456d04ce7a31ea97454c445c7789cc678 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 27 Jul 2022 16:23:33 +1000 Subject: [PATCH 060/133] update pods --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index 70045a1da..bab1ae426 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -232,4 +232,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 6ab902a81a379cc2c0a9a92c334c78d413190338 -COCOAPODS: 1.11.3 +COCOAPODS: 1.11.2 From 82f058eae84b66927ac51dc7dfd304f8a8c0a1c1 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 27 Jul 2022 16:39:47 +1000 Subject: [PATCH 061/133] fix snapshot --- Session/Conversations/Context Menu/ContextMenuVC.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index 2bdb77a7b..b7375d13f 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -174,8 +174,8 @@ final class ContextMenuVC: UIViewController { 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: frame.width) - snapshot.set(.height, to: frame.height) + 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) From cc2bf11950fc0d1c08ae8795212bcd4313d2e648 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 28 Jul 2022 13:06:29 +1000 Subject: [PATCH 062/133] feat: remove all reaction per emoji for open group moderation. --- .../ConversationVC+Interaction.swift | 29 ++++++++++++------- .../Open Groups/OpenGroupAPI.swift | 23 +++++++++++++++ .../Open Groups/Types/SOGSEndpoint.swift | 13 +++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 977773ab1..422a11bf3 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1009,20 +1009,27 @@ extension ConversationVC: Storage.shared .read { db -> Promise in - guard let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: cellViewModel.threadId) else { + 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) } - // TODO: Need to add this API () - return Promise.value(()) - // OpenGroupAPI - // .reactionDelete( - // db, - // emoji: emoji, - // in: openGroup.roomToken, - // on: openGroup.server - // ) - // .map { _ in () } + return OpenGroupAPI + .reactionDelete( + db, + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server + ) + .map { _ in () } } .done { _ in Storage.shared.writeAsync { db in diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 65190d397..f6f64a230 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -591,6 +591,29 @@ public enum OpenGroupAPI { ) } + // MARK: - Reactions + + public static func reactionDelete( + _ db: Database, + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + using dependencies: SMKDependencies = SMKDependencies() + ) -> Promise { + return OpenGroupAPI + .send( + db, + request: Request( + method: .delete, + server: server, + endpoint: .reactionDelete(roomToken, id: id, emoji: emoji) + ), + using: dependencies + ) + .map { responseInfo, _ in responseInfo } + } + // MARK: - Pinning /// Adds a pinned message to this room diff --git a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 052b2fe80..c0f70cf30 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -26,6 +26,11 @@ 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) + // Pinning case roomPinMessage(String, id: Int64) @@ -86,6 +91,14 @@ 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)" // Pinning From f7a6d0dbb0d1b1236d9bd80fd7a1cc964fb8cf78 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 28 Jul 2022 16:32:51 +1000 Subject: [PATCH 063/133] feat: add & delete emoji reacts in open groups --- .../ConversationVC+Interaction.swift | 81 +++++++++++++------ .../Open Groups/OpenGroupAPI.swift | 42 ++++++++++ 2 files changed, 97 insertions(+), 26 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 422a11bf3..4584c92aa 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1022,7 +1022,7 @@ extension ConversationVC: } return OpenGroupAPI - .reactionDelete( + .reactionDeleteAll( db, emoji: emoji, id: openGroupServerMessageId, @@ -1077,7 +1077,6 @@ extension ConversationVC: .filter(id: thread.id) .updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) - // TODO: Need to handle open group specific logic // Update the database if remove { _ = try Reaction @@ -1100,30 +1099,60 @@ extension ConversationVC: Emoji.addRecent(db, emoji: emoji) } - // Send the actual message - try MessageSender.send( - db, - message: VisibleMessage( - sentTimestamp: UInt64(sentTimestamp), - // TODO: Is the 'groupPublicKey' needed here? - groupPublicKey: (thread.variant == .closedGroup ? thread.id : nil), - 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 - ) + if let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: cellViewModel.threadId) { + // 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 { + _ = OpenGroupAPI + .reactionDelete( + db, + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server + ) + } else { + _ = OpenGroupAPI + .reactionAdd( + db, + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server + ) + } + + } 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 + ) + } }, completion: { [weak self] _, _ in self?.handleMessageSent() diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index f6f64a230..128c2f5eb 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -593,6 +593,27 @@ public enum OpenGroupAPI { // MARK: - Reactions + public static func reactionAdd( + _ db: Database, + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + using dependencies: SMKDependencies = SMKDependencies() + ) -> Promise { + return OpenGroupAPI + .send( + db, + request: Request( + method: .put, + server: server, + endpoint: .reaction(roomToken, id: id, emoji: emoji) + ), + using: dependencies + ) + .map { responseInfo, _ in responseInfo } + } + public static func reactionDelete( _ db: Database, emoji: String, @@ -600,6 +621,27 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: SMKDependencies = SMKDependencies() + ) -> Promise { + return OpenGroupAPI + .send( + db, + request: Request( + method: .delete, + server: server, + endpoint: .reaction(roomToken, id: id, emoji: emoji) + ), + using: dependencies + ) + .map { responseInfo, _ in responseInfo } + } + + public static func reactionDeleteAll( + _ db: Database, + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + using dependencies: SMKDependencies = SMKDependencies() ) -> Promise { return OpenGroupAPI .send( From a66948812edea12f838acfd8f45ab1712fe93d4d Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 1 Aug 2022 15:45:59 +1000 Subject: [PATCH 064/133] feat: open group emoji reacts endpoints work --- .../ConversationVC+Interaction.swift | 16 ++++++- .../Open Groups/OpenGroupAPI.swift | 48 +++++++++++++++++-- .../Open Groups/Types/OpenGroupAPIError.swift | 2 + .../Open Groups/Types/SOGSEndpoint.swift | 4 ++ SessionSnodeKit/OnionRequestAPI.swift | 2 +- 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 4584c92aa..b2fda1a60 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1110,7 +1110,7 @@ extension ConversationVC: else { return } if remove { - _ = OpenGroupAPI + OpenGroupAPI .reactionDelete( db, emoji: emoji, @@ -1118,8 +1118,19 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) + .retainUntilComplete() } else { - _ = OpenGroupAPI + OpenGroupAPI + .reactors( + db, + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server + ) + .retainUntilComplete() + + OpenGroupAPI .reactionAdd( db, emoji: emoji, @@ -1127,6 +1138,7 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) + .retainUntilComplete() } } else { diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 391d8fadb..a80d5f3d7 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -659,6 +659,34 @@ 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 { + guard let encodedEmoji: String = "๐Ÿ‘".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 + print("Ryan Test: \(responseInfo)") + return responseInfo + } + } + public static func reactionAdd( _ db: Database, emoji: String, @@ -667,13 +695,17 @@ public enum OpenGroupAPI { on server: String, using dependencies: SMKDependencies = SMKDependencies() ) -> Promise { + 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: emoji) + endpoint: .reaction(roomToken, id: id, emoji: encodedEmoji) ), using: dependencies ) @@ -688,13 +720,17 @@ public enum OpenGroupAPI { on server: String, using dependencies: SMKDependencies = SMKDependencies() ) -> Promise { + 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: emoji) + endpoint: .reaction(roomToken, id: id, emoji: encodedEmoji) ), using: dependencies ) @@ -709,13 +745,17 @@ public enum OpenGroupAPI { on server: String, using dependencies: SMKDependencies = SMKDependencies() ) -> Promise { + 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: emoji) + endpoint: .reactionDelete(roomToken, id: id, emoji: encodedEmoji) ), using: dependencies ) @@ -1286,7 +1326,7 @@ public enum OpenGroupAPI { guard let url: URL = request.url else { return nil } var updatedRequest: URLRequest = request - let path: String = url.path + let path: String = url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path .appending(url.query.map { value in "?\(value)" }) let method: String = (request.httpMethod ?? "GET") let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970)) 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 c0f70cf30..60c148595 100644 --- a/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -30,6 +30,7 @@ extension OpenGroupAPI { case reactionDelete(String, id: Int64, emoji: String) case reaction(String, id: Int64, emoji: String) + case reactors(String, id: Int64, emoji: String) // Pinning @@ -99,6 +100,9 @@ extension OpenGroupAPI { 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/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 3efd5d4ba..084081df9 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.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path) .appending(url.query.map { value in "?\(value)" }) let requestInfo: RequestInfo = RequestInfo( From 28f4aad691d13a5e38696b92d8cd13d02df4c113 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 1 Aug 2022 16:07:51 +1000 Subject: [PATCH 065/133] clean --- SessionMessagingKit/Common Networking/QueryParam.swift | 2 ++ SessionMessagingKit/Open Groups/OpenGroupAPI.swift | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 7a1fe0f18..78b2c78f1 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -9,4 +9,6 @@ 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 reactions = "t" } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index a80d5f3d7..24a83b4ee 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -618,7 +618,8 @@ public enum OpenGroupAPI { db, request: Request( server: server, - endpoint: .roomMessagesSince(roomToken, seqNo: seqNo) + endpoint: .roomMessagesSince(roomToken, seqNo: seqNo), + queryParameters: [.reactions : "r"] ), using: dependencies ) @@ -667,7 +668,7 @@ public enum OpenGroupAPI { on server: String, using dependencies: SMKDependencies = SMKDependencies() ) -> Promise { - guard let encodedEmoji: String = "๐Ÿ‘".addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { + guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { return Promise(error: OpenGroupAPIError.invalidEmoji) } @@ -681,10 +682,7 @@ public enum OpenGroupAPI { ), using: dependencies ) - .map { responseInfo, _ in - print("Ryan Test: \(responseInfo)") - return responseInfo - } + .map { responseInfo, _ in responseInfo } } public static func reactionAdd( From 85162cd8a25330fb1a6a2cb200cae0e060543a30 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 1 Aug 2022 17:14:05 +1000 Subject: [PATCH 066/133] WIP: decode open group reactions --- .../Open Groups/Models/SOGSMessage.swift | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index f668454bb..f8b64d1d4 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -17,6 +17,8 @@ extension OpenGroupAPI { case base64EncodedData = "data" case base64EncodedSignature = "signature" + + case reactions = "reactions" } public let id: Int64 @@ -30,6 +32,20 @@ extension OpenGroupAPI { public let base64EncodedData: String? public let base64EncodedSignature: String? + + public let reactions: [String:Reaction]? + } + + public struct Reaction: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case total + case reactors + case you + } + + public let total: Int64 + public let reactors: [String]? + public let you: Bool } } @@ -42,6 +58,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 { @@ -84,7 +101,20 @@ 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: [:] + ) + } +} + +extension OpenGroupAPI.Reaction { + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = OpenGroupAPI.Reaction( + total: try container.decode(Int64.self, forKey: .total), + reactors: try? container.decode([String].self, forKey: .reactors), + you: (try? container.decode(Bool.self, forKey: .you)) ?? false ) } } From a742e97d00f9ae8fcd56d212a0b79c936669c9dc Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 2 Aug 2022 13:50:31 +1000 Subject: [PATCH 067/133] feat: decode reactions when polling for open group messages --- .../Open Groups/Models/SOGSMessage.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index f8b64d1d4..1bf648d0a 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -33,19 +33,19 @@ extension OpenGroupAPI { public let base64EncodedData: String? public let base64EncodedSignature: String? - public let reactions: [String:Reaction]? - } - - public struct Reaction: Codable, Equatable { - enum CodingKeys: String, CodingKey { - case total - case reactors - case you + public struct Reaction: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case count + case reactors + case you + } + + public let count: Int64 + public let reactors: [String]? + public let you: Bool } - public let total: Int64 - public let reactors: [String]? - public let you: Bool + public let reactions: [String:Reaction]? } } @@ -102,17 +102,17 @@ extension OpenGroupAPI.Message { whisperTo: try? container.decode(String.self, forKey: .whisperTo), base64EncodedData: maybeBase64EncodedData, base64EncodedSignature: maybeBase64EncodedSignature, - reactions: [:] + reactions: maybeReactions ) } } -extension OpenGroupAPI.Reaction { +extension OpenGroupAPI.Message.Reaction { public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - - self = OpenGroupAPI.Reaction( - total: try container.decode(Int64.self, forKey: .total), + + 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 ) From 19a1edb33ae98093bafade963ed6607582325b86 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 3 Aug 2022 09:58:13 +1000 Subject: [PATCH 068/133] WIP: persist reaction locally --- SessionMessagingKit/Messages/Message.swift | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index e33da9647..02333c089 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -302,6 +302,19 @@ public extension Message { throw MessageReceiverError.invalidMessage } + if let reactions = message.reactions { + try processRawReceivedReactions( + db, + reactions: reactions, + serverExpirationTimestamp: nil, + serverHash: nil, + openGroupId: openGroupId, + openGroupMessageServerId: message.id, + openGroupServerPublicKey: openGroupServerPublicKey, + dependencies: dependencies + ) + } + return try processRawReceivedMessage( db, envelope: envelope, @@ -348,6 +361,34 @@ public extension Message { ) } + private static func processRawReceivedReactions( + _ db: Database, + reactions: [String:OpenGroupAPI.Message.Reaction], + serverExpirationTimestamp: TimeInterval?, + serverHash: String?, + openGroupId: String? = nil, + openGroupMessageServerId: Int64? = nil, + openGroupServerPublicKey: String? = nil, + dependencies: SMKDependencies = SMKDependencies() + ) throws { + guard let openGroupMessageServerId = openGroupMessageServerId else { return } + for (encodedEmoji, rawReaction) in reactions { + if let emoji = encodedEmoji.removingPercentEncoding, + rawReaction.count > 0, + let reactors = rawReaction.reactors?.joined(separator: ",") + { + try Reaction( + interactionId: openGroupMessageServerId, + serverHash: nil, + timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), + authorId: reactors, + emoji: emoji, + count: rawReaction.count + ).insert(db) + } + } + } + private static func processRawReceivedMessage( _ db: Database, envelope: SNProtoEnvelope, From cd08c792d3ba4fe5cffdd85c9e266e5950a94bc6 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 3 Aug 2022 16:08:23 +1000 Subject: [PATCH 069/133] WIP: include reactions into open group message detail info --- .../ConversationVC+Interaction.swift | 10 --- .../Jobs/Types/MessageReceiveJob.swift | 14 +++- SessionMessagingKit/Messages/Message.swift | 71 ++++++++++++------- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index b2fda1a60..49786dea3 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1120,16 +1120,6 @@ extension ConversationVC: ) .retainUntilComplete() } else { - OpenGroupAPI - .reactors( - db, - emoji: emoji, - id: openGroupServerMessageId, - in: openGroup.roomToken, - on: openGroup.server - ) - .retainUntilComplete() - OpenGroupAPI .reactionAdd( db, diff --git a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift index 6822f1fe0..91bc8c819 100644 --- a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift @@ -106,30 +106,36 @@ extension MessageReceiveJob { case message case variant case serializedProtoData + case reactions } public let message: Message public let variant: Message.Variant public let serializedProtoData: Data + public let reactions: [Reaction] public init( message: Message, variant: Message.Variant, - proto: SNProtoContent + proto: SNProtoContent, + reactions: [Reaction] ) throws { self.message = message self.variant = variant self.serializedProtoData = try proto.serializedData() + self.reactions = reactions } private init( message: Message, variant: Message.Variant, - serializedProtoData: Data + serializedProtoData: Data, + reactions: [Reaction] ) { self.message = message self.variant = variant self.serializedProtoData = serializedProtoData + self.reactions = reactions } // MARK: - Codable @@ -145,7 +151,8 @@ extension MessageReceiveJob { self = MessageInfo( message: try variant.decode(from: container, forKey: .message), variant: variant, - serializedProtoData: try container.decode(Data.self, forKey: .serializedProtoData) + serializedProtoData: try container.decode(Data.self, forKey: .serializedProtoData), + reactions: try container.decode([Reaction].self, forKey: .reactions) ) } @@ -160,6 +167,7 @@ extension MessageReceiveJob { try container.encode(message, forKey: .message) try container.encode(variant, forKey: .variant) try container.encode(serializedProtoData, forKey: .serializedProtoData) + try container.encode(reactions, forKey: .reactions) } } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 02333c089..6aeaa1788 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 { @@ -302,22 +303,21 @@ public extension Message { throw MessageReceiverError.invalidMessage } - if let reactions = message.reactions { - try processRawReceivedReactions( - db, - reactions: reactions, - serverExpirationTimestamp: nil, - serverHash: nil, - openGroupId: openGroupId, - openGroupMessageServerId: message.id, - openGroupServerPublicKey: openGroupServerPublicKey, - dependencies: dependencies - ) - } + let reactions = processRawReceivedReactions( + db, + reactions: message.reactions, + serverExpirationTimestamp: nil, + serverHash: nil, + openGroupId: openGroupId, + openGroupMessageServerId: message.id, + openGroupServerPublicKey: openGroupServerPublicKey, + dependencies: dependencies + ) return try processRawReceivedMessage( db, envelope: envelope, + reactions: reactions, serverExpirationTimestamp: nil, serverHash: nil, openGroupId: openGroupId, @@ -363,35 +363,55 @@ public extension Message { private static func processRawReceivedReactions( _ db: Database, - reactions: [String:OpenGroupAPI.Message.Reaction], + reactions: [String:OpenGroupAPI.Message.Reaction]?, serverExpirationTimestamp: TimeInterval?, serverHash: String?, openGroupId: String? = nil, openGroupMessageServerId: Int64? = nil, openGroupServerPublicKey: String? = nil, dependencies: SMKDependencies = SMKDependencies() - ) throws { - guard let openGroupMessageServerId = openGroupMessageServerId else { return } + ) -> [Reaction] { + var results: [Reaction] = [] + guard let openGroupMessageServerId = openGroupMessageServerId, let reactions = reactions else { return results } + let userPublicKey: String = getUserHexEncodedPublicKey(db) for (encodedEmoji, rawReaction) in reactions { if let emoji = encodedEmoji.removingPercentEncoding, rawReaction.count > 0, - let reactors = rawReaction.reactors?.joined(separator: ",") + let reactors = rawReaction.reactors { - try Reaction( - interactionId: openGroupMessageServerId, - serverHash: nil, - timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), - authorId: reactors, - emoji: emoji, - count: rawReaction.count - ).insert(db) + var count = rawReaction.count + for reactor in reactors { + let reaction = Reaction( + interactionId: openGroupMessageServerId, + serverHash: nil, + timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), + authorId: reactor, + emoji: emoji, + count: count + ) + count = 0 // Only insert the first reaction with the total count of this emoji + results.append(reaction) + } + if rawReaction.you && !reactors.contains(userPublicKey) { + let reaction = Reaction( + interactionId: openGroupMessageServerId, + serverHash: nil, + timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), + authorId: userPublicKey, + emoji: emoji, + count: 0 + ) + results.append(reaction) + } } } + return results } private static func processRawReceivedMessage( _ db: Database, envelope: SNProtoEnvelope, + reactions: [Reaction] = [], serverExpirationTimestamp: TimeInterval?, serverHash: String?, openGroupId: String? = nil, @@ -461,7 +481,8 @@ public extension Message { try MessageReceiveJob.Details.MessageInfo( message: message, variant: variant, - proto: proto + proto: proto, + reactions: reactions ) ) } From 30e48209e2a6b689e945e35bb8a4f63f073d861a Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 3 Aug 2022 16:40:29 +1000 Subject: [PATCH 070/133] feat: persist open group reactions into local database --- SessionMessagingKit/Database/Models/Reaction.swift | 13 +++++++++++++ .../Open Groups/OpenGroupManager.swift | 1 + .../MessageReceiver+VisibleMessages.swift | 5 +++++ .../Sending & Receiving/MessageReceiver.swift | 2 ++ 4 files changed, 21 insertions(+) diff --git a/SessionMessagingKit/Database/Models/Reaction.swift b/SessionMessagingKit/Database/Models/Reaction.swift index e045a42e8..08f4965c9 100644 --- a/SessionMessagingKit/Database/Models/Reaction.swift +++ b/SessionMessagingKit/Database/Models/Reaction.swift @@ -90,4 +90,17 @@ public extension Reaction { count: (count ?? self.count) ) } + + func with( + interactionId: Int64? + ) -> Reaction { + return Reaction( + interactionId: (interactionId ?? self.interactionId), + serverHash: self.serverHash, + timestampMs: self.timestampMs, + authorId: self.authorId, + emoji: self.emoji, + count: self.count + ) + } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index c72dea525..a97b84fdd 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -537,6 +537,7 @@ public final class OpenGroupManager: NSObject { message: messageInfo.message, associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), openGroupId: openGroup.id, + openGroupReactions: messageInfo.reactions, isBackgroundPoll: isBackgroundPoll, dependencies: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index b1055223e..f92ce527b 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -12,6 +12,7 @@ extension MessageReceiver { message: VisibleMessage, associatedWithProto proto: SNProtoContent, openGroupId: String?, + openGroupReactions: [Reaction] = [], isBackgroundPoll: Bool, dependencies: Dependencies = Dependencies() ) throws -> Int64 { @@ -140,6 +141,10 @@ extension MessageReceiver { return recipientParts[2] }() ).inserted(db) + + for reaction in openGroupReactions { + try reaction.with(interactionId: interaction.id).insert(db) + } } catch { switch error { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 4524b75d2..e97b4544d 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -180,6 +180,7 @@ public enum MessageReceiver { message: Message, associatedWithProto proto: SNProtoContent, openGroupId: String?, + openGroupReactions: [Reaction] = [], isBackgroundPoll: Bool, dependencies: SMKDependencies = SMKDependencies() ) throws { @@ -217,6 +218,7 @@ public enum MessageReceiver { message: message, associatedWithProto: proto, openGroupId: openGroupId, + openGroupReactions: openGroupReactions, isBackgroundPoll: isBackgroundPoll ) From 6e0c812c6db47fab284e2c449e4d04b435b1964a Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 4 Aug 2022 11:03:58 +1000 Subject: [PATCH 071/133] fix a crash where reactors in open group doesn't have a profile --- SessionMessagingKit/Shared Models/MessageViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 9e11f9a05..50e867485 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -452,7 +452,7 @@ public extension MessageViewModel { public let rowId: Int64 public let reaction: Reaction - public let profile: Profile + public let profile: Profile? // MARK: - Identifiable From 4d0b47fa7fd809673eb15cd12dab7219da8c2116 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 4 Aug 2022 11:32:35 +1000 Subject: [PATCH 072/133] clean --- .../Database/Models/Reaction.swift | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Reaction.swift b/SessionMessagingKit/Database/Models/Reaction.swift index 08f4965c9..f02d0d8af 100644 --- a/SessionMessagingKit/Database/Models/Reaction.swift +++ b/SessionMessagingKit/Database/Models/Reaction.swift @@ -78,29 +78,18 @@ public struct Reaction: Codable, Equatable, Hashable, FetchableRecord, Persistab public extension Reaction { func with( + interactionId: Int64? = nil, serverHash: String? = nil, + authorId: String? = nil, count: Int64? = nil ) -> Reaction { return Reaction( - interactionId: interactionId, + interactionId: (interactionId ?? self.interactionId), serverHash: (serverHash ?? self.serverHash), timestampMs: self.timestampMs, - authorId: self.authorId, + authorId: (authorId ?? self.authorId), emoji: self.emoji, count: (count ?? self.count) ) } - - func with( - interactionId: Int64? - ) -> Reaction { - return Reaction( - interactionId: (interactionId ?? self.interactionId), - serverHash: self.serverHash, - timestampMs: self.timestampMs, - authorId: self.authorId, - emoji: self.emoji, - count: self.count - ) - } } From 37f876dffd32518ddd898f38441e34c59b992db9 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 4 Aug 2022 13:43:47 +1000 Subject: [PATCH 073/133] deal with blinded id reactor --- SessionMessagingKit/Common Networking/QueryParam.swift | 1 + SessionMessagingKit/Messages/Message.swift | 8 +++++++- SessionMessagingKit/Open Groups/OpenGroupAPI.swift | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 78b2c78f1..184c6d4da 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -11,4 +11,5 @@ enum QueryParam: String { case platform // For file server session version check case reactions = "t" + case reactors = "reactors" } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 6aeaa1788..dcede8ef5 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -366,7 +366,7 @@ public extension Message { reactions: [String:OpenGroupAPI.Message.Reaction]?, serverExpirationTimestamp: TimeInterval?, serverHash: String?, - openGroupId: String? = nil, + openGroupId: String, openGroupMessageServerId: Int64? = nil, openGroupServerPublicKey: String? = nil, dependencies: SMKDependencies = SMKDependencies() @@ -374,6 +374,11 @@ public extension Message { var results: [Reaction] = [] guard let openGroupMessageServerId = openGroupMessageServerId, let reactions = 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 emoji = encodedEmoji.removingPercentEncoding, rawReaction.count > 0, @@ -381,6 +386,7 @@ public extension Message { { var count = rawReaction.count for reactor in reactors { + if reactor == blindedUserPublicKey { continue } // Will add a reaction for this case outside of the loop let reaction = Reaction( interactionId: openGroupMessageServerId, serverHash: nil, diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 24a83b4ee..6191b4ded 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -96,7 +96,9 @@ public enum OpenGroupAPI { endpoint: (shouldRetrieveRecentMessages ? .roomMessagesRecent(openGroup.roomToken) : .roomMessagesSince(openGroup.roomToken, seqNo: openGroup.sequenceNumber) - ) + ), + queryParameters: [.reactions: "r", + .reactors: "20"] ), responseType: [Failable].self ) From a2c9bee269e70da0d826060ab3c292697a890ee8 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 4 Aug 2022 17:10:24 +1000 Subject: [PATCH 074/133] WIP: add sortId --- .../ConversationVC+Interaction.swift | 8 +- .../Migrations/_005_EmojiReacts.swift | 3 + .../Database/Models/Reaction.swift | 47 ++++++++++- .../Jobs/Types/MessageReceiveJob.swift | 14 +--- SessionMessagingKit/Messages/Message.swift | 37 +++----- .../Open Groups/Models/SOGSMessage.swift | 2 +- .../Open Groups/OpenGroupManager.swift | 84 ++++++++++++------- .../MessageReceiver+VisibleMessages.swift | 14 ++-- .../Sending & Receiving/MessageReceiver.swift | 25 +++++- 9 files changed, 154 insertions(+), 80 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 49786dea3..9dc0ba8ed 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1086,13 +1086,19 @@ extension ConversationVC: .deleteAll(db) } else { + let sortId = Reaction.getSortId( + db, + interactionId: cellViewModel.id, + emoji: emoji + ) try Reaction( interactionId: cellViewModel.id, serverHash: nil, timestampMs: sentTimestamp, authorId: cellViewModel.currentUserPublicKey, emoji: emoji, - count: 1 // TODO: For open groups this should be '0' + count: 1, + sortId: sortId ).insert(db) // Add it to the recent list diff --git a/SessionMessagingKit/Database/Migrations/_005_EmojiReacts.swift b/SessionMessagingKit/Database/Migrations/_005_EmojiReacts.swift index 2ec472c8b..a6d57340c 100644 --- a/SessionMessagingKit/Database/Migrations/_005_EmojiReacts.swift +++ b/SessionMessagingKit/Database/Migrations/_005_EmojiReacts.swift @@ -29,6 +29,9 @@ enum _005_EmojiReacts: Migration { 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]) diff --git a/SessionMessagingKit/Database/Models/Reaction.swift b/SessionMessagingKit/Database/Models/Reaction.swift index f02d0d8af..a5266e65e 100644 --- a/SessionMessagingKit/Database/Models/Reaction.swift +++ b/SessionMessagingKit/Database/Models/Reaction.swift @@ -19,6 +19,7 @@ public struct Reaction: Codable, Equatable, Hashable, FetchableRecord, Persistab case authorId case emoji case count + case sortId } /// The id for the interaction this reaction belongs to @@ -45,6 +46,9 @@ public struct Reaction: Codable, Equatable, Hashable, FetchableRecord, Persistab /// regardless of the type of conversation) public let count: Int64 + /// The id for sorting + public let sortId: Int64 + // MARK: - Relationships public var interaction: QueryInterfaceRequest { @@ -63,7 +67,8 @@ public struct Reaction: Codable, Equatable, Hashable, FetchableRecord, Persistab timestampMs: Int64, authorId: String, emoji: String, - count: Int64 + count: Int64, + sortId: Int64 ) { self.interactionId = interactionId self.serverHash = serverHash @@ -71,6 +76,7 @@ public struct Reaction: Codable, Equatable, Hashable, FetchableRecord, Persistab self.authorId = authorId self.emoji = emoji self.count = count + self.sortId = sortId } } @@ -81,7 +87,8 @@ public extension Reaction { interactionId: Int64? = nil, serverHash: String? = nil, authorId: String? = nil, - count: Int64? = nil + count: Int64? = nil, + sortId: Int64? = nil ) -> Reaction { return Reaction( interactionId: (interactionId ?? self.interactionId), @@ -89,7 +96,41 @@ public extension Reaction { timestampMs: self.timestampMs, authorId: (authorId ?? self.authorId), emoji: self.emoji, - count: (count ?? self.count) + count: (count ?? self.count), + sortId: (sortId ?? self.sortId) ) } } + +// MARK: - SortId + +public extension Reaction { + static func getSortId( + _ db: Database, + interactionId: Int64, + emoji: String + ) -> Int64 { + let existingSortId: Int64? = try? Reaction + .select(Columns.sortId) + .filter(Columns.interactionId == interactionId) + .filter(Columns.emoji == emoji) + .asRequest(of: Int64.self) + .fetchOne(db) + + if let sortId = existingSortId { + return sortId + } + + let existingLargestSortId: Int64? = try? Reaction + .select(max(Columns.sortId)) + .filter(Columns.interactionId == interactionId) + .asRequest(of: Int64.self) + .fetchOne(db) + + if let sortId = existingLargestSortId { + return sortId + 1 + } + + return 0 + } +} diff --git a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift index 91bc8c819..6822f1fe0 100644 --- a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift @@ -106,36 +106,30 @@ extension MessageReceiveJob { case message case variant case serializedProtoData - case reactions } public let message: Message public let variant: Message.Variant public let serializedProtoData: Data - public let reactions: [Reaction] public init( message: Message, variant: Message.Variant, - proto: SNProtoContent, - reactions: [Reaction] + proto: SNProtoContent ) throws { self.message = message self.variant = variant self.serializedProtoData = try proto.serializedData() - self.reactions = reactions } private init( message: Message, variant: Message.Variant, - serializedProtoData: Data, - reactions: [Reaction] + serializedProtoData: Data ) { self.message = message self.variant = variant self.serializedProtoData = serializedProtoData - self.reactions = reactions } // MARK: - Codable @@ -151,8 +145,7 @@ extension MessageReceiveJob { self = MessageInfo( message: try variant.decode(from: container, forKey: .message), variant: variant, - serializedProtoData: try container.decode(Data.self, forKey: .serializedProtoData), - reactions: try container.decode([Reaction].self, forKey: .reactions) + serializedProtoData: try container.decode(Data.self, forKey: .serializedProtoData) ) } @@ -167,7 +160,6 @@ extension MessageReceiveJob { try container.encode(message, forKey: .message) try container.encode(variant, forKey: .variant) try container.encode(serializedProtoData, forKey: .serializedProtoData) - try container.encode(reactions, forKey: .reactions) } } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index dcede8ef5..f563fe932 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -303,21 +303,9 @@ public extension Message { throw MessageReceiverError.invalidMessage } - let reactions = processRawReceivedReactions( - db, - reactions: message.reactions, - serverExpirationTimestamp: nil, - serverHash: nil, - openGroupId: openGroupId, - openGroupMessageServerId: message.id, - openGroupServerPublicKey: openGroupServerPublicKey, - dependencies: dependencies - ) - return try processRawReceivedMessage( db, envelope: envelope, - reactions: reactions, serverExpirationTimestamp: nil, serverHash: nil, openGroupId: openGroupId, @@ -361,18 +349,14 @@ public extension Message { ) } - private static func processRawReceivedReactions( + static func processRawReceivedReactions( _ db: Database, - reactions: [String:OpenGroupAPI.Message.Reaction]?, - serverExpirationTimestamp: TimeInterval?, - serverHash: String?, openGroupId: String, - openGroupMessageServerId: Int64? = nil, - openGroupServerPublicKey: String? = nil, + message: OpenGroupAPI.Message, dependencies: SMKDependencies = SMKDependencies() ) -> [Reaction] { var results: [Reaction] = [] - guard let openGroupMessageServerId = openGroupMessageServerId, let reactions = reactions else { return results } + guard let reactions = message.reactions else { return results } let userPublicKey: String = getUserHexEncodedPublicKey(db) let blindedUserPublicKey: String? = SessionThread .getUserHexEncodedBlindedKey( @@ -385,27 +369,30 @@ public extension Message { let reactors = rawReaction.reactors { var count = rawReaction.count + let sortId: Int64 = 0 // TODO: Need to be modified to the server returned value for reactor in reactors { if reactor == blindedUserPublicKey { continue } // Will add a reaction for this case outside of the loop let reaction = Reaction( - interactionId: openGroupMessageServerId, + interactionId: message.id, serverHash: nil, timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), authorId: reactor, emoji: emoji, - count: count + count: count, + sortId: sortId ) count = 0 // Only insert the first reaction with the total count of this emoji results.append(reaction) } if rawReaction.you && !reactors.contains(userPublicKey) { let reaction = Reaction( - interactionId: openGroupMessageServerId, + interactionId: message.id, serverHash: nil, timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), authorId: userPublicKey, emoji: emoji, - count: 0 + count: count, + sortId: sortId ) results.append(reaction) } @@ -417,7 +404,6 @@ public extension Message { private static func processRawReceivedMessage( _ db: Database, envelope: SNProtoEnvelope, - reactions: [Reaction] = [], serverExpirationTimestamp: TimeInterval?, serverHash: String?, openGroupId: String? = nil, @@ -487,8 +473,7 @@ public extension Message { try MessageReceiveJob.Details.MessageInfo( message: message, variant: variant, - proto: proto, - reactions: reactions + proto: proto ) ) } diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 1bf648d0a..b9ff94939 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -94,7 +94,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)) ?? Date().timeIntervalSince1970), // Reaction updates don't include posted edited: try? container.decode(TimeInterval.self, forKey: .edited), seqNo: try container.decode(Int64.self, forKey: .seqNo), whisper: ((try? container.decode(Bool.self, forKey: .whisper)) ?? false), diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index a97b84fdd..c1b228a24 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -512,48 +512,72 @@ public final class OpenGroupManager: NSObject { // Process the messages sortedMessages.forEach { message in - guard - let base64EncodedString: String = message.base64EncodedData, - let data = Data(base64Encoded: base64EncodedString) - else { + if message.base64EncodedData == nil && message.reactions == nil { // A message with no data has been deleted so add it to the list to remove messageServerIdsToRemove.append(UInt64(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, - openGroupReactions: messageInfo.reactions, - 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, + isBackgroundPoll: isBackgroundPoll, + 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, + dependencies: dependencies + ) - default: SNLog("Couldn't receive open group message due to error: \(error).") + if !reactions.isEmpty { + try MessageReceiver.handleOpenGroupReactions( + db, + openGroupMessageServerId: message.id, + openGroupReactions: reactions + ) + } + } + catch { + SNLog("Couldn't handle open group reactions due to error: \(error).") } } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index f92ce527b..b028be09d 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?, - openGroupReactions: [Reaction] = [], isBackgroundPoll: Bool, dependencies: Dependencies = Dependencies() ) throws -> Int64 { @@ -141,10 +140,6 @@ extension MessageReceiver { return recipientParts[2] }() ).inserted(db) - - for reaction in openGroupReactions { - try reaction.with(interactionId: interaction.id).insert(db) - } } catch { switch error { @@ -329,6 +324,12 @@ extension MessageReceiver { throw StorageError.objectNotFound } + let sortId = Reaction.getSortId( + db, + interactionId: interactionId, + emoji: reaction.emoji + ) + switch reaction.kind { case .react: try Reaction( @@ -337,7 +338,8 @@ extension MessageReceiver { timestampMs: Int64(messageSentTimestamp * 1000), authorId: sender, emoji: reaction.emoji, - count: 1 // TODO: Handle Open Group case + count: 1, + sortId: sortId ).insert(db) case .remove: diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index e97b4544d..21df40614 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?, - openGroupReactions: [Reaction] = [], isBackgroundPoll: Bool, dependencies: SMKDependencies = SMKDependencies() ) throws { @@ -218,7 +217,6 @@ public enum MessageReceiver { message: message, associatedWithProto: proto, openGroupId: openGroupId, - openGroupReactions: openGroupReactions, isBackgroundPoll: isBackgroundPoll ) @@ -251,6 +249,29 @@ public enum MessageReceiver { } } + public static func handleOpenGroupReactions( + _ db: Database, + openGroupMessageServerId: Int64, + openGroupReactions: [Reaction] + ) throws { + guard let interactionId: Int64 = try? Interaction + .select(.id) + .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)? { From 073516ae78c2bdaf8702e8f059885e268757ac6a Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 5 Aug 2022 14:23:28 +1000 Subject: [PATCH 075/133] feat: add sortId for reaction sorting --- .../ConversationVC+Interaction.swift | 3 ++- .../Database/Models/Reaction.swift | 24 ++++++------------- SessionMessagingKit/Messages/Message.swift | 2 +- .../Open Groups/Models/SOGSMessage.swift | 5 +++- .../MessageReceiver+VisibleMessages.swift | 3 ++- .../Shared Models/MessageViewModel.swift | 2 +- 6 files changed, 17 insertions(+), 22 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 9dc0ba8ed..75a9b2f8c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1089,7 +1089,8 @@ extension ConversationVC: let sortId = Reaction.getSortId( db, interactionId: cellViewModel.id, - emoji: emoji + emoji: emoji, + timestamp: sentTimestamp ) try Reaction( interactionId: cellViewModel.id, diff --git a/SessionMessagingKit/Database/Models/Reaction.swift b/SessionMessagingKit/Database/Models/Reaction.swift index a5266e65e..b1e737e96 100644 --- a/SessionMessagingKit/Database/Models/Reaction.swift +++ b/SessionMessagingKit/Database/Models/Reaction.swift @@ -46,7 +46,7 @@ public struct Reaction: Codable, Equatable, Hashable, FetchableRecord, Persistab /// regardless of the type of conversation) public let count: Int64 - /// The id for sorting + /// The first timestamp that an emoji is added public let sortId: Int64 // MARK: - Relationships @@ -108,29 +108,19 @@ public extension Reaction { static func getSortId( _ db: Database, interactionId: Int64, - emoji: String + emoji: String, + timestamp: Int64 ) -> Int64 { - let existingSortId: Int64? = try? Reaction + if let existingSortId: Int64 = try? Reaction .select(Columns.sortId) .filter(Columns.interactionId == interactionId) .filter(Columns.emoji == emoji) .asRequest(of: Int64.self) .fetchOne(db) - - if let sortId = existingSortId { - return sortId + { + return existingSortId } - let existingLargestSortId: Int64? = try? Reaction - .select(max(Columns.sortId)) - .filter(Columns.interactionId == interactionId) - .asRequest(of: Int64.self) - .fetchOne(db) - - if let sortId = existingLargestSortId { - return sortId + 1 - } - - return 0 + return timestamp } } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index f563fe932..6022a4299 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -369,7 +369,7 @@ public extension Message { let reactors = rawReaction.reactors { var count = rawReaction.count - let sortId: Int64 = 0 // TODO: Need to be modified to the server returned value + let sortId: Int64 = Int64(floor((rawReaction.first * 1000))) for reactor in reactors { if reactor == blindedUserPublicKey { continue } // Will add a reaction for this case outside of the loop let reaction = Reaction( diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index b9ff94939..2785c51e9 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -38,11 +38,13 @@ extension OpenGroupAPI { case count case reactors case you + case first } public let count: Int64 public let reactors: [String]? public let you: Bool + public let first: TimeInterval } public let reactions: [String:Reaction]? @@ -114,7 +116,8 @@ extension OpenGroupAPI.Message.Reaction { 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 + you: (try? container.decode(Bool.self, forKey: .you)) ?? false, + first: ((try? container.decode(TimeInterval.self, forKey: .first)) ?? Date().timeIntervalSince1970) ) } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index b028be09d..d4ab1f9e5 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -327,7 +327,8 @@ extension MessageReceiver { let sortId = Reaction.getSortId( db, interactionId: interactionId, - emoji: reaction.emoji + emoji: reaction.emoji, + timestamp: Int64(messageSentTimestamp * 1000) ) switch reaction.kind { diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 50e867485..37e4751ab 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -465,7 +465,7 @@ public extension MessageViewModel { // MARK: - Comparable public static func < (lhs: ReactionInfo, rhs: ReactionInfo) -> Bool { - return (lhs.reaction.timestampMs < rhs.reaction.timestampMs) + return (lhs.reaction.sortId < rhs.reaction.sortId) } } } From 07a1d10212178b3758e55677fafdeccbd4c37415 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 5 Aug 2022 16:16:39 +1000 Subject: [PATCH 076/133] fix an issue where reaction updates triggers scrolling --- Session/Conversations/ConversationVC+Interaction.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 75a9b2f8c..3e38c2a54 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1069,9 +1069,6 @@ extension ConversationVC: return } - // Let the viewModel know we are about to send a message - self?.viewModel.sentMessageBeforeUpdate = true - // Update the thread to be visible _ = try SessionThread .filter(id: thread.id) From 7383ddad04ed9d2b7ccf482a208811c99dc17f33 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 8 Aug 2022 13:07:29 +1000 Subject: [PATCH 077/133] update sortId logic after open group reaction index --- .../ConversationVC+Interaction.swift | 3 +- .../Database/Models/Reaction.swift | 14 ++++++-- SessionMessagingKit/Messages/Message.swift | 5 ++- .../Open Groups/Models/SOGSMessage.swift | 10 +++--- .../Open Groups/OpenGroupManager.swift | 36 ++++++++----------- .../MessageReceiver+VisibleMessages.swift | 3 +- 6 files changed, 35 insertions(+), 36 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 487b3cef6..12852c69c 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1088,8 +1088,7 @@ extension ConversationVC: let sortId = Reaction.getSortId( db, interactionId: cellViewModel.id, - emoji: emoji, - timestamp: sentTimestamp + emoji: emoji ) try Reaction( interactionId: cellViewModel.id, diff --git a/SessionMessagingKit/Database/Models/Reaction.swift b/SessionMessagingKit/Database/Models/Reaction.swift index b1e737e96..df10caad4 100644 --- a/SessionMessagingKit/Database/Models/Reaction.swift +++ b/SessionMessagingKit/Database/Models/Reaction.swift @@ -108,8 +108,7 @@ public extension Reaction { static func getSortId( _ db: Database, interactionId: Int64, - emoji: String, - timestamp: Int64 + emoji: String ) -> Int64 { if let existingSortId: Int64 = try? Reaction .select(Columns.sortId) @@ -121,6 +120,15 @@ public extension Reaction { return existingSortId } - return timestamp + 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/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 6022a4299..117670484 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -356,20 +356,19 @@ public extension Message { 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 { + for (encodedEmoji, rawReaction) in message.reactions { if let emoji = encodedEmoji.removingPercentEncoding, rawReaction.count > 0, let reactors = rawReaction.reactors { var count = rawReaction.count - let sortId: Int64 = Int64(floor((rawReaction.first * 1000))) + let sortId: Int64 = rawReaction.index for reactor in reactors { if reactor == blindedUserPublicKey { continue } // Will add a reaction for this case outside of the loop let reaction = Reaction( diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index c4e543221..9bb7bb7ec 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -40,16 +40,16 @@ extension OpenGroupAPI { case count case reactors case you - case first + case index } public let count: Int64 public let reactors: [String]? public let you: Bool - public let first: TimeInterval + public let index: Int64 } - public let reactions: [String:Reaction]? + public let reactions: [String:Reaction] } } @@ -107,7 +107,7 @@ extension OpenGroupAPI.Message { whisperTo: try? container.decode(String.self, forKey: .whisperTo), base64EncodedData: maybeBase64EncodedData, base64EncodedSignature: maybeBase64EncodedSignature, - reactions: maybeReactions + reactions: maybeReactions ?? [:] ) } } @@ -120,7 +120,7 @@ extension 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, - first: ((try? container.decode(TimeInterval.self, forKey: .first)) ?? Date().timeIntervalSince1970) + index: (try container.decode(Int64.self, forKey: .index)) ) } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index de6085058..a40b58faf 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -515,10 +515,6 @@ public final class OpenGroupManager: NSObject { // Process the messages sortedMessages.forEach { message in - if message.base64EncodedData == nil && message.reactions == nil { - return - } - // Handle messages if let base64EncodedString: String = message.base64EncodedData, let data = Data(base64Encoded: base64EncodedString) @@ -560,27 +556,25 @@ public final class OpenGroupManager: NSObject { } // Handle reactions - if message.reactions != nil { - do { - let reactions: [Reaction] = Message.processRawReceivedReactions( + do { + let reactions: [Reaction] = Message.processRawReceivedReactions( + db, + openGroupId: openGroup.id, + message: message, + dependencies: dependencies + ) + + if !reactions.isEmpty { + try MessageReceiver.handleOpenGroupReactions( db, - openGroupId: openGroup.id, - message: message, - dependencies: dependencies + openGroupMessageServerId: message.id, + openGroupReactions: reactions ) - - if !reactions.isEmpty { - try MessageReceiver.handleOpenGroupReactions( - db, - openGroupMessageServerId: message.id, - openGroupReactions: reactions - ) - } - } - catch { - SNLog("Couldn't handle open group reactions due to error: \(error).") } } + catch { + SNLog("Couldn't handle open group reactions due to error: \(error).") + } } // Handle any deletions that are needed diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index d4ab1f9e5..b028be09d 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -327,8 +327,7 @@ extension MessageReceiver { let sortId = Reaction.getSortId( db, interactionId: interactionId, - emoji: reaction.emoji, - timestamp: Int64(messageSentTimestamp * 1000) + emoji: reaction.emoji ) switch reaction.kind { From d98221feb442d65326ed480975df5f5d4e7d9de1 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 8 Aug 2022 14:57:46 +1000 Subject: [PATCH 078/133] fix: minor ui issue on the reaction user list --- Session/Conversations/Views & Modals/ReactionListSheet.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index cb548c797..95ca5b7d6 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -50,8 +50,8 @@ final class ReactionListSheet: BaseVC { bottom: 0, trailing: Values.smallSpacing ) - result.minimumLineSpacing = 0 - result.minimumInteritemSpacing = 0 + result.minimumLineSpacing = Values.smallSpacing + result.minimumInteritemSpacing = Values.smallSpacing result.estimatedItemSize = UICollectionViewFlowLayout.automaticSize return result From 4cb2d81670797e8d92bbbce9359ce116528a09bd Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 11 Aug 2022 11:32:29 +1000 Subject: [PATCH 079/133] clean up --- Podfile.lock | 2 +- .../Common Networking/QueryParam.swift | 1 - SessionMessagingKit/Messages/Message.swift | 77 ++++++++++++------- .../Open Groups/Models/SOGSMessage.swift | 4 +- .../Open Groups/OpenGroupAPI.swift | 5 +- .../Open Groups/OpenGroupManager.swift | 12 ++- .../Sending & Receiving/MessageSender.swift | 5 +- 7 files changed, 62 insertions(+), 44 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 5e750d933..37f4ac9c5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -244,4 +244,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f0857369c4831b2e5c1946345e76e493f3286805 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 184c6d4da..28280b495 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -10,6 +10,5 @@ enum QueryParam: String { case limit // For messages - number between 1 and 256 (default is 100) case platform // For file server session version check - case reactions = "t" case reactors = "reactors" } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 117670484..a8267acae 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -292,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) @@ -367,34 +367,55 @@ public extension Message { rawReaction.count > 0, let reactors = rawReaction.reactors { - var count = rawReaction.count - let sortId: Int64 = rawReaction.index - for reactor in reactors { - if reactor == blindedUserPublicKey { continue } // Will add a reaction for this case outside of the loop - let reaction = Reaction( - interactionId: message.id, - serverHash: nil, - timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), - authorId: reactor, - emoji: emoji, - count: count, - sortId: sortId + let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) + let desiredReactorIds: [String] = reactors + .filter { $0 != blindedUserPublicKey } + + results = results + .appending( // Add the first reaction (with the count) + desiredReactorIds.first + .map { reactor in + Reaction( + interactionId: message.id, + serverHash: nil, + timestampMs: timestampMs, + authorId: reactor, + emoji: emoji, + count: rawReaction.count, + sortId: rawReaction.index + ) + } ) - count = 0 // Only insert the first reaction with the total count of this emoji - results.append(reaction) - } - if rawReaction.you && !reactors.contains(userPublicKey) { - let reaction = Reaction( - interactionId: message.id, - serverHash: nil, - timestampMs: Int64(floor((Date().timeIntervalSince1970 * 1000))), - authorId: userPublicKey, - emoji: emoji, - count: count, - sortId: sortId + .appending( // Add all other reactions + contentsOf: desiredReactorIds.count <= 1 ? + [] : + desiredReactorIds + .suffix(from: 1) + .map { reactor in + Reaction( + interactionId: message.id, + serverHash: nil, + timestampMs: timestampMs, + authorId: reactor, + emoji: emoji, + count: 0, // Only want this on the first reaction + sortId: rawReaction.index + ) + } + ) + .appending( // Add the current user reaction (if applicable and not already included) + !rawReaction.you || reactors.contains(userPublicKey) ? + nil : + Reaction( + interactionId: message.id, + serverHash: nil, + timestampMs: timestampMs, + authorId: userPublicKey, + emoji: emoji, + count: (desiredReactorIds.isEmpty ? rawReaction.count : 0), + sortId: rawReaction.index + ) ) - results.append(reaction) - } } } return results diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index 9bb7bb7ec..c30dfcf3a 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -24,7 +24,7 @@ extension OpenGroupAPI { 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 @@ -98,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)) ?? Date().timeIntervalSince1970), // Reaction updates don't include 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), diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 6191b4ded..518cec16c 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -97,8 +97,7 @@ public enum OpenGroupAPI { .roomMessagesRecent(openGroup.roomToken) : .roomMessagesSince(openGroup.roomToken, seqNo: openGroup.sequenceNumber) ), - queryParameters: [.reactions: "r", - .reactors: "20"] + queryParameters: [.reactors: "20"] ), responseType: [Failable].self ) @@ -621,7 +620,7 @@ public enum OpenGroupAPI { request: Request( server: server, endpoint: .roomMessagesSince(roomToken, seqNo: seqNo), - queryParameters: [.reactions : "r"] + queryParameters: [.reactors: "20"] ), using: dependencies ) diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 84f4cd7cc..7bee7652e 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -564,13 +564,11 @@ public final class OpenGroupManager: NSObject { dependencies: dependencies ) - if !reactions.isEmpty { - try MessageReceiver.handleOpenGroupReactions( - db, - openGroupMessageServerId: message.id, - openGroupReactions: reactions - ) - } + try MessageReceiver.handleOpenGroupReactions( + db, + openGroupMessageServerId: message.id, + openGroupReactions: reactions + ) } catch { SNLog("Couldn't handle open group reactions due to error: \(error).") diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 242f37372..8235b9417 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 == nil) ? nil : UInt64(floor(data.posted! * 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(()) } From 4a5674f7b35f78e6722ee01820eedda2005bc636 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 11 Aug 2022 11:51:15 +1000 Subject: [PATCH 080/133] tweak: keep the old logic of removing open group messsages --- SessionMessagingKit/Messages/Message.swift | 3 +- .../Open Groups/Models/SOGSMessage.swift | 4 +- .../Open Groups/OpenGroupManager.swift | 39 +++++++++++-------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index a8267acae..9dec50347 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -356,13 +356,14 @@ public extension Message { 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 message.reactions { + for (encodedEmoji, rawReaction) in reactions { if let emoji = encodedEmoji.removingPercentEncoding, rawReaction.count > 0, let reactors = rawReaction.reactors diff --git a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index c30dfcf3a..8ce774b31 100644 --- a/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -49,7 +49,7 @@ extension OpenGroupAPI { public let index: Int64 } - public let reactions: [String:Reaction] + public let reactions: [String:Reaction]? } } @@ -107,7 +107,7 @@ extension OpenGroupAPI.Message { whisperTo: try? container.decode(String.self, forKey: .whisperTo), base64EncodedData: maybeBase64EncodedData, base64EncodedSignature: maybeBase64EncodedSignature, - reactions: maybeReactions ?? [:] + reactions: !container.contains(.reactions) ? nil : (maybeReactions ?? [:]) ) } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 7bee7652e..a54f51ec0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -515,6 +515,11 @@ public final class OpenGroupManager: NSObject { // Process the messages sortedMessages.forEach { message in + if message.base64EncodedData == nil && message.reactions == nil { + messageServerIdsToRemove.append(Int64(message.id)) + return + } + // Handle messages if let base64EncodedString: String = message.base64EncodedData, let data = Data(base64Encoded: base64EncodedString) @@ -556,22 +561,24 @@ public final class OpenGroupManager: NSObject { } // Handle reactions - do { - let reactions: [Reaction] = Message.processRawReceivedReactions( - db, - openGroupId: openGroup.id, - message: message, - dependencies: dependencies - ) - - try MessageReceiver.handleOpenGroupReactions( - db, - openGroupMessageServerId: message.id, - openGroupReactions: reactions - ) - } - catch { - SNLog("Couldn't handle open group reactions due to error: \(error).") + if message.reactions != nil { + do { + let reactions: [Reaction] = Message.processRawReceivedReactions( + db, + openGroupId: openGroup.id, + message: message, + dependencies: dependencies + ) + + try MessageReceiver.handleOpenGroupReactions( + db, + openGroupMessageServerId: message.id, + openGroupReactions: reactions + ) + } + catch { + SNLog("Couldn't handle open group reactions due to error: \(error).") + } } } From 645f8e1d2472e81545d1e745377ba82880371d3e Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 11 Aug 2022 14:50:31 +1000 Subject: [PATCH 081/133] tweak: add update types of reaction to query parameter --- Session.xcodeproj/project.pbxproj | 4 ++++ SessionMessagingKit/Common Networking/QueryParam.swift | 1 + .../Common Networking/UpdateTypes.swift | 7 +++++++ SessionMessagingKit/Open Groups/OpenGroupAPI.swift | 10 ++++++++-- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 SessionMessagingKit/Common Networking/UpdateTypes.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 7b148cb74..7fce16fa7 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -136,6 +136,7 @@ 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 */; }; 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 */; }; @@ -1171,6 +1172,7 @@ 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 = ""; }; 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 = ""; }; @@ -3840,6 +3842,7 @@ FDC4384E27B4804F00C60D73 /* Header.swift */, FDC4385027B4807400C60D73 /* QueryParam.swift */, FD83B9CD27D17A04005E1583 /* Request.swift */, + 7B81682228A4C1210069F315 /* UpdateTypes.swift */, ); path = "Common Networking"; sourceTree = ""; @@ -5191,6 +5194,7 @@ FD245C6B2850667400B966DD /* VisibleMessage+Profile.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 */, diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 28280b495..d50ffbab5 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -9,6 +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/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 518cec16c..b00f3a7e3 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -97,7 +97,10 @@ public enum OpenGroupAPI { .roomMessagesRecent(openGroup.roomToken) : .roomMessagesSince(openGroup.roomToken, seqNo: openGroup.sequenceNumber) ), - queryParameters: [.reactors: "20"] + queryParameters: [ + .updateTypes: UpdateTypes.reaction.rawValue, + .reactors: "20" + ] ), responseType: [Failable].self ) @@ -620,7 +623,10 @@ public enum OpenGroupAPI { request: Request( server: server, endpoint: .roomMessagesSince(roomToken, seqNo: seqNo), - queryParameters: [.reactors: "20"] + queryParameters: [ + .updateTypes: UpdateTypes.reaction.rawValue, + .reactors: "20" + ] ), using: dependencies ) From 88e419aac04932772658567a03071457beef2201 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 11 Aug 2022 15:21:19 +1000 Subject: [PATCH 082/133] clean --- SessionMessagingKit/Sending & Receiving/MessageSender.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 8235b9417..b4c5fcaea 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -433,7 +433,7 @@ public final class MessageSender { ) .done(on: DispatchQueue.global(qos: .default)) { responseInfo, data in message.openGroupServerMessageId = UInt64(data.id) - let serverTimestampMs: UInt64? = (data.posted == nil) ? nil : UInt64(floor(data.posted! * 1000)) + 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 From 9c9f1e3cd3166b1d554327db8b4ae6fcb7214553 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 12 Aug 2022 10:52:35 +1000 Subject: [PATCH 083/133] tweak: encode and sign reaction endpoints with raw emoji --- SessionMessagingKit/Open Groups/OpenGroupAPI.swift | 10 +++++++++- SessionSnodeKit/OnionRequestAPI.swift | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index b00f3a7e3..bcef9def5 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -675,6 +675,8 @@ public enum OpenGroupAPI { 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) } @@ -700,6 +702,8 @@ public enum OpenGroupAPI { 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) } @@ -725,6 +729,8 @@ public enum OpenGroupAPI { 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) } @@ -750,6 +756,8 @@ public enum OpenGroupAPI { 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) } @@ -1331,7 +1339,7 @@ public enum OpenGroupAPI { guard let url: URL = request.url else { return nil } var updatedRequest: URLRequest = request - let path: String = url.path.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path + let path: String = url.path .appending(url.query.map { value in "?\(value)" }) let method: String = (request.httpMethod ?? "GET") let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970)) diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 084081df9..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.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? url.path) + let endpoint: String = url.path .appending(url.query.map { value in "?\(value)" }) let requestInfo: RequestInfo = RequestInfo( From 93e2e295bcdf704e00c7df5057db97849ebe2ae1 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 17 Aug 2022 10:15:24 +1000 Subject: [PATCH 084/133] tweak: update default emojis --- Session/Emoji/EmojiWithSkinTones.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift index 12acd0340..d8b7d1283 100644 --- a/Session/Emoji/EmojiWithSkinTones.swift +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -52,7 +52,7 @@ extension Emoji { guard withDefaultEmoji else { return recentReactionEmoji } // Add in our default emoji if desired - let defaultEmoji = ["๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ˜ˆ", "๐Ÿฅธ", "๐Ÿ€"] + let defaultEmoji = ["๐Ÿ˜‚", "๐Ÿฅฐ", "๐Ÿ˜ข", "๐Ÿ˜ก", "๐Ÿ˜ฎ", "๐Ÿ˜ˆ"] .filter { !recentReactionEmoji.contains($0) } return Array(recentReactionEmoji From 78d8e939f1c33beea878254afd6fcf55b078fc6b Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 17 Aug 2022 15:08:49 +1000 Subject: [PATCH 085/133] tweak: fix search emoji --- .../Conversations/Emoji Picker/EmojiPickerCollectionView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index 8fb2340b6..e6ba1b3a8 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -152,7 +152,7 @@ class EmojiPickerCollectionView: UICollectionView { func searchWithText(_ searchText: String?) { if let searchText = searchText { emojiSearchResults = allSendableEmoji.filter { emoji in - return emoji.baseEmoji.name.range(of: searchText, options: [.caseInsensitive, .anchored]) != nil + return emoji.baseEmoji.name.range(of: searchText, options: [.caseInsensitive]) != nil } } else { emojiSearchResults = [] From 0a9856d49a0794e942024d30a682f58335335372 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 17 Aug 2022 16:41:01 +1000 Subject: [PATCH 086/133] tweak: fix reaction notifications --- Session.xcodeproj/project.pbxproj | 16 ++++++++-------- Session/Emoji/EmojiWithSkinTones.swift | 2 +- .../Database/Models/Interaction.swift | 2 +- .../MessageReceiver+VisibleMessages.swift | 17 ++++++++++++----- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 287f94a6b..8b7d37a4b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5782,7 +5782,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 369; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5807,7 +5807,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.0.2; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5855,7 +5855,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 369; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5885,7 +5885,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.0.2; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5921,7 +5921,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 369; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5944,7 +5944,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.0.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -5995,7 +5995,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 348; + CURRENT_PROJECT_VERSION = 369; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6023,7 +6023,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.13.0; + MARKETING_VERSION = 2.0.2; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift index d8b7d1283..44fd4e8a3 100644 --- a/Session/Emoji/EmojiWithSkinTones.swift +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Open Whisper Systems. All rights reserved. +// Copyright ยฉ 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index d94708b0d..ccd696ad9 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -582,7 +582,7 @@ public extension Interaction { func notificationIdentifier(isBackgroundPoll: Bool) -> String { // When the app is in the background we want the notifications to be grouped to prevent spam - guard isBackgroundPoll else { return threadId } + guard !isBackgroundPoll else { return threadId } return "\(threadId)-\(id ?? 0)" } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index b028be09d..8183a21b2 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -89,7 +89,7 @@ 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, threadId: threadInfo.id) { + if let interactionId: Int64 = try handleEmojiReactIfNeeded(db, message: message, associatedWithProto: proto, sender: sender, messageSentTimestamp: messageSentTimestamp, openGroupId: openGroupId, thread: thread) { return interactionId } @@ -304,7 +304,7 @@ extension MessageReceiver { sender: String, messageSentTimestamp: TimeInterval, openGroupId: String?, - threadId: String + thread: SessionThread ) throws -> Int64? { guard let reaction: VisibleMessage.VMReaction = message.reaction, @@ -313,7 +313,7 @@ extension MessageReceiver { let maybeInteractionId: Int64? = try? Interaction .select(.id) - .filter(Interaction.Columns.threadId == threadId) + .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) @@ -332,7 +332,7 @@ extension MessageReceiver { switch reaction.kind { case .react: - try Reaction( + let reaction = Reaction( interactionId: interactionId, serverHash: message.serverHash, timestampMs: Int64(messageSentTimestamp * 1000), @@ -340,7 +340,14 @@ extension MessageReceiver { emoji: reaction.emoji, count: 1, sortId: sortId - ).insert(db) + ) + try reaction.insert(db) + Environment.shared?.notificationsManager.wrappedValue? + .notifyUser( + db, + forReaction: reaction, + in: thread + ) case .remove: try Reaction From 95cd516bcebe867ae05e4c14f27cf88a7d70ed7e Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 17 Aug 2022 17:01:57 +1000 Subject: [PATCH 087/133] =?UTF-8?q?tweak:=20if=20current=20user=20reacts?= =?UTF-8?q?=20then=20their=20username=20should=20appear=20as=20=E2=80=98Yo?= =?UTF-8?q?u=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Session/Shared/UserCell.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Session/Shared/UserCell.swift b/Session/Shared/UserCell.swift index 8cde84c0a..45a523552 100644 --- a/Session/Shared/UserCell.swift +++ b/Session/Shared/UserCell.swift @@ -122,11 +122,15 @@ final class UserCell: UITableViewCell { .systemFont(ofSize: Values.mediumFontSize) : .boldSystemFont(ofSize: Values.mediumFontSize) ) - displayNameLabel.text = Profile.displayName( - for: .contact, - id: publicKey, - name: profile?.name, - nickname: profile?.nickname + + displayNameLabel.text = (getUserHexEncodedPublicKey() == publicKey ? + "MEDIA_GALLERY_SENDER_NAME_YOU".localized() : + Profile.displayName( + for: .contact, + id: publicKey, + name: profile?.name, + nickname: profile?.nickname + ) ) switch accessory { From 094c64bb6a2482b9f767783b03baf086cb40b859 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 18 Aug 2022 10:44:07 +1000 Subject: [PATCH 088/133] tweak: fix context menu screen layout --- .../Conversations/Context Menu/ContextMenuVC.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC.swift b/Session/Conversations/Context Menu/ContextMenuVC.swift index b7375d13f..976e8febf 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC.swift @@ -215,6 +215,7 @@ final class ContextMenuVC: UIViewController { 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 { @@ -224,7 +225,15 @@ final class ContextMenuVC: UIViewController { finalFrame.origin.y = UIScreen.main.bounds.height - finalFrame.size.height - menuHeight - bottomMargin - spacing } else { - finalFrame.origin.y = (UIScreen.main.bounds.height - finalFrame.size.height) / 2 + // 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 From 36801e48765f19f6b91cf56c45d0221de56040d1 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 18 Aug 2022 11:16:01 +1000 Subject: [PATCH 089/133] feat: disable emoji reacts for message requests --- Session/Conversations/ConversationVC+Interaction.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 6b2dc4691..27c1d4e8e 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -671,8 +671,9 @@ extension ConversationVC: contextMenuWindow == nil, let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( for: cellViewModel, - recentEmojis: (self.viewModel.threadData.recentReactionEmoji ?? []) - .compactMap { EmojiWithSkinTones(rawValue: $0) }, + recentEmojis: ((self.viewModel.threadData.threadIsMessageRequest == true) ? [] : + (self.viewModel.threadData.recentReactionEmoji ?? []) + ).compactMap { EmojiWithSkinTones(rawValue: $0) }, currentUserIsOpenGroupModerator: OpenGroupManager.isUserModeratorOrAdmin( self.viewModel.threadData.currentUserPublicKey, for: self.viewModel.threadData.openGroupRoomToken, @@ -1061,6 +1062,9 @@ extension ConversationVC: 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 From d116ac0ebe20e610a61c9b6959668ac437943814 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Aug 2022 13:38:02 +1000 Subject: [PATCH 090/133] Fixed an issue where the 2.0.2 migration broke the original _003_ migration --- .../_001_InitialSetupMigration.swift | 2 +- .../Migrations/_003_YDBToGRDBMigration.swift | 15 +++----- .../_006_FixHiddenModAdminSupport.swift | 38 +++++++++++++++++++ 3 files changed, 45 insertions(+), 10 deletions(-) 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..f1aefba2f 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) { 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 + } + } +} From f2f2083d9269b40355b333718112dacbfac291d0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 18 Aug 2022 14:20:44 +1000 Subject: [PATCH 091/133] Version number increases --- Session.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e77691409..073c981a7 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -6850,7 +6850,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 = 371; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6889,7 +6889,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.0.3; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -6922,7 +6922,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 = 371; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6961,7 +6961,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.0.3; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; From 7ca48e9c7bd8e905d5843321b77c29e28ec97125 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 18 Aug 2022 14:37:33 +1000 Subject: [PATCH 092/133] tweak: fix clear all action for open group moderators --- Session.xcodeproj/project.pbxproj | 2 +- Session/Conversations/ConversationVC+Interaction.swift | 10 ++++++---- .../Views & Modals/ReactionListSheet.swift | 10 ++++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 8b7d37a4b..f8ab9478f 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -3528,10 +3528,10 @@ FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */, FDF0B7412804EA4F004C14C5 /* _002_SetupStandardJobs.swift */, FD17D79827F40AB800122BE0 /* _003_YDBToGRDBMigration.swift */, - FD09B7E4288670BB00ED0B66 /* _007_EmojiReacts.swift */, FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, + FD09B7E4288670BB00ED0B66 /* _007_EmojiReacts.swift */, ); path = Migrations; sourceTree = ""; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 27c1d4e8e..5f3e9b1ba 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -978,7 +978,12 @@ extension ConversationVC: reactionListSheet.handleInteractionUpdates( allMessages, selectedReaction: selectedReaction, - initialLoad: true + 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) @@ -1176,9 +1181,6 @@ extension ConversationVC: in: thread ) } - }, - completion: { [weak self] _, _ in - self?.handleMessageSent() } ) } diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 95ca5b7d6..03e1f039a 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -186,7 +186,8 @@ final class ReactionListSheet: BaseVC { _ allMessages: [MessageViewModel], selectedReaction: EmojiWithSkinTones? = nil, updatedReactionIndex: Int? = nil, - initialLoad: Bool = false + initialLoad: Bool = false, + shouldShowClearAllButton: Bool = false ) { guard let cellViewModel: MessageViewModel = allMessages.first(where: { $0.id == self.interactionId }) else { return @@ -254,10 +255,8 @@ final class ReactionListSheet: BaseVC { } // Update the general UI - self.detailInfoLabel.text = updatedSummaries[safe: updatedSelectedReactionIndex]?.description - self.clearAllButton.isHidden = !cellViewModel.isSenderOpenGroupModerator - + // Update general properties self.messageViewModel = cellViewModel self.lastSelectedReactionIndex = updatedSelectedReactionIndex @@ -271,6 +270,9 @@ final class ReactionListSheet: BaseVC { .map { updatedReactionInfo.value(forKey: $0) } .defaulting(to: []) + // Update clear all button visibility + self.clearAllButton.isHidden = !shouldShowClearAllButton + UIView.performWithoutAnimation { self.reactionContainer.reloadData() self.userListView.reloadData() From 651373234ad287fa4bda6e27624cea4237eebcb2 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 18 Aug 2022 16:13:20 +1000 Subject: [PATCH 093/133] feat: add capability check for open group emoji reacts --- .../Context Menu/ContextMenuVC+Action.swift | 10 +++++++- .../ConversationVC+Interaction.swift | 9 ++++---- .../Database/Models/Capability.swift | 3 ++- .../Open Groups/OpenGroupManager.swift | 23 +++++++++++++++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 90fce90e0..d3765b7c3 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -105,6 +105,7 @@ extension ContextMenuVC { for cellViewModel: MessageViewModel, recentEmojis: [EmojiWithSkinTones], currentUserIsOpenGroupModerator: Bool, + currentThreadIsMessageRequest: Bool, delegate: ContextMenuActionDelegate? ) -> [Action]? { // No context items for info messages @@ -156,6 +157,13 @@ extension ContextMenuVC { 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), @@ -165,7 +173,7 @@ extension ContextMenuVC { (canBan ? Action.ban(cellViewModel, delegate) : nil), (canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil), ] - .appending(contentsOf: recentEmojis.map { Action.react(cellViewModel, $0, delegate) }) + .appending(contentsOf: (shouldShowEmojiActions ? recentEmojis : []).map { Action.react(cellViewModel, $0, delegate) }) .appending(Action.emojiPlusButton(cellViewModel, delegate)) .compactMap { $0 } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 5f3e9b1ba..ff2ecc991 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -671,14 +671,13 @@ extension ConversationVC: contextMenuWindow == nil, let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( for: cellViewModel, - recentEmojis: ((self.viewModel.threadData.threadIsMessageRequest == true) ? [] : - (self.viewModel.threadData.recentReactionEmoji ?? []) - ).compactMap { EmojiWithSkinTones(rawValue: $0) }, + 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 } @@ -1125,7 +1124,9 @@ extension ConversationVC: Emoji.addRecent(db, emoji: emoji) } - if let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: cellViewModel.threadId) { + 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 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/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index d291af196..7b6521d27 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -740,6 +740,29 @@ public final class OpenGroupManager: NSObject { // MARK: - Convenience + /// 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, From 43b6f0b6497459485636889364dd21169af10eec Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 18 Aug 2022 17:11:08 +1000 Subject: [PATCH 094/133] WIP: unsupported emojis --- Scripts/EmojiGenerator.swift | 2 +- Session/Emoji/EmojiWithSkinTones+String.swift | 2 +- Session/Emoji/EmojiWithSkinTones.swift | 38 +++++++++++++------ 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift index c52fedd5d..35d2b8ae8 100755 --- a/Scripts/EmojiGenerator.swift +++ b/Scripts/EmojiGenerator.swift @@ -301,7 +301,7 @@ extension EmojiGenerator { fileHandle.writeLine("} else {") fileHandle.indent { - fileHandle.writeLine("return nil") + fileHandle.writeLine("self.init(unsupportedValue: rawValue)") } fileHandle.writeLine("}") } diff --git a/Session/Emoji/EmojiWithSkinTones+String.swift b/Session/Emoji/EmojiWithSkinTones+String.swift index 9eeac6495..218048c86 100644 --- a/Session/Emoji/EmojiWithSkinTones+String.swift +++ b/Session/Emoji/EmojiWithSkinTones+String.swift @@ -7263,7 +7263,7 @@ extension EmojiWithSkinTones { } else if rawValue == "๐Ÿด๓ ง๓ ข๓ ท๓ ฌ๓ ณ๓ ฟ" { self.init(baseEmoji: .flagWales, skinTones: nil) } else { - return nil + self.init(unsupportedValue: rawValue) } } } diff --git a/Session/Emoji/EmojiWithSkinTones.swift b/Session/Emoji/EmojiWithSkinTones.swift index 44fd4e8a3..293a9c9c2 100644 --- a/Session/Emoji/EmojiWithSkinTones.swift +++ b/Session/Emoji/EmojiWithSkinTones.swift @@ -6,8 +6,9 @@ import DifferenceKit import SessionMessagingKit public struct EmojiWithSkinTones: Hashable, Equatable, ContentEquatable, ContentIdentifiable { - let baseEmoji: Emoji + let baseEmoji: Emoji? let skinTones: [Emoji.SkinTone]? + let unsupportedValue: String? init(baseEmoji: Emoji, skinTones: [Emoji.SkinTone]? = nil) { self.baseEmoji = baseEmoji @@ -20,23 +21,34 @@ public struct EmojiWithSkinTones: Hashable, Equatable, ContentEquatable, Content 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 skinTones = skinTones { - return baseEmoji.emojiPerSkinTonePermutation?[skinTones] ?? baseEmoji.rawValue - } else { - return baseEmoji.rawValue + 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 { - switch (baseEmoji, skinTones) { - case (let base, nil) where base.normalized != base: - return EmojiWithSkinTones(baseEmoji: base.normalized) - default: - return self + if let baseEmoji = baseEmoji, baseEmoji.normalized != baseEmoji { + return EmojiWithSkinTones(baseEmoji: baseEmoji.normalized) } + return self } var isNormalized: Bool { self == normalized } @@ -104,7 +116,11 @@ extension Emoji { init?(_ string: String) { guard let emojiWithSkinTonePermutation = EmojiWithSkinTones(rawValue: string) else { return nil } - self = emojiWithSkinTonePermutation.baseEmoji + if let baseEmoji = emojiWithSkinTonePermutation.baseEmoji { + self = baseEmoji + } else { + return nil + } } } From 3944fe52c1dbe45e1edb976ac2a35588ba1096f1 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 19 Aug 2022 10:10:07 +1000 Subject: [PATCH 095/133] feat: show unsupported emoji --- .../Conversations/Context Menu/ContextMenuVC+Action.swift | 2 +- .../Emoji Picker/EmojiPickerCollectionView.swift | 4 ++-- .../Conversations/Emoji Picker/EmojiSkinTonePicker.swift | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index d3765b7c3..fa6a64f2f 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -161,7 +161,7 @@ extension ContextMenuVC { if cellViewModel.threadVariant == .openGroup { return OpenGroupManager.isOpenGroupSupport(.reactions, on: cellViewModel.threadOpenGroupServer) } - return currentThreadIsMessageRequest + return !currentThreadIsMessageRequest }() let generatedActions: [Action] = [ diff --git a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift index e6ba1b3a8..a9bfcd1ae 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerCollectionView.swift @@ -152,7 +152,7 @@ class EmojiPickerCollectionView: UICollectionView { func searchWithText(_ searchText: String?) { if let searchText = searchText { emojiSearchResults = allSendableEmoji.filter { emoji in - return emoji.baseEmoji.name.range(of: searchText, options: [.caseInsensitive]) != nil + return emoji.baseEmoji?.name.range(of: searchText, options: [.caseInsensitive]) != nil } } else { emojiSearchResults = [] @@ -187,7 +187,7 @@ class EmojiPickerCollectionView: UICollectionView { currentSkinTonePicker = EmojiSkinTonePicker.present(referenceView: cell, emoji: emoji) { [weak self] emoji in if let emoji: EmojiWithSkinTones = emoji { Storage.shared.writeAsync { db in - emoji.baseEmoji.setPreferredSkinTones( + emoji.baseEmoji?.setPreferredSkinTones( db, preferredSkinTonePermutation: emoji.skinTones ) diff --git a/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift b/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift index ac7b5b34f..37eb27e42 100644 --- a/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift +++ b/Session/Conversations/Emoji Picker/EmojiSkinTonePicker.swift @@ -14,7 +14,7 @@ class EmojiSkinTonePicker: UIView { emoji: EmojiWithSkinTones, completion: @escaping (EmojiWithSkinTones?) -> Void ) -> EmojiSkinTonePicker? { - guard emoji.baseEmoji.hasSkinTones else { return nil } + guard let baseEmoji = emoji.baseEmoji, baseEmoji.hasSkinTones else { return nil } UIImpactFeedbackGenerator(style: .light).impactOccurred() @@ -104,9 +104,9 @@ class EmojiSkinTonePicker: UIView { } init(emoji: EmojiWithSkinTones, completion: @escaping (EmojiWithSkinTones?) -> Void) { - owsAssertDebug(emoji.baseEmoji.hasSkinTones) + owsAssertDebug(emoji.baseEmoji!.hasSkinTones) - self.emoji = emoji.baseEmoji + self.emoji = emoji.baseEmoji! self.preferredSkinTonePermutation = emoji.skinTones self.completion = completion @@ -127,7 +127,7 @@ class EmojiSkinTonePicker: UIView { containerView.autoPinWidthToSuperview() containerView.setCompressionResistanceHigh() - if emoji.baseEmoji.allowsMultipleSkinTones { + if emoji.baseEmoji!.allowsMultipleSkinTones { prepareForMultipleSkinTones() } else { prepareForSingleSkinTone() From 7097853d58ef07adc930963e2a239535f4c31005 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 19 Aug 2022 16:58:47 +1000 Subject: [PATCH 096/133] A few bugs fixes and some optimisations Fixed a bug where notifications could incorrectly appear for messages in the current thread Fixed a bug where swiping left/right on images in the MediaDetailViewController could load images from other threads Fixed a bug where the unread count wouldn't appear correctly when opening a conversation Fixed a bug where the unread count on the conversation cell could get truncated Fixed a bug where notifications weren't working correctly when the app is in the foreground Fixed a bug where we weren't clearing the 'received X new messages' count when in the foreground Fixed a bug where outgoing messages could get marked as read in a very specific case Updated the "group notification" logic to only apply to Open Groups (and always doing it rather than just in the background) Added a placeholder person icon when you have a closed group with a single member Added a couple of indexes to improve the HomeVC database query performance (reduce launch time by ~15% in some cases) Added a background task to give message sending the chance to complete when sending the app to the background Removed an unneeded query from the HomeViewModel init (reduce launch time by ~10% in some cases) Optimised one of the queries used to load the data needed for the conversation screen --- Session.xcodeproj/project.pbxproj | 28 ++-- Session/Conversations/ConversationVC.swift | 85 ++++++++-- Session/Home/HomeVC.swift | 4 +- Session/Home/HomeViewModel.swift | 15 +- .../MessageRequestsViewModel.swift | 4 +- .../MediaGalleryViewModel.swift | 14 +- .../MediaPageViewController.swift | 18 ++- Session/Meta/AppDelegate.swift | 7 +- Session/Meta/Signal-Bridging-Header.h | 1 - Session/Notifications/AppNotifications.swift | 149 +++++++++++------- .../UserNotificationsAdaptee.swift | 93 +++++++---- Session/Shared/FullConversationCell.swift | 2 + Session/Utilities/BackgroundPoller.swift | 6 +- Session/Utilities/IP2Country.swift | 4 +- SessionMessagingKit/Configuration.swift | 3 +- .../Migrations/_003_YDBToGRDBMigration.swift | 2 +- .../_007_HomeQueryOptimisationIndexes.swift | 37 +++++ .../Database/Models/Interaction.swift | 44 ++++-- .../Database/Models/SessionThread.swift | 27 ++-- .../Jobs/Types/MessageReceiveJob.swift | 15 +- .../Meta/SessionMessagingKit.h | 1 - .../Open Groups/OpenGroupManager.swift | 4 - .../MessageReceiver+VisibleMessages.swift | 4 +- .../Sending & Receiving/MessageReceiver.swift | 6 +- .../Notifications/NotificationsProtocol.swift | 8 +- .../Pollers/ClosedGroupPoller.swift | 18 +-- .../Pollers/OpenGroupPoller.swift | 21 ++- .../Sending & Receiving/Pollers/Poller.swift | 2 +- .../SessionThreadViewModel.swift | 27 ++-- .../Open Groups/OpenGroupManagerSpec.swift | 21 --- .../NSENotificationPresenter.swift | 61 ++++--- .../NotificationServiceExtension.swift | 5 +- .../Types/PagedDatabaseObserver.swift | 132 +++++++++++++--- SessionUtilitiesKit/JobRunner/JobRunner.swift | 64 +++++++- .../Meta/SessionUtilitiesKit.h | 1 + .../Utilities/OWSBackgroundTask.h | 3 + .../Utilities/OWSBackgroundTask.m | 25 +++ .../Profile Pictures/ProfilePictureView.swift | 27 ++++ .../Utilities/NoopNotificationsManager.swift | 2 +- 39 files changed, 691 insertions(+), 299 deletions(-) create mode 100644 SessionMessagingKit/Database/Migrations/_007_HomeQueryOptimisationIndexes.swift rename {SessionMessagingKit => SessionUtilitiesKit}/Utilities/OWSBackgroundTask.h (97%) rename {SessionMessagingKit => SessionUtilitiesKit}/Utilities/OWSBackgroundTask.m (95%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index e77691409..8af44cbd2 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -302,8 +302,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 */; }; @@ -648,6 +646,7 @@ FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; }; FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; }; FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; }; + FD37EA1B28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; }; FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; @@ -661,6 +660,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 */; }; @@ -780,7 +781,6 @@ 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 */; }; @@ -1691,6 +1691,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 = ""; }; @@ -1816,7 +1817,6 @@ 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 = ""; }; @@ -3057,8 +3057,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 */, @@ -3126,7 +3124,6 @@ B8A582AE258C65D000AFD84C /* Networking */, B8A582AD258C655E00AFD84C /* PromiseKit */, FD09796527F6B0A800936362 /* Utilities */, - FDCDB8EF2817ABCE00352A0C /* Utilities */, C3D9E43025676D3D0040E4F3 /* Configuration.swift */, ); path = SessionUtilitiesKit; @@ -3426,6 +3423,8 @@ FD09797127FAA2F500936362 /* Optional+Utilities.swift */, FD09797C27FBDB2000936362 /* Notification+Utilities.swift */, FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */, + C33FDB38255A580B00E217F9 /* OWSBackgroundTask.h */, + C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */, ); path = Utilities; sourceTree = ""; @@ -3464,6 +3463,7 @@ FDF40CDD2897A1BC006A0CC4 /* _004_RemoveLegacyYDB.swift */, FD37EA0C28AB2A45003AE748 /* _005_FixDeletedMessageReadState.swift */, FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */, + FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */, ); path = Migrations; sourceTree = ""; @@ -3884,14 +3884,6 @@ path = Models; sourceTree = ""; }; - FDCDB8EF2817ABCE00352A0C /* Utilities */ = { - isa = PBXGroup; - children = ( - FDCDB8F02817ABE600352A0C /* Optional+Utilities.swift */, - ); - path = Utilities; - sourceTree = ""; - }; FDE7214E287E50D50093DF33 /* Scripts */ = { isa = PBXGroup; children = ( @@ -4006,6 +3998,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; }; @@ -4015,7 +4008,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 */, @@ -5022,7 +5014,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 */, @@ -5068,6 +5059,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 */, @@ -5114,6 +5106,7 @@ files = ( FD86585828507B24008B6CF9 /* NSData+messagePadding.m in Sources */, FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */, + FD37EA1B28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift in Sources */, B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */, FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */, @@ -5241,7 +5234,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 */, diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index b06c8d0b2..a8491c901 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -688,6 +688,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 +731,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 +740,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 +768,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 +782,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 +816,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,13 +832,36 @@ 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() + } + } self.tableView.reload( using: changeset, @@ -837,13 +887,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 +1068,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() @@ -1225,6 +1276,7 @@ final class ConversationVC: BaseVC, OWSConversationSettingsViewDelegate, Convers self.scrollToInteractionIfNeeded( with: lastInteractionId, position: .bottom, + isJumpingToLastInteraction: true, isAnimated: true ) return @@ -1283,7 +1335,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 +1446,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 +1470,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/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..3b20abebe 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 @@ -139,14 +138,14 @@ public class HomeViewModel { }() ) ], - /// **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/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index e894f5ad6..3d3f62fbf 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 ) } } @@ -243,9 +272,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { 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,23 +300,26 @@ 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 ) } @@ -294,9 +329,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { 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 ) } @@ -306,24 +344,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 @@ -340,7 +378,10 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { title: notificationTitle, body: notificationBody, userInfo: userInfo, - sound: sound + previewType: previewType, + sound: sound, + threadVariant: thread.variant, + threadName: threadName ) } } 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/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/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/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/Configuration.swift b/SessionMessagingKit/Configuration.swift index 2230a04de..9aaba1842 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -18,7 +18,8 @@ public enum SNMessagingKit { // Just to make the external API nice ], [ _005_FixDeletedMessageReadState.self, - _006_FixHiddenModAdminSupport.self + _006_FixHiddenModAdminSupport.self, + _007_HomeQueryOptimisationIndexes.self ] ] ) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index b748da16d..df7ddce57 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -1253,7 +1253,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/_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/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index d94708b0d..641d33d75 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 @@ -409,7 +409,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 +453,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 +590,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 +624,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/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/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/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 3e8370ddb..52cc02e49 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -512,7 +512,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 @@ -564,7 +563,6 @@ public final class OpenGroupManager: NSObject { message: messageInfo.message, associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData), openGroupId: openGroup.id, - isBackgroundPoll: isBackgroundPoll, dependencies: dependencies ) } @@ -597,7 +595,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 +691,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 ) } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index a18e0560b..78a4f4cdc 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 { @@ -285,8 +284,7 @@ extension MessageReceiver { .notifyUser( db, for: interaction, - in: thread, - isBackgroundPoll: isBackgroundPoll + in: thread ) return interactionId diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 4524b75d2..db3c18a7e 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() diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift index 0fefd991f..2622f7d2d 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/NotificationsProtocol.swift @@ -4,8 +4,14 @@ 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 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..02a3d139a 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -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/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 58e07f7e5..d9eea8da0 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -350,7 +350,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 +367,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 +412,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 +440,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 +461,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 +545,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 +563,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 +574,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 +601,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 +689,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 +703,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/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 9a598a15b..2158aa3e6 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -2120,7 +2120,6 @@ class OpenGroupManagerSpec: QuickSpec { ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2142,7 +2141,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2182,7 +2180,6 @@ class OpenGroupManagerSpec: QuickSpec { ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2215,7 +2212,6 @@ class OpenGroupManagerSpec: QuickSpec { ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2230,7 +2226,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testMessage], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2260,7 +2255,6 @@ class OpenGroupManagerSpec: QuickSpec { ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2298,7 +2292,6 @@ class OpenGroupManagerSpec: QuickSpec { ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2327,7 +2320,6 @@ class OpenGroupManagerSpec: QuickSpec { ], for: "testRoom", on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2379,7 +2371,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2413,7 +2404,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2452,7 +2442,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2478,7 +2467,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2509,7 +2497,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2524,7 +2511,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2549,7 +2535,6 @@ class OpenGroupManagerSpec: QuickSpec { ], fromOutbox: false, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2576,7 +2561,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2607,7 +2591,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2623,7 +2606,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2659,7 +2641,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2674,7 +2655,6 @@ class OpenGroupManagerSpec: QuickSpec { messages: [testDirectMessage], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } @@ -2699,7 +2679,6 @@ class OpenGroupManagerSpec: QuickSpec { ], fromOutbox: true, on: "testServer", - isBackgroundPoll: false, dependencies: dependencies ) } diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 04503ed5f..40b5c01ee 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,7 +117,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationContent.userInfo[NotificationServiceExtension.threadNotificationCounter] = numberOfNotifications } - let request = UNNotificationRequest(identifier: identifier, content: notificationContent, trigger: trigger) + let request = UNNotificationRequest( + identifier: identifier, + content: notificationContent, + trigger: trigger + ) SNLog("Add remote notification request: \(notificationContent.body)") let semaphore = DispatchSemaphore(value: 0) 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/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index bdc17323e..9b155386e 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -379,7 +379,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 +388,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 +405,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 +422,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 +466,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 +487,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 +569,8 @@ public class PagedDatabaseObserver: TransactionObserver where pageOffset: currentPageInfo.pageOffset, currentCount: currentPageInfo.currentCount, totalCount: totalCount - ) + ), + callback ) } @@ -540,7 +609,7 @@ public class PagedDatabaseObserver: TransactionObserver where ) } - return (newData, updatedLimitInfo) + return (newData, updatedLimitInfo, nil) } // Unwrap the updated data @@ -554,6 +623,7 @@ public class PagedDatabaseObserver: TransactionObserver where self.pageInfo.mutate { $0 = updatedPageInfo } } self.isLoadingMoreData.mutate { $0 = false } + loadedPage?.failureCallback?() return } @@ -651,6 +721,7 @@ public protocol ErasedAssociatedRecord { pageInfo: PagedData.PageInfo ) -> Bool @discardableResult func updateCache(_ db: Database, rowIds: [Int64], hasOtherChanges: Bool) -> Bool + func clearCache(_ db: Database) func attachAssociatedData(to unassociatedCache: DataCache) -> DataCache } @@ -733,6 +804,7 @@ public enum PagedData { case pageBefore case pageAfter case untilInclusive(id: SQLExpression, padding: Int) + case jumpTo(id: SQLExpression, paddingForInclusive: Int) case reloadCurrent } @@ -755,6 +827,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 +841,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) } } } @@ -1144,6 +1226,10 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet return true } + public func clearCache(_ db: Database) { + dataCache.mutate { $0 = DataCache() } + } + public func attachAssociatedData(to unassociatedCache: DataCache) -> DataCache { guard let typedCache: DataCache = unassociatedCache as? DataCache else { return unassociatedCache diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index c84a11283..b49666600 100644 --- a/SessionUtilitiesKit/JobRunner/JobRunner.swift +++ b/SessionUtilitiesKit/JobRunner/JobRunner.swift @@ -103,6 +103,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 +223,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 +268,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 } } } @@ -370,7 +426,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( 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..909a9f347 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("") } From 838623171d95e762cc28516a927dda51151b66f4 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 22 Aug 2022 16:33:25 +1000 Subject: [PATCH 097/133] tweak: always left align emoji reacts --- .../Content Views/ReactionContainerView.swift | 17 +++-------------- .../Message Cells/VisibleMessageCell.swift | 1 - 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift index 15826ab44..982195f50 100644 --- a/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift +++ b/Session/Conversations/Message Cells/Content Views/ReactionContainerView.swift @@ -5,7 +5,6 @@ import SessionUIKit final class ReactionContainerView: UIView { var showingAllReactions = false - private var isOutgoingMessage = false private var showNumbers = true private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6 @@ -68,9 +67,8 @@ final class ReactionContainerView: UIView { mainStackView.pin(to: self) } - public func update(_ reactions: [ReactionViewModel], isOutgoingMessage: Bool, showNumbers: Bool) { + public func update(_ reactions: [ReactionViewModel], showNumbers: Bool) { self.reactions = reactions - self.isOutgoingMessage = isOutgoingMessage self.showNumbers = showNumbers prepareForUpdate() @@ -89,15 +87,6 @@ final class ReactionContainerView: UIView { stackView.spacing = Values.smallSpacing stackView.alignment = .center - if isOutgoingMessage { - stackView.semanticContentAttribute = .forceRightToLeft - reactionContainerView.semanticContentAttribute = .forceRightToLeft - } - else { - stackView.semanticContentAttribute = .unspecified - reactionContainerView.semanticContentAttribute = .unspecified - } - var displayedReactions: [ReactionViewModel] var expandButtonReactions: [EmojiWithSkinTones] @@ -168,14 +157,14 @@ final class ReactionContainerView: UIView { guard !showingAllReactions else { return } showingAllReactions = true - update(reactions, isOutgoingMessage: isOutgoingMessage, showNumbers: showNumbers) + update(reactions, showNumbers: showNumbers) } public func showLessEmojis() { guard showingAllReactions else { return } showingAllReactions = false - update(reactions, isOutgoingMessage: isOutgoingMessage, showNumbers: showNumbers) + update(reactions, showNumbers: showNumbers) } } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index d50f8618b..721244882 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -641,7 +641,6 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { reactionContainerView.showingAllReactions = showExpandedReactions reactionContainerView.update( reactions.orderedValues, - isOutgoingMessage: (cellViewModel.variant == .standardOutgoing), showNumbers: ( cellViewModel.threadVariant == .closedGroup || cellViewModel.threadVariant == .openGroup From 6a32fbaf833511542f8a8e78ec71be0f634fdf51 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 23 Aug 2022 09:02:56 +1000 Subject: [PATCH 098/133] Fixed a bug where the "default" notification sound wouldn't be used in the foreground --- Session/Notifications/AppNotifications.swift | 28 +++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index e894f5ad6..f4a46f23b 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -230,15 +230,21 @@ 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, @@ -287,9 +293,14 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { ) ) } + 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, @@ -331,9 +342,14 @@ 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, @@ -366,12 +382,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 { From 014e8620c42fc761953f284c1fe3b453b6b72b79 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 23 Aug 2022 12:17:23 +1000 Subject: [PATCH 099/133] bump up version & build number --- Session.xcodeproj/project.pbxproj | 25 ++++++++++++------------- Session/Meta/Session-Info.plist | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5d9dfebd2..bfb2637c1 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -666,7 +666,6 @@ FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA0E28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift */; }; FD37EA1128AB34B3003AE748 /* TypedTableAlteration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1028AB34B3003AE748 /* TypedTableAlteration.swift */; }; FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1428AB42CB003AE748 /* IdentitySpec.swift */; }; - FD37EA1B28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */; }; FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; }; FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; }; FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; @@ -5785,7 +5784,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 369; + CURRENT_PROJECT_VERSION = 372; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5810,7 +5809,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5858,7 +5857,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 369; + CURRENT_PROJECT_VERSION = 372; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5888,7 +5887,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5924,7 +5923,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 369; + CURRENT_PROJECT_VERSION = 372; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5947,7 +5946,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -5998,7 +5997,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 369; + CURRENT_PROJECT_VERSION = 372; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6026,7 +6025,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.0.2; + MARKETING_VERSION = 2.1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6936,7 +6935,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 371; + CURRENT_PROJECT_VERSION = 372; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6975,7 +6974,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.3; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7008,7 +7007,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 371; + CURRENT_PROJECT_VERSION = 372; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7047,7 +7046,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.0.3; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Meta/Session-Info.plist b/Session/Meta/Session-Info.plist index 3bb826685..54e23fb8d 100644 --- a/Session/Meta/Session-Info.plist +++ b/Session/Meta/Session-Info.plist @@ -90,7 +90,7 @@ NSContactsUsageDescription Signal uses your contacts to find users you know. We do not store your contacts on the server. NSFaceIDUsageDescription - Session's Screen Lock feature uses Face ID. + Session's Screen Lock feature uses Face ID. NSHumanReadableCopyright com.loki-project.loki-messenger NSMicrophoneUsageDescription From 851ad6c4c9f975add8e4195b4618b115658a0031 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 23 Aug 2022 12:25:23 +1000 Subject: [PATCH 100/133] minor refactor --- SessionMessagingKit/Configuration.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 166f1c6ed..54e1b0258 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -19,7 +19,9 @@ public enum SNMessagingKit { // Just to make the external API nice [ _005_FixDeletedMessageReadState.self, _006_FixHiddenModAdminSupport.self, - _007_HomeQueryOptimisationIndexes.self, + _007_HomeQueryOptimisationIndexes.self + ], + [ _008_EmojiReacts.self ] ] From f9c2a2ce99b174fac0eef5cea6e2713f920827af Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 25 Aug 2022 11:12:22 +1000 Subject: [PATCH 101/133] tweak: fix an issue where we may delete the wrong open group message and update reactions to wrong open group messages in other threads with the same server id --- SessionMessagingKit/Open Groups/OpenGroupManager.swift | 2 ++ SessionMessagingKit/Sending & Receiving/MessageReceiver.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 0f532bb9b..33fee96cc 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -594,6 +594,7 @@ public final class OpenGroupManager: NSObject { try MessageReceiver.handleOpenGroupReactions( db, + threadId: openGroup.threadId, openGroupMessageServerId: message.id, openGroupReactions: reactions ) @@ -608,6 +609,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) } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 20141c74b..2e427fda1 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -249,11 +249,13 @@ 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) From 128ef747b33aefd7ecb5b010709f6f8759c2576b Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 25 Aug 2022 11:51:53 +1000 Subject: [PATCH 102/133] tweak: only fetch and show 5 reactors for open group reactions --- SessionMessagingKit/Messages/Message.swift | 3 +++ SessionMessagingKit/Open Groups/OpenGroupAPI.swift | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 9dec50347..f37f93f10 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -369,8 +369,11 @@ public extension Message { let reactors = rawReaction.reactors { let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) + let maxLength: Int = !rawReaction.you || reactors.contains(userPublicKey) ? 5 : 4 let desiredReactorIds: [String] = reactors .filter { $0 != blindedUserPublicKey } + .prefix(maxLength) + .map{ $0 } results = results .appending( // Add the first reaction (with the count) diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index bcef9def5..e27f93428 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -99,7 +99,7 @@ public enum OpenGroupAPI { ), queryParameters: [ .updateTypes: UpdateTypes.reaction.rawValue, - .reactors: "20" + .reactors: "5" ] ), responseType: [Failable].self From 3a81ffc752c835f483ef59868f271c35296b1a04 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 25 Aug 2022 12:01:49 +1000 Subject: [PATCH 103/133] feat: add response for reaction endpoints --- Session.xcodeproj/project.pbxproj | 4 ++ .../Open Groups/Models/ReactionResponse.swift | 44 +++++++++++++++++++ .../Open Groups/OpenGroupAPI.swift | 18 ++++++-- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 SessionMessagingKit/Open Groups/Models/ReactionResponse.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index bfb2637c1..15c1cea40 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ 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 */; }; 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 */; }; @@ -1178,6 +1179,7 @@ 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 = ""; }; 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 = ""; }; @@ -3853,6 +3855,7 @@ FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, + 7B81682928B6F1420069F315 /* ReactionResponse.swift */, ); path = Models; sourceTree = ""; @@ -5203,6 +5206,7 @@ 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 */, diff --git a/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift b/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift new file mode 100644 index 000000000..51df94e20 --- /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/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index e27f93428..01ceb20fc 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -718,7 +718,11 @@ public enum OpenGroupAPI { ), using: dependencies ) - .map { responseInfo, _ in responseInfo } + .decoded(as: ReactionAddResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) + .map { responseInfo, addResponse in + print("\(addResponse)") + return responseInfo + } } public static func reactionDelete( @@ -745,7 +749,11 @@ public enum OpenGroupAPI { ), using: dependencies ) - .map { responseInfo, _ in responseInfo } + .decoded(as: ReactionRemoveResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) + .map { responseInfo, removeResponse in + print("\(removeResponse)") + return responseInfo + } } public static func reactionDeleteAll( @@ -772,7 +780,11 @@ public enum OpenGroupAPI { ), using: dependencies ) - .map { responseInfo, _ in responseInfo } + .decoded(as: ReactionRemoveAllResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) + .map { responseInfo, removeAllResponse in + print("\(removeAllResponse)") + return responseInfo + } } // MARK: - Pinning From c73bb43c56981482db7bc1461ed0486e10675794 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 25 Aug 2022 12:55:41 +1000 Subject: [PATCH 104/133] Fixed a few bugs Fixed a bug where open group message deletion wasn't thread-specific Fixed a bug where the user couldn't delete pending/failed messages being sent to an open group Fixed a bug where deleting content from "related" tables wouldn't correctly trigger an update within the PagedDatabaseObserver Fixed a bug where a user that was an admin/mod of one open group would incorrectly appear to be an admin/mod of all open groups --- .../Context Menu/ContextMenuVC+Action.swift | 3 +- .../ConversationVC+Interaction.swift | 53 ++++++++- .../Open Groups/OpenGroupManager.swift | 1 + .../Shared Models/MessageViewModel.swift | 3 + .../Types/PagedDatabaseObserver.swift | 59 ++++++++-- SessionUtilitiesKit/JobRunner/JobRunner.swift | 102 +++++++++++++++--- 6 files changed, 199 insertions(+), 22 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index fa6a64f2f..cbe87d1b0 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -150,7 +150,8 @@ extension ContextMenuVC { ) let canDelete: Bool = ( cellViewModel.threadVariant != .openGroup || - currentUserIsOpenGroupModerator + currentUserIsOpenGroupModerator || + cellViewModel.state == .failed ) let canBan: Bool = ( cellViewModel.threadVariant == .openGroup && diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ff2ecc991..5a9a05bca 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1134,7 +1134,58 @@ extension ConversationVC: .filter(id: cellViewModel.id) .asRequest(of: Int64.self) .fetchOne(db) - 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 + } if remove { OpenGroupAPI diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 0f532bb9b..553fe0786 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -608,6 +608,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) } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 37e4751ab..861c82628 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -637,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) @@ -715,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)")) ) diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index b9af5b36b..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 @@ -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: []) @@ -904,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 } } diff --git a/SessionUtilitiesKit/JobRunner/JobRunner.swift b/SessionUtilitiesKit/JobRunner/JobRunner.swift index b49666600..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, @@ -332,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 } @@ -339,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 { @@ -445,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([:]) @@ -560,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) { @@ -900,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() } @@ -914,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() @@ -923,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() @@ -1003,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() } @@ -1014,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( @@ -1055,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) } + } + } } From 84ccb63b3520bc6ffc836559381324ec332f1b0c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 25 Aug 2022 14:16:59 +1000 Subject: [PATCH 105/133] Fixed a bug where the "DataExtractionNotification" messages weren't correctly setting their tmestamps --- .../MessageReceiver+DataExtractionNotification.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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) } } From 543f729247507f20e89d304235a26b7c7ebb160a Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 25 Aug 2022 17:24:43 +1000 Subject: [PATCH 106/133] feat: add local cache and deal with merging for reaction changes --- Session.xcodeproj/project.pbxproj | 4 ++ .../ConversationVC+Interaction.swift | 30 +++++++++ SessionMessagingKit/Messages/Message.swift | 38 +++++++++--- .../Open Groups/Models/PendingChange.swift | 41 +++++++++++++ .../Open Groups/OpenGroupAPI.swift | 18 +----- .../Open Groups/OpenGroupManager.swift | 61 ++++++++++++++++++- 6 files changed, 167 insertions(+), 25 deletions(-) create mode 100644 SessionMessagingKit/Open Groups/Models/PendingChange.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 15c1cea40..81e1ae0a9 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -139,6 +139,7 @@ 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 */; }; @@ -1180,6 +1181,7 @@ 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 = ""; }; @@ -3856,6 +3858,7 @@ FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, 7B81682928B6F1420069F315 /* ReactionResponse.swift */, + 7B81682B28B72F480069F315 /* PendingChange.swift */, ); path = Models; sourceTree = ""; @@ -5193,6 +5196,7 @@ 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 */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index ff2ecc991..394810b2b 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1137,6 +1137,14 @@ extension ConversationVC: else { return } if remove { + let pendingChange = OpenGroupManager + .addPendingReaction( + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server, + type: .remove + ) OpenGroupAPI .reactionDelete( db, @@ -1145,8 +1153,23 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } .retainUntilComplete() } else { + let pendingChange = OpenGroupManager + .addPendingReaction( + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server, + type: .react + ) OpenGroupAPI .reactionAdd( db, @@ -1155,6 +1178,13 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } .retainUntilComplete() } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index f37f93f10..b16291a26 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -353,6 +353,7 @@ public extension Message { _ db: Database, openGroupId: String, message: OpenGroupAPI.Message, + associatedPendingChanges: [OpenGroupAPI.PendingChange], dependencies: SMKDependencies = SMKDependencies() ) -> [Reaction] { var results: [Reaction] = [] @@ -364,14 +365,33 @@ public extension Message { threadVariant: .openGroup ) for (encodedEmoji, rawReaction) in reactions { - if let emoji = encodedEmoji.removingPercentEncoding, + if let decodedEmoji = encodedEmoji.removingPercentEncoding, rawReaction.count > 0, let reactors = rawReaction.reactors { + // Decide whether we need to add an extra reaction from current user + let pendingChanges = associatedPendingChanges + .filter { + if case .reaction(_, let emoji, _) = $0.metadata { + return emoji == decodedEmoji + } + return false + } + var shouldAddSelfReaction: Bool = rawReaction.you || reactors.contains(userPublicKey) + pendingChanges.forEach { + if case .reaction(_, _, let action) = $0.metadata { + switch action { + case .react: shouldAddSelfReaction = true + case .remove: shouldAddSelfReaction = false + } + } + } + + let count: Int64 = shouldAddSelfReaction ? rawReaction.count - 1 : rawReaction.count let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) - let maxLength: Int = !rawReaction.you || reactors.contains(userPublicKey) ? 5 : 4 + let maxLength: Int = shouldAddSelfReaction ? 4 : 5 let desiredReactorIds: [String] = reactors - .filter { $0 != blindedUserPublicKey } + .filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed .prefix(maxLength) .map{ $0 } @@ -384,8 +404,8 @@ public extension Message { serverHash: nil, timestampMs: timestampMs, authorId: reactor, - emoji: emoji, - count: rawReaction.count, + emoji: decodedEmoji, + count: count, sortId: rawReaction.index ) } @@ -401,22 +421,22 @@ public extension Message { serverHash: nil, timestampMs: timestampMs, authorId: reactor, - emoji: emoji, + 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) - !rawReaction.you || reactors.contains(userPublicKey) ? + !shouldAddSelfReaction ? nil : Reaction( interactionId: message.id, serverHash: nil, timestampMs: timestampMs, authorId: userPublicKey, - emoji: emoji, - count: (desiredReactorIds.isEmpty ? rawReaction.count : 0), + emoji: decodedEmoji, + count: 1, sortId: rawReaction.index ) ) diff --git a/SessionMessagingKit/Open Groups/Models/PendingChange.swift b/SessionMessagingKit/Open Groups/Models/PendingChange.swift new file mode 100644 index 000000000..1c9c3e513 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/PendingChange.swift @@ -0,0 +1,41 @@ +// Copyright ยฉ 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPI { + public struct PendingChange: Equatable { + enum ChangeType { + case reaction + } + + enum Metadata { + case reaction(messageId: Int64, emoji: String, action: VisibleMessage.VMReaction.Kind) + } + + 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/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 01ceb20fc..04cf63b57 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -701,7 +701,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: SMKDependencies = SMKDependencies() - ) -> Promise { + ) -> 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 { @@ -719,10 +719,6 @@ public enum OpenGroupAPI { using: dependencies ) .decoded(as: ReactionAddResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) - .map { responseInfo, addResponse in - print("\(addResponse)") - return responseInfo - } } public static func reactionDelete( @@ -732,7 +728,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: SMKDependencies = SMKDependencies() - ) -> Promise { + ) -> 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 { @@ -750,10 +746,6 @@ public enum OpenGroupAPI { using: dependencies ) .decoded(as: ReactionRemoveResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) - .map { responseInfo, removeResponse in - print("\(removeResponse)") - return responseInfo - } } public static func reactionDeleteAll( @@ -763,7 +755,7 @@ public enum OpenGroupAPI { in roomToken: String, on server: String, using dependencies: SMKDependencies = SMKDependencies() - ) -> Promise { + ) -> 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 { @@ -781,10 +773,6 @@ public enum OpenGroupAPI { using: dependencies ) .decoded(as: ReactionRemoveAllResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) - .map { responseInfo, removeAllResponse in - print("\(removeAllResponse)") - return responseInfo - } } // MARK: - Pinning diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 33fee96cc..4364a4dff 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -20,6 +20,8 @@ public protocol OGMCacheType { var timeSinceLastPoll: [String: TimeInterval] { get set } func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval + + var pendingChanges: [OpenGroupAPI.PendingChange] { get set } } // MARK: - OpenGroupManager @@ -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 @@ -529,11 +533,17 @@ 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 @@ -589,6 +599,17 @@ public final class OpenGroupManager: NSObject { 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 ) @@ -738,6 +759,44 @@ public final class OpenGroupManager: NSObject { // MARK: - Convenience + public static func addPendingReaction( + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + type: VisibleMessage.VMReaction.Kind, + 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 + } + } + } + /// This method specifies if the given capability is supported on a specified Open Group public static func isOpenGroupSupport( _ capability: Capability.Variant, From 81ef5a744a7cd44bc017aa5e4a3cfc0e69a25aae Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 25 Aug 2022 17:28:48 +1000 Subject: [PATCH 107/133] fix count logic --- SessionMessagingKit/Messages/Message.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index b16291a26..7b0765885 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -387,7 +387,8 @@ public extension Message { } } - let count: Int64 = shouldAddSelfReaction ? rawReaction.count - 1 : rawReaction.count + 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 From ae639b2474fcda6378ed41e00df2b95b5f079e15 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 26 Aug 2022 10:53:48 +1000 Subject: [PATCH 108/133] minor fix --- Session/Notifications/AppNotifications.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 96c6bea16..357b39c4a 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -384,9 +384,12 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { 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) + let sound = self.requestSound(thread: thread, fallbackSound: fallbackSound) self.adaptee.notify( category: category, From 8f1a03db889588b41d82c3998728c1557e5843db Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 26 Aug 2022 13:33:49 +1000 Subject: [PATCH 109/133] Moved the code for a previous fix to the correct location and fixed a build issue --- .../ConversationVC+Interaction.swift | 108 +++++++++--------- Session/Notifications/AppNotifications.swift | 7 +- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 5a9a05bca..dd44a4036 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1134,58 +1134,7 @@ extension ConversationVC: .filter(id: cellViewModel.id) .asRequest(of: Int64.self) .fetchOne(db) - 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 - } + else { return } if remove { OpenGroupAPI @@ -1434,7 +1383,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/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 96c6bea16..e0c25749b 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -384,9 +384,14 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { 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) + let sound = self.requestSound( + thread: thread, + fallbackSound: fallbackSound + ) self.adaptee.notify( category: category, From 29dd85ebe7450a607fba9bf89c9449b24e55854d Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 26 Aug 2022 14:22:31 +1000 Subject: [PATCH 110/133] minor refactor --- SessionMessagingKit/Messages/Message.swift | 40 ++++++++++++++-------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 7b0765885..28c2b3f28 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -370,22 +370,32 @@ public extension Message { let reactors = rawReaction.reactors { // Decide whether we need to add an extra reaction from current user - let pendingChanges = associatedPendingChanges - .filter { - if case .reaction(_, let emoji, _) = $0.metadata { - return emoji == decodedEmoji + 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 } - return false - } - var shouldAddSelfReaction: Bool = rawReaction.you || reactors.contains(userPublicKey) - pendingChanges.forEach { - if case .reaction(_, _, let action) = $0.metadata { - switch action { - case .react: shouldAddSelfReaction = true - case .remove: shouldAddSelfReaction = 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 == .react) + }() + let shouldAddSelfReaction: Bool = ( + pendingChangeSelfReaction ?? + (rawReaction.you || reactors.contains(userPublicKey)) + ) let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count From a9b084f322447eda5faca33d09dace40fa2ab293 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Fri, 26 Aug 2022 14:41:03 +1000 Subject: [PATCH 111/133] update build number --- Session.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 81e1ae0a9..c9075892b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5792,7 +5792,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 373; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5865,7 +5865,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 373; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5931,7 +5931,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 373; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6005,7 +6005,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 373; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6943,7 +6943,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 373; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7015,7 +7015,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 373; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", From 6b20fbf610e7e5e51778ce8045c498a457132489 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 29 Aug 2022 09:53:09 +1000 Subject: [PATCH 112/133] add translation --- Session/Meta/Translations/de.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/en.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/es.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/fa.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/fi.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/fr.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/hi.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/hr.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/id-ID.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/it.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/ja.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/nl.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/pl.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/pt_BR.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/ru.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/si.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/sk.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/sv.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/th.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/vi-VN.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/zh-Hant.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/zh_CN.lproj/Localizable.strings | 3 ++- 22 files changed, 44 insertions(+), 22 deletions(-) diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 4650c3a01..dfdeac0d9 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 1b088606e..0fa8957e9 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 0b46eb925..49e49136f 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 14d251bd4..3ebbb2baa 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index bdb3af166..55dd7ab1c 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index ca123a3e0..79dc442ca 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index cd3797083..ebfd8600e 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 1b995f4a7..7acac8775 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other 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 ede8337f5..9368c6de0 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 2e3a2d95e..8ca104b4a 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index ccdf6c570..2e4bf2d21 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index fa3fc1332..57a701bf3 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 075b1e650..30d4cdbbb 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other 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 4ece8d754..29716af72 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index d24c568c4..1e9108ce9 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 5b35c9d28..8d15082d0 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 84f0b412d..5b2e990d4 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 463cc8ddc..e2a1b4697 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 07327949b..eb6d7dbd2 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other 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 e7ac16f63..af1bc651c 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other 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 d2fcb0c8e..7cd6e6103 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other 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 f7c393a48..19f504b05 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -702,4 +702,5 @@ "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_NOTIFICATION" = "%@ reacts to a message with %@."; +"EMOJI_REACTS_MORE_REACTORS" = "And %@ other have reacted to this message." From 5a29df56fc8eb85bac239f2d375cccd64fcdedd1 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 29 Aug 2022 10:41:43 +1000 Subject: [PATCH 113/133] update translation --- Session/Meta/Translations/de.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/en.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/es.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/fa.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/fi.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/fr.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/hi.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/hr.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/id-ID.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/it.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/ja.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/nl.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/pl.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/pt_BR.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/ru.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/si.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/sk.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/sv.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/th.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/vi-VN.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/zh-Hant.lproj/Localizable.strings | 3 ++- Session/Meta/Translations/zh_CN.lproj/Localizable.strings | 3 ++- 22 files changed, 44 insertions(+), 22 deletions(-) diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index dfdeac0d9..621d6c0a2 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 0fa8957e9..ccec585cd 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 49e49136f..886c0dae8 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 3ebbb2baa..74c25a4ea 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 55dd7ab1c..2d7e0e1a3 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 79dc442ca..150f5e259 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 ebfd8600e..e17ee5866 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 7acac8775..85b84b63f 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 9368c6de0..b18fe6b6e 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 8ca104b4a..1fea0035b 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 2e4bf2d21..9d20655e4 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 57a701bf3..9c77114bd 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 30d4cdbbb..c68a081c9 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 29716af72..56d506aa7 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 1e9108ce9..45f840c79 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 8d15082d0..a2b88a2f4 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 5b2e990d4..11ea67246 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 e2a1b4697..5ec16013d 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 eb6d7dbd2..93f11141a 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 af1bc651c..87a4d11e3 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 7cd6e6103..4e48629af 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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 19f504b05..1a7803edf 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -703,4 +703,5 @@ /* 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" = "And %@ other have reacted to this message." +"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."; From 7efc0a61c7bcde79eecfc4adea8e2a8d420b12b5 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 29 Aug 2022 13:08:31 +1000 Subject: [PATCH 114/133] update translation --- Session/Meta/Translations/de.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/en.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/es.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/fa.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/fi.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/fr.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/hi.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/hr.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/id-ID.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/it.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/ja.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/nl.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/pl.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/pt_BR.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/ru.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/si.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/sk.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/sv.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/th.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/vi-VN.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/zh-Hant.lproj/Localizable.strings | 4 ++-- Session/Meta/Translations/zh_CN.lproj/Localizable.strings | 4 ++-- 22 files changed, 44 insertions(+), 44 deletions(-) diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 621d6c0a2..415dd41d3 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 ccec585cd..64e3a9c83 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 886c0dae8..0de94349d 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 74c25a4ea..8a310885b 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 2d7e0e1a3..bea2c1277 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 150f5e259..345cbc2e6 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 e17ee5866..c9298f93c 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 85b84b63f..324a0702d 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 b18fe6b6e..27c64084d 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 1fea0035b..2ae8ccb16 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 9d20655e4..a0f7b3120 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 9c77114bd..58c2486c5 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 c68a081c9..4a809aa69 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 56d506aa7..2aacfc5d1 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 45f840c79..5d2070325 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 a2b88a2f4..12553bb60 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 11ea67246..6f6cff3ca 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 5ec16013d..d47e96467 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 93f11141a..6a9d0aae9 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 87a4d11e3..aff668e23 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 4e48629af..2125e6eea 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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 1a7803edf..0352a3a29 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -703,5 +703,5 @@ /* 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."; +"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."; From 08a52f94976f5e626726a3d4432167a902af15a3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 29 Aug 2022 13:11:45 +1000 Subject: [PATCH 115/133] Fixed an issue which could cause the DB migration to fail for users with closed groups --- .../Database/Models/Interaction.swift | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 641d33d75..0b9c64e96 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -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) } From e9d598e62a6b1f1cb10e77a3797c2e953bec526e Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 29 Aug 2022 14:04:48 +1000 Subject: [PATCH 116/133] feat: add text at the bottom saying of the reactors list indicating the number of other reactors --- .../Views & Modals/ReactionListSheet.swift | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 03e1f039a..6ad1191be 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -95,6 +95,7 @@ final class ReactionListSheet: BaseVC { 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 @@ -140,7 +141,7 @@ final class ReactionListSheet: BaseVC { private func setUpViewHierarchy() { view.addSubview(contentView) contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view) - contentView.set(.height, to: 440) + contentView.set(.height, to: 490) populateContentView() } @@ -384,10 +385,21 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.selectedReactionUserList.count + 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 footerCell: FooterCell = tableView.dequeue(type: FooterCell.self, for: indexPath) + footerCell.update( + moreReactorCount: self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count, + emoji: self.reactionSummaries[lastSelectedReactionIndex].emoji.rawValue + ) + + return footerCell + } + let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath) let cellViewModel: MessageViewModel.ReactionInfo = self.selectedReactionUserList[indexPath.row] cell.update( @@ -500,6 +512,44 @@ extension ReactionListSheet { ) } } + + 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 From c4aeecdf4bc2b8a7bbbb8cde35e2aac08e6ce6c2 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 29 Aug 2022 14:52:11 +1000 Subject: [PATCH 117/133] tweak: fully rounded corners for all message bubbles --- .../Message Cells/VisibleMessageCell.swift | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 721244882..f7b23ca19 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -900,19 +900,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // 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 { From 4a3fe4cdeb024d88463655a55ed6f8ff8ecf21db Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 29 Aug 2022 14:52:36 +1000 Subject: [PATCH 118/133] tweak: view height of reaction user list --- Session/Conversations/Views & Modals/ReactionListSheet.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 6ad1191be..c88c914b2 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -141,7 +141,9 @@ final class ReactionListSheet: BaseVC { private func setUpViewHierarchy() { view.addSubview(contentView) contentView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view) - contentView.set(.height, to: 490) + // 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() } From 31dcb3c985d12b726036978914174b60c5d8ba53 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 29 Aug 2022 15:58:49 +1000 Subject: [PATCH 119/133] feat: cache remove all for open group emoji reacts --- .../ConversationVC+Interaction.swift | 19 +++++++++++++++++-- SessionMessagingKit/Messages/Message.swift | 18 ++++++++++++++---- .../Open Groups/Models/PendingChange.swift | 10 ++++++++-- .../Open Groups/OpenGroupManager.swift | 2 +- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 4c743a856..231445b18 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1040,6 +1040,15 @@ extension ConversationVC: 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, @@ -1048,7 +1057,13 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) - .map { _ in () } + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } } .done { _ in Storage.shared.writeAsync { db in @@ -1168,7 +1183,7 @@ extension ConversationVC: id: openGroupServerMessageId, in: openGroup.roomToken, on: openGroup.server, - type: .react + type: .add ) OpenGroupAPI .reactionAdd( diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 28c2b3f28..dbd6acc86 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -369,12 +369,20 @@ public extension Message { 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) }) + .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 @@ -390,11 +398,11 @@ public extension Message { else { return nil } // Otherwise add/remove accordingly - return (action == .react) + return action == .add }() let shouldAddSelfReaction: Bool = ( pendingChangeSelfReaction ?? - (rawReaction.you || reactors.contains(userPublicKey)) + ((rawReaction.you || reactors.contains(userPublicKey)) && !pendingChangeRemoveAllReaction) ) let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count @@ -408,6 +416,8 @@ public extension Message { results = results .appending( // Add the first reaction (with the count) + pendingChangeRemoveAllReaction ? + nil : desiredReactorIds.first .map { reactor in Reaction( @@ -422,7 +432,7 @@ public extension Message { } ) .appending( // Add all other reactions - contentsOf: desiredReactorIds.count <= 1 ? + contentsOf: desiredReactorIds.count <= 1 || pendingChangeRemoveAllReaction ? [] : desiredReactorIds .suffix(from: 1) diff --git a/SessionMessagingKit/Open Groups/Models/PendingChange.swift b/SessionMessagingKit/Open Groups/Models/PendingChange.swift index 1c9c3e513..dd5af98b5 100644 --- a/SessionMessagingKit/Open Groups/Models/PendingChange.swift +++ b/SessionMessagingKit/Open Groups/Models/PendingChange.swift @@ -4,12 +4,18 @@ import Foundation extension OpenGroupAPI { public struct PendingChange: Equatable { - enum ChangeType { + public enum ChangeType { case reaction } + public enum ReactAction: Equatable { + case add + case remove + case removeAll + } + enum Metadata { - case reaction(messageId: Int64, emoji: String, action: VisibleMessage.VMReaction.Kind) + case reaction(messageId: Int64, emoji: String, action: ReactAction) } let server: String diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 4364a4dff..611fd0a94 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -764,7 +764,7 @@ public final class OpenGroupManager: NSObject { id: Int64, in roomToken: String, on server: String, - type: VisibleMessage.VMReaction.Kind, + type: OpenGroupAPI.PendingChange.ReactAction, using dependencies: OGMDependencies = OGMDependencies() ) -> OpenGroupAPI.PendingChange { let pendingChange = OpenGroupAPI.PendingChange( From fc860e395712931b635667ca6311745e53d1e227 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 30 Aug 2022 09:45:40 +1000 Subject: [PATCH 120/133] Added fixes and defensive coding for a number of crashes Fixed a crash which could occur due to multithreaded dependency access Fixed a crash which could occur if the 'keyWindow' wasn't set on the LandingVC in time since we were force-unwrapping Fixed a crash which could occur due to multithreaded swarm cache access Fixed the broken unit tests --- Session/Onboarding/LandingVC.swift | 2 +- .../Open Groups/OpenGroupManager.swift | 10 ++-- .../Sending & Receiving/Pollers/Poller.swift | 2 +- .../Utilities/SMKDependencies.swift | 54 +++++++++---------- .../Open Groups/OpenGroupAPISpec.swift | 6 ++- .../Open Groups/OpenGroupManagerSpec.swift | 22 +++++--- .../_TestUtilities/DependencyExtensions.swift | 26 ++++----- .../_TestUtilities/MockGeneralCache.swift | 5 ++ .../_TestUtilities/MockOGMCache.swift | 5 ++ .../OGMDependencyExtensions.swift | 28 +++++----- SessionSnodeKit/SnodeAPI.swift | 16 +++--- .../General/Dependencies.swift | 40 ++++++++------ 12 files changed, 123 insertions(+), 93 deletions(-) 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/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 4364a4dff..c6db9bce9 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -19,9 +19,9 @@ public protocol OGMCacheType { var hasPerformedInitialPoll: [String: Bool] { get set } var timeSinceLastPoll: [String: TimeInterval] { get set } - func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval - var pendingChanges: [OpenGroupAPI.PendingChange] { get set } + + func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval } // MARK: - OpenGroupManager @@ -1099,10 +1099,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 } @@ -1123,7 +1123,7 @@ extension OpenGroupManager { standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { - _mutableCache = cache + _mutableCache = Atomic(cache) super.init( onionApi: onionApi, diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index 02a3d139a..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 { 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 2158aa3e6..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,7 +2117,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2175,7 +2178,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2207,7 +2211,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2249,7 +2254,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: Data([1, 2, 3]).base64EncodedString(), - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ), testMessage, ], @@ -2287,7 +2293,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", @@ -2315,7 +2322,8 @@ class OpenGroupManagerSpec: QuickSpec { whisperMods: false, whisperTo: nil, base64EncodedData: nil, - base64EncodedSignature: nil + base64EncodedSignature: nil, + reactions: nil ) ], for: "testRoom", 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/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/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) } From faecb8206204b013e2d95144c3c23a8a37d0dc1c Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 30 Aug 2022 11:59:11 +1000 Subject: [PATCH 121/133] tweak: update the logic of adding local reaction entry after message sent or open group api api call success --- .../ConversationVC+Interaction.swift | 92 ++++++++++++------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 231445b18..4cd0f74e4 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1101,7 +1101,7 @@ extension ConversationVC: // Perform the sending logic Storage.shared.writeAsync( - updates: { [weak self] db in + updates: { db in guard let thread: SessionThread = try SessionThread.fetchOne(db, id: cellViewModel.threadId) else { return } @@ -1111,34 +1111,6 @@ extension ConversationVC: .filter(id: thread.id) .updateAll(db, SessionThread.Columns.shouldBeVisible.set(to: true)) - // 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 { - let sortId = Reaction.getSortId( - db, - interactionId: cellViewModel.id, - emoji: emoji - ) - try Reaction( - interactionId: cellViewModel.id, - serverHash: nil, - timestampMs: sentTimestamp, - authorId: cellViewModel.currentUserPublicKey, - emoji: emoji, - count: 1, - sortId: sortId - ).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) { @@ -1168,12 +1140,18 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) - .map { _, response in + .map { [weak self] _, response in OpenGroupManager .updatePendingChange( pendingChange, seqNo: response.seqNo ) + self?.handleReactionSent( + cellViewModel, + with: emoji, + at: sentTimestamp, + remove: remove + ) } .retainUntilComplete() } else { @@ -1193,19 +1171,25 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) - .map { _, response in + .map { [weak self] _, response in OpenGroupManager .updatePendingChange( pendingChange, seqNo: response.seqNo ) + self?.handleReactionSent( + cellViewModel, + with: emoji, + at: sentTimestamp, + remove: remove + ) } .retainUntilComplete() } } else { // Send the actual message - try MessageSender.send( + try MessageSender.sendNonDurably( db, message: VisibleMessage( sentTimestamp: UInt64(sentTimestamp), @@ -1224,13 +1208,53 @@ extension ConversationVC: ) ), interactionId: cellViewModel.id, - in: thread - ) + in: thread) + .map { [weak self] in + self?.handleReactionSent( + cellViewModel, + with: emoji, + at: sentTimestamp, + remove: remove + ) + } + .retainUntilComplete() } } ) } + private func handleReactionSent(_ cellViewModel: MessageViewModel, with emoji: String, at sentTimestamp: Int64, remove: Bool) { + Storage.shared.writeAsync { db in + // 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 { + let sortId = Reaction.getSortId( + db, + interactionId: cellViewModel.id, + emoji: emoji + ) + try Reaction( + interactionId: cellViewModel.id, + serverHash: nil, + timestampMs: sentTimestamp, + authorId: cellViewModel.currentUserPublicKey, + emoji: emoji, + count: 1, + sortId: sortId + ).insert(db) + + // Add it to the recent list + Emoji.addRecent(db, emoji: emoji) + } + } + } + func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) { hideInputAccessoryView() From eef56d47de013345f461286c883fee2dee0aa4aa Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 30 Aug 2022 12:04:44 +1000 Subject: [PATCH 122/133] update build number --- Session.xcodeproj/project.pbxproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index c9075892b..cc9d38a72 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -5792,7 +5792,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 373; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5865,7 +5865,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 373; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5931,7 +5931,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 373; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6005,7 +6005,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 373; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6943,7 +6943,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 373; + CURRENT_PROJECT_VERSION = 374; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7015,7 +7015,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 373; + CURRENT_PROJECT_VERSION = 374; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", From 82e55a8d5b04bd7c5ab3e1f5d358a681f9856e47 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 30 Aug 2022 14:58:31 +1000 Subject: [PATCH 123/133] tweak: local reaction entry logic change --- .../ConversationVC+Interaction.swift | 104 ++++++++++-------- .../Open Groups/OpenGroupManager.swift | 11 ++ .../Sending & Receiving/MessageSender.swift | 1 + 3 files changed, 71 insertions(+), 45 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 4cd0f74e4..7813c9bbd 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1111,6 +1111,43 @@ extension ConversationVC: .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 pendingReaction?.delete(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) { @@ -1140,18 +1177,21 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) - .map { [weak self] _, response in + .map { _, response in OpenGroupManager .updatePendingChange( pendingChange, seqNo: response.seqNo ) - self?.handleReactionSent( - cellViewModel, - with: emoji, - at: sentTimestamp, + } + .catch { [weak self] _ in + OpenGroupManager.removePendingChange(pendingChange) + + self?.handleReactionSentFailure( + pendingReaction, remove: remove ) + } .retainUntilComplete() } else { @@ -1171,16 +1211,18 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) - .map { [weak self] _, response in + .map { _, response in OpenGroupManager .updatePendingChange( pendingChange, seqNo: response.seqNo ) - self?.handleReactionSent( - cellViewModel, - with: emoji, - at: sentTimestamp, + } + .catch { [weak self] _ in + OpenGroupManager.removePendingChange(pendingChange) + + self?.handleReactionSentFailure( + pendingReaction, remove: remove ) } @@ -1189,7 +1231,7 @@ extension ConversationVC: } else { // Send the actual message - try MessageSender.sendNonDurably( + try MessageSender.send( db, message: VisibleMessage( sentTimestamp: UInt64(sentTimestamp), @@ -1208,49 +1250,21 @@ extension ConversationVC: ) ), interactionId: cellViewModel.id, - in: thread) - .map { [weak self] in - self?.handleReactionSent( - cellViewModel, - with: emoji, - at: sentTimestamp, - remove: remove - ) - } - .retainUntilComplete() + in: thread + ) } } ) } - private func handleReactionSent(_ cellViewModel: MessageViewModel, with emoji: String, at sentTimestamp: Int64, remove: Bool) { + func handleReactionSentFailure(_ pendingReaction: Reaction?, remove: Bool) { Storage.shared.writeAsync { db in - // Update the database + // Reverse 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) + try pendingReaction?.insert(db) } else { - let sortId = Reaction.getSortId( - db, - interactionId: cellViewModel.id, - emoji: emoji - ) - try Reaction( - interactionId: cellViewModel.id, - serverHash: nil, - timestampMs: sentTimestamp, - authorId: cellViewModel.currentUserPublicKey, - emoji: emoji, - count: 1, - sortId: sortId - ).insert(db) - - // Add it to the recent list - Emoji.addRecent(db, emoji: emoji) + try pendingReaction?.delete(db) } } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 2a23d0bf0..1a330e7aa 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -797,6 +797,17 @@ public final class OpenGroupManager: NSObject { } } + 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, diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index b4c5fcaea..cd61d1169 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -665,6 +665,7 @@ 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 } From 6a8b14b8d2c57d54c82e7099eef477594d30cd89 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Tue, 30 Aug 2022 15:26:23 +1000 Subject: [PATCH 124/133] tweak: refactor reaction deletion --- .../ConversationVC+Interaction.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 7813c9bbd..d2c150b9a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1139,7 +1139,11 @@ extension ConversationVC: // Update the database if remove { - try pendingReaction?.delete(db) + 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) @@ -1186,12 +1190,12 @@ extension ConversationVC: } .catch { [weak self] _ in OpenGroupManager.removePendingChange(pendingChange) - + self?.handleReactionSentFailure( pendingReaction, remove: remove ) - + } .retainUntilComplete() } else { @@ -1258,13 +1262,18 @@ extension ConversationVC: } 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) + try pendingReaction.insert(db) } else { - try pendingReaction?.delete(db) + try Reaction + .filter(Reaction.Columns.interactionId == pendingReaction.interactionId) + .filter(Reaction.Columns.authorId == pendingReaction.authorId) + .filter(Reaction.Columns.emoji == pendingReaction.emoji) + .deleteAll(db) } } } From 1afdfa8bcb1f2ebabe36eefc7e8cd15105c047b4 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 31 Aug 2022 12:01:21 +1000 Subject: [PATCH 125/133] Fixed a bug where the typing indicator wasn't working on the home screen --- Session/Home/HomeViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 3b20abebe..e343e3ea1 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -134,7 +134,7 @@ 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])") }() ) ], From 3e8abd195237d81cc9da778f6a18ed60f8952dea Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 31 Aug 2022 16:38:25 +1000 Subject: [PATCH 126/133] tweak: make seqNo optional --- .../Open Groups/Models/ReactionResponse.swift | 6 +++--- SessionMessagingKit/Open Groups/OpenGroupManager.swift | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift b/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift index 51df94e20..cfded186d 100644 --- a/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift @@ -13,7 +13,7 @@ extension OpenGroupAPI { public let added: Bool /// The seqNo after the reaction is added. - public let seqNo: Int64 + public let seqNo: Int64? } public struct ReactionRemoveResponse: Codable, Equatable { @@ -26,7 +26,7 @@ extension OpenGroupAPI { public let removed: Bool /// The seqNo after the reaction is removed. - public let seqNo: Int64 + public let seqNo: Int64? } public struct ReactionRemoveAllResponse: Codable, Equatable { @@ -39,6 +39,6 @@ extension OpenGroupAPI { public let removed: Int64 /// The seqNo after the reactions is all removed. - public let seqNo: Int64 + public let seqNo: Int64? } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 1a330e7aa..7afea37a0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -787,7 +787,7 @@ public final class OpenGroupManager: NSObject { public static func updatePendingChange( _ pendingChange: OpenGroupAPI.PendingChange, - seqNo: Int64, + seqNo: Int64?, using dependencies: OGMDependencies = OGMDependencies() ) { dependencies.mutableCache.mutate { From 98cf10cfea308a559766d87aa8230909f402c3a6 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 31 Aug 2022 16:57:38 +1000 Subject: [PATCH 127/133] fix crash on clicking on more reactors cell --- Session/Conversations/Views & Modals/ReactionListSheet.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index c88c914b2..3aab2bf3e 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -398,6 +398,7 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { moreReactorCount: self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count, emoji: self.reactionSummaries[lastSelectedReactionIndex].emoji.rawValue ) + footerCell.selectionStyle = .none return footerCell } @@ -421,6 +422,8 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { 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 From d5948c014242ec5378b3deced26b3769e739933b Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 31 Aug 2022 17:42:37 +1000 Subject: [PATCH 128/133] fix a crash when remove reaction and there are exactly 6 reactors --- .../Views & Modals/ReactionListSheet.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 3aab2bf3e..2de3cca58 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -326,7 +326,10 @@ final class ReactionListSheet: BaseVC { deleteRowsAnimation: .none, insertRowsAnimation: .none, reloadRowsAnimation: .none, - interrupt: { $0.changeCount > 100 } + interrupt: { + $0.elementInserted.count == 1 && self.selectedReactionUserList.count == 4 || + $0.changeCount > 100 + } ) { [weak self] updatedData in self?.selectedReactionUserList = updatedData } @@ -387,18 +390,19 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat 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 + return self.selectedReactionUserList.count + 1 } 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: self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count, + moreReactorCount: moreReactorCount, emoji: self.reactionSummaries[lastSelectedReactionIndex].emoji.rawValue ) footerCell.selectionStyle = .none + footerCell.isHidden = (moreReactorCount <= 0) return footerCell } From e6e97684b4cd81291c191d9ed90b33ad59ff1304 Mon Sep 17 00:00:00 2001 From: ryanzhao Date: Wed, 31 Aug 2022 17:50:16 +1000 Subject: [PATCH 129/133] fix 6 reactors case in user list --- Session/Conversations/Views & Modals/ReactionListSheet.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 2de3cca58..b5c3f97d2 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -328,6 +328,7 @@ final class ReactionListSheet: BaseVC { reloadRowsAnimation: .none, interrupt: { $0.elementInserted.count == 1 && self.selectedReactionUserList.count == 4 || + $0.elementDeleted.count == 1 && self.selectedReactionUserList.count == 6 || $0.changeCount > 100 } ) { [weak self] updatedData in From ed3dcf76342253e8faf80c175cd59d347b310c6c Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 1 Sep 2022 10:51:40 +1000 Subject: [PATCH 130/133] add comments and minor refactor back to previous logic --- .../Views & Modals/ReactionListSheet.swift | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index b5c3f97d2..8cc1f29d0 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -326,10 +326,18 @@ final class ReactionListSheet: BaseVC { deleteRowsAnimation: .none, insertRowsAnimation: .none, reloadRowsAnimation: .none, - interrupt: { - $0.elementInserted.count == 1 && self.selectedReactionUserList.count == 4 || - $0.elementDeleted.count == 1 && self.selectedReactionUserList.count == 6 || - $0.changeCount > 100 + 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 @@ -391,7 +399,8 @@ extension ReactionListSheet: UICollectionViewDataSource, UICollectionViewDelegat extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.selectedReactionUserList.count + 1 + 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 { @@ -403,7 +412,6 @@ extension ReactionListSheet: UITableViewDelegate, UITableViewDataSource { emoji: self.reactionSummaries[lastSelectedReactionIndex].emoji.rawValue ) footerCell.selectionStyle = .none - footerCell.isHidden = (moreReactorCount <= 0) return footerCell } From e5a46a993d110e3030efb30511dffdab136c7558 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 1 Sep 2022 11:23:50 +1000 Subject: [PATCH 131/133] update emoji names for searching --- Scripts/EmojiGenerator.swift | 9 +- .../Emoji Picker/EmojiPickerSheet.swift | 1 + Session/Emoji/Emoji+Name.swift | 3708 ++++++++--------- 3 files changed, 1862 insertions(+), 1856 deletions(-) diff --git a/Scripts/EmojiGenerator.swift b/Scripts/EmojiGenerator.swift index 35d2b8ae8..44f906cc1 100755 --- a/Scripts/EmojiGenerator.swift +++ b/Scripts/EmojiGenerator.swift @@ -23,6 +23,7 @@ enum RemoteModel { let sortOrder: UInt let category: EmojiCategory let skinVariations: [String: SkinVariation]? + let shortNames: [String]? } struct SkinVariation: Codable { @@ -64,6 +65,7 @@ struct EmojiModel { let category: RemoteModel.EmojiCategory let rawName: String let enumName: String + var shortNames: Set let variants: [Emoji] var baseEmoji: Character { variants[0].base } @@ -91,7 +93,10 @@ struct EmojiModel { 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) @@ -509,7 +514,7 @@ extension EmojiGenerator { fileHandle.indent { fileHandle.writeLine("switch self {") emojiModel.definitions.forEach { - fileHandle.writeLine("case .\($0.enumName): return \"\($0.rawName)\"") + fileHandle.writeLine("case .\($0.enumName): return \"\($0.shortNames.joined(separator:", "))\"") } fileHandle.writeLine("}") } diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift index e4c84d568..621d66940 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -24,6 +24,7 @@ class EmojiPickerSheet: BaseVC { result.tintColor = Colors.text result.backgroundColor = .clear result.delegate = self + result.showsCancelButton = true return result }() diff --git a/Session/Emoji/Emoji+Name.swift b/Session/Emoji/Emoji+Name.swift index 8418c2c4a..6acfa85e2 100644 --- a/Session/Emoji/Emoji+Name.swift +++ b/Session/Emoji/Emoji+Name.swift @@ -4,1860 +4,1860 @@ extension Emoji { var name: String { switch self { - case .grinning: return "GRINNING FACE" - case .smiley: return "SMILING FACE WITH OPEN MOUTH" - case .smile: return "SMILING FACE WITH OPEN MOUTH AND SMILING EYES" - case .grin: return "GRINNING FACE WITH SMILING EYES" - case .laughing: return "SMILING FACE WITH OPEN MOUTH AND TIGHTLY-CLOSED EYES" - case .sweatSmile: return "SMILING FACE WITH OPEN MOUTH AND COLD SWEAT" - case .rollingOnTheFloorLaughing: return "ROLLING ON THE FLOOR LAUGHING" - case .joy: return "FACE WITH TEARS OF JOY" - case .slightlySmilingFace: return "SLIGHTLY SMILING FACE" - case .upsideDownFace: return "UPSIDE-DOWN FACE" - case .meltingFace: return "MELTING FACE" - case .wink: return "WINKING FACE" - case .blush: return "SMILING FACE WITH SMILING EYES" - case .innocent: return "SMILING FACE WITH HALO" - case .smilingFaceWith3Hearts: return "SMILING FACE WITH SMILING EYES AND THREE HEARTS" - case .heartEyes: return "SMILING FACE WITH HEART-SHAPED EYES" - case .starStruck: return "GRINNING FACE WITH STAR EYES" - case .kissingHeart: return "FACE THROWING A KISS" - case .kissing: return "KISSING FACE" - case .relaxed: return "WHITE SMILING FACE" - case .kissingClosedEyes: return "KISSING FACE WITH CLOSED EYES" - case .kissingSmilingEyes: return "KISSING FACE WITH SMILING EYES" - case .smilingFaceWithTear: return "SMILING FACE WITH TEAR" - case .yum: return "FACE SAVOURING DELICIOUS FOOD" - case .stuckOutTongue: return "FACE WITH STUCK-OUT TONGUE" - case .stuckOutTongueWinkingEye: return "FACE WITH STUCK-OUT TONGUE AND WINKING EYE" - case .zanyFace: return "GRINNING FACE WITH ONE LARGE AND ONE SMALL EYE" - case .stuckOutTongueClosedEyes: return "FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES" - case .moneyMouthFace: return "MONEY-MOUTH FACE" - case .huggingFace: return "HUGGING FACE" - case .faceWithHandOverMouth: return "SMILING FACE WITH SMILING EYES AND HAND COVERING MOUTH" - case .faceWithOpenEyesAndHandOverMouth: return "FACE WITH OPEN EYES AND HAND OVER MOUTH" - case .faceWithPeekingEye: return "FACE WITH PEEKING EYE" - case .shushingFace: return "FACE WITH FINGER COVERING CLOSED LIPS" - case .thinkingFace: return "THINKING FACE" - case .salutingFace: return "SALUTING FACE" - case .zipperMouthFace: return "ZIPPER-MOUTH FACE" - case .faceWithRaisedEyebrow: return "FACE WITH ONE EYEBROW RAISED" - case .neutralFace: return "NEUTRAL FACE" - case .expressionless: return "EXPRESSIONLESS FACE" - case .noMouth: return "FACE WITHOUT MOUTH" - case .dottedLineFace: return "DOTTED LINE FACE" - case .faceInClouds: return "FACE IN CLOUDS" - case .smirk: return "SMIRKING FACE" - case .unamused: return "UNAMUSED FACE" - case .faceWithRollingEyes: return "FACE WITH ROLLING EYES" - case .grimacing: return "GRIMACING FACE" - case .faceExhaling: return "FACE EXHALING" - case .lyingFace: return "LYING FACE" - case .relieved: return "RELIEVED FACE" - case .pensive: return "PENSIVE FACE" - case .sleepy: return "SLEEPY FACE" - case .droolingFace: return "DROOLING FACE" - case .sleeping: return "SLEEPING FACE" - case .mask: return "FACE WITH MEDICAL MASK" - case .faceWithThermometer: return "FACE WITH THERMOMETER" - case .faceWithHeadBandage: return "FACE WITH HEAD-BANDAGE" - case .nauseatedFace: return "NAUSEATED FACE" - case .faceVomiting: return "FACE WITH OPEN MOUTH VOMITING" - case .sneezingFace: return "SNEEZING FACE" - case .hotFace: return "OVERHEATED FACE" - case .coldFace: return "FREEZING FACE" - case .woozyFace: return "FACE WITH UNEVEN EYES AND WAVY MOUTH" - case .dizzyFace: return "DIZZY FACE" - case .faceWithSpiralEyes: return "FACE WITH SPIRAL EYES" - case .explodingHead: return "SHOCKED FACE WITH EXPLODING HEAD" - case .faceWithCowboyHat: return "FACE WITH COWBOY HAT" - case .partyingFace: return "FACE WITH PARTY HORN AND PARTY HAT" - case .disguisedFace: return "DISGUISED FACE" - case .sunglasses: return "SMILING FACE WITH SUNGLASSES" - case .nerdFace: return "NERD FACE" - case .faceWithMonocle: return "FACE WITH MONOCLE" - case .confused: return "CONFUSED FACE" - case .faceWithDiagonalMouth: return "FACE WITH DIAGONAL MOUTH" - case .worried: return "WORRIED FACE" - case .slightlyFrowningFace: return "SLIGHTLY FROWNING FACE" - case .whiteFrowningFace: return "FROWNING FACE" - case .openMouth: return "FACE WITH OPEN MOUTH" - case .hushed: return "HUSHED FACE" - case .astonished: return "ASTONISHED FACE" - case .flushed: return "FLUSHED FACE" - case .pleadingFace: return "FACE WITH PLEADING EYES" - case .faceHoldingBackTears: return "FACE HOLDING BACK TEARS" - case .frowning: return "FROWNING FACE WITH OPEN MOUTH" - case .anguished: return "ANGUISHED FACE" - case .fearful: return "FEARFUL FACE" - case .coldSweat: return "FACE WITH OPEN MOUTH AND COLD SWEAT" - case .disappointedRelieved: return "DISAPPOINTED BUT RELIEVED FACE" - case .cry: return "CRYING FACE" - case .sob: return "LOUDLY CRYING FACE" - case .scream: return "FACE SCREAMING IN FEAR" - case .confounded: return "CONFOUNDED FACE" - case .persevere: return "PERSEVERING FACE" - case .disappointed: return "DISAPPOINTED FACE" - case .sweat: return "FACE WITH COLD SWEAT" - case .weary: return "WEARY FACE" - case .tiredFace: return "TIRED FACE" - case .yawningFace: return "YAWNING FACE" - case .triumph: return "FACE WITH LOOK OF TRIUMPH" - case .rage: return "POUTING FACE" - case .angry: return "ANGRY FACE" - case .faceWithSymbolsOnMouth: return "SERIOUS FACE WITH SYMBOLS COVERING MOUTH" - case .smilingImp: return "SMILING FACE WITH HORNS" - case .imp: return "IMP" - case .skull: return "SKULL" - case .skullAndCrossbones: return "SKULL AND CROSSBONES" - case .hankey: return "PILE OF POO" - case .clownFace: return "CLOWN FACE" - case .japaneseOgre: return "JAPANESE OGRE" - case .japaneseGoblin: return "JAPANESE GOBLIN" - case .ghost: return "GHOST" - case .alien: return "EXTRATERRESTRIAL ALIEN" - case .spaceInvader: return "ALIEN MONSTER" - case .robotFace: return "ROBOT FACE" - case .smileyCat: return "SMILING CAT FACE WITH OPEN MOUTH" - case .smileCat: return "GRINNING CAT FACE WITH SMILING EYES" - case .joyCat: return "CAT FACE WITH TEARS OF JOY" - case .heartEyesCat: return "SMILING CAT FACE WITH HEART-SHAPED EYES" - case .smirkCat: return "CAT FACE WITH WRY SMILE" - case .kissingCat: return "KISSING CAT FACE WITH CLOSED EYES" - case .screamCat: return "WEARY CAT FACE" - case .cryingCatFace: return "CRYING CAT FACE" - case .poutingCat: return "POUTING CAT FACE" - case .seeNoEvil: return "SEE-NO-EVIL MONKEY" - case .hearNoEvil: return "HEAR-NO-EVIL MONKEY" - case .speakNoEvil: return "SPEAK-NO-EVIL MONKEY" - case .kiss: return "KISS MARK" - case .loveLetter: return "LOVE LETTER" - case .cupid: return "HEART WITH ARROW" - case .giftHeart: return "HEART WITH RIBBON" - case .sparklingHeart: return "SPARKLING HEART" - case .heartpulse: return "GROWING HEART" - case .heartbeat: return "BEATING HEART" - case .revolvingHearts: return "REVOLVING HEARTS" - case .twoHearts: return "TWO HEARTS" - case .heartDecoration: return "HEART DECORATION" - case .heavyHeartExclamationMarkOrnament: return "HEART EXCLAMATION" - case .brokenHeart: return "BROKEN HEART" - case .heartOnFire: return "HEART ON FIRE" - case .mendingHeart: return "MENDING HEART" - case .heart: return "HEAVY BLACK HEART" - case .orangeHeart: return "ORANGE HEART" - case .yellowHeart: return "YELLOW HEART" - case .greenHeart: return "GREEN HEART" - case .blueHeart: return "BLUE HEART" - case .purpleHeart: return "PURPLE HEART" - case .brownHeart: return "BROWN HEART" - case .blackHeart: return "BLACK HEART" - case .whiteHeart: return "WHITE HEART" - case .oneHundred: return "HUNDRED POINTS SYMBOL" - case .anger: return "ANGER SYMBOL" - case .boom: return "COLLISION SYMBOL" - case .dizzy: return "DIZZY SYMBOL" - case .sweatDrops: return "SPLASHING SWEAT SYMBOL" - case .dash: return "DASH SYMBOL" - case .hole: return "HOLE" - case .bomb: return "BOMB" - case .speechBalloon: return "SPEECH BALLOON" - case .eyeInSpeechBubble: return "EYE IN SPEECH BUBBLE" - case .leftSpeechBubble: return "LEFT SPEECH BUBBLE" - case .rightAngerBubble: return "RIGHT ANGER BUBBLE" - case .thoughtBalloon: return "THOUGHT BALLOON" - case .zzz: return "SLEEPING SYMBOL" - case .wave: return "WAVING HAND SIGN" - case .raisedBackOfHand: return "RAISED BACK OF HAND" - case .raisedHandWithFingersSplayed: return "HAND WITH FINGERS SPLAYED" - case .hand: return "RAISED HAND" - case .spockHand: return "RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS" - case .rightwardsHand: return "RIGHTWARDS HAND" - case .leftwardsHand: return "LEFTWARDS HAND" - case .palmDownHand: return "PALM DOWN HAND" - case .palmUpHand: return "PALM UP HAND" - case .okHand: return "OK HAND SIGN" - case .pinchedFingers: return "PINCHED FINGERS" - case .pinchingHand: return "PINCHING HAND" - case .v: return "VICTORY HAND" - case .crossedFingers: return "HAND WITH INDEX AND MIDDLE FINGERS CROSSED" - case .handWithIndexFingerAndThumbCrossed: return "HAND WITH INDEX FINGER AND THUMB CROSSED" - case .iLoveYouHandSign: return "I LOVE YOU HAND SIGN" - case .theHorns: return "SIGN OF THE HORNS" - case .callMeHand: return "CALL ME HAND" - case .pointLeft: return "WHITE LEFT POINTING BACKHAND INDEX" - case .pointRight: return "WHITE RIGHT POINTING BACKHAND INDEX" - case .pointUp2: return "WHITE UP POINTING BACKHAND INDEX" - case .middleFinger: return "REVERSED HAND WITH MIDDLE FINGER EXTENDED" - case .pointDown: return "WHITE DOWN POINTING BACKHAND INDEX" - case .pointUp: return "WHITE UP POINTING INDEX" - case .indexPointingAtTheViewer: return "INDEX POINTING AT THE VIEWER" - case .plusOne: return "THUMBS UP SIGN" - case .negativeOne: return "THUMBS DOWN SIGN" - case .fist: return "RAISED FIST" - case .facepunch: return "FISTED HAND SIGN" - case .leftFacingFist: return "LEFT-FACING FIST" - case .rightFacingFist: return "RIGHT-FACING FIST" - case .clap: return "CLAPPING HANDS SIGN" - case .raisedHands: return "PERSON RAISING BOTH HANDS IN CELEBRATION" - case .heartHands: return "HEART HANDS" - case .openHands: return "OPEN HANDS SIGN" - case .palmsUpTogether: return "PALMS UP TOGETHER" - case .handshake: return "HANDSHAKE" - case .pray: return "PERSON WITH FOLDED HANDS" - case .writingHand: return "WRITING HAND" - case .nailCare: return "NAIL POLISH" - case .selfie: return "SELFIE" - case .muscle: return "FLEXED BICEPS" - case .mechanicalArm: return "MECHANICAL ARM" - case .mechanicalLeg: return "MECHANICAL LEG" - case .leg: return "LEG" - case .foot: return "FOOT" - case .ear: return "EAR" - case .earWithHearingAid: return "EAR WITH HEARING AID" - case .nose: return "NOSE" - case .brain: return "BRAIN" - case .anatomicalHeart: return "ANATOMICAL HEART" - 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" - case .bitingLip: return "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" - case .man: return "MAN" - case .beardedPerson: return "BEARDED PERSON" - case .manWithBeard: return "MAN: BEARD" - case .womanWithBeard: return "WOMAN: BEARD" - case .redHairedMan: return "MAN: RED HAIR" - case .curlyHairedMan: return "MAN: CURLY HAIR" - case .whiteHairedMan: return "MAN: WHITE HAIR" - case .baldMan: return "MAN: BALD" - case .woman: return "WOMAN" - case .redHairedWoman: return "WOMAN: RED HAIR" - case .redHairedPerson: return "PERSON: RED HAIR" - case .curlyHairedWoman: return "WOMAN: CURLY HAIR" - case .curlyHairedPerson: return "PERSON: CURLY HAIR" - case .whiteHairedWoman: return "WOMAN: WHITE HAIR" - case .whiteHairedPerson: return "PERSON: WHITE HAIR" - case .baldWoman: return "WOMAN: BALD" - case .baldPerson: return "PERSON: BALD" - case .blondHairedWoman: return "WOMAN: BLOND HAIR" - case .blondHairedMan: return "MAN: BLOND HAIR" - case .olderAdult: return "OLDER ADULT" - case .olderMan: return "OLDER MAN" - case .olderWoman: return "OLDER WOMAN" - case .personFrowning: return "PERSON FROWNING" - case .manFrowning: return "MAN FROWNING" - case .womanFrowning: return "WOMAN FROWNING" - case .personWithPoutingFace: return "PERSON WITH POUTING FACE" - case .manPouting: return "MAN POUTING" - case .womanPouting: return "WOMAN POUTING" - case .noGood: return "FACE WITH NO GOOD GESTURE" - case .manGesturingNo: return "MAN GESTURING NO" - case .womanGesturingNo: return "WOMAN GESTURING NO" - case .okWoman: return "FACE WITH OK GESTURE" - case .manGesturingOk: return "MAN GESTURING OK" - case .womanGesturingOk: return "WOMAN GESTURING OK" - case .informationDeskPerson: return "INFORMATION DESK PERSON" - case .manTippingHand: return "MAN TIPPING HAND" - case .womanTippingHand: return "WOMAN TIPPING HAND" - case .raisingHand: return "HAPPY PERSON RAISING ONE HAND" - case .manRaisingHand: return "MAN RAISING HAND" - case .womanRaisingHand: return "WOMAN RAISING HAND" - case .deafPerson: return "DEAF PERSON" - case .deafMan: return "DEAF MAN" - case .deafWoman: return "DEAF WOMAN" - case .bow: return "PERSON BOWING DEEPLY" - case .manBowing: return "MAN BOWING" - case .womanBowing: return "WOMAN BOWING" - case .facePalm: return "FACE PALM" - case .manFacepalming: return "MAN FACEPALMING" - case .womanFacepalming: return "WOMAN FACEPALMING" - case .shrug: return "SHRUG" - case .manShrugging: return "MAN SHRUGGING" - case .womanShrugging: return "WOMAN SHRUGGING" - case .healthWorker: return "HEALTH WORKER" - case .maleDoctor: return "MAN HEALTH WORKER" - case .femaleDoctor: return "WOMAN HEALTH WORKER" - case .student: return "STUDENT" - case .maleStudent: return "MAN STUDENT" - case .femaleStudent: return "WOMAN STUDENT" - case .teacher: return "TEACHER" - case .maleTeacher: return "MAN TEACHER" - case .femaleTeacher: return "WOMAN TEACHER" - case .judge: return "JUDGE" - case .maleJudge: return "MAN JUDGE" - case .femaleJudge: return "WOMAN JUDGE" - case .farmer: return "FARMER" - case .maleFarmer: return "MAN FARMER" - case .femaleFarmer: return "WOMAN FARMER" - case .cook: return "COOK" - case .maleCook: return "MAN COOK" - case .femaleCook: return "WOMAN COOK" - case .mechanic: return "MECHANIC" - case .maleMechanic: return "MAN MECHANIC" - case .femaleMechanic: return "WOMAN MECHANIC" - case .factoryWorker: return "FACTORY WORKER" - case .maleFactoryWorker: return "MAN FACTORY WORKER" - case .femaleFactoryWorker: return "WOMAN FACTORY WORKER" - case .officeWorker: return "OFFICE WORKER" - case .maleOfficeWorker: return "MAN OFFICE WORKER" - case .femaleOfficeWorker: return "WOMAN OFFICE WORKER" - case .scientist: return "SCIENTIST" - case .maleScientist: return "MAN SCIENTIST" - case .femaleScientist: return "WOMAN SCIENTIST" - case .technologist: return "TECHNOLOGIST" - case .maleTechnologist: return "MAN TECHNOLOGIST" - case .femaleTechnologist: return "WOMAN TECHNOLOGIST" - case .singer: return "SINGER" - case .maleSinger: return "MAN SINGER" - case .femaleSinger: return "WOMAN SINGER" - case .artist: return "ARTIST" - case .maleArtist: return "MAN ARTIST" - case .femaleArtist: return "WOMAN ARTIST" - case .pilot: return "PILOT" - case .malePilot: return "MAN PILOT" - case .femalePilot: return "WOMAN PILOT" - case .astronaut: return "ASTRONAUT" - case .maleAstronaut: return "MAN ASTRONAUT" - case .femaleAstronaut: return "WOMAN ASTRONAUT" - case .firefighter: return "FIREFIGHTER" - case .maleFirefighter: return "MAN FIREFIGHTER" - case .femaleFirefighter: return "WOMAN FIREFIGHTER" - case .cop: return "POLICE OFFICER" - case .malePoliceOfficer: return "MAN POLICE OFFICER" - case .femalePoliceOfficer: return "WOMAN POLICE OFFICER" - case .sleuthOrSpy: return "DETECTIVE" - case .maleDetective: return "MAN DETECTIVE" - case .femaleDetective: return "WOMAN DETECTIVE" - case .guardsman: return "GUARDSMAN" - case .maleGuard: return "MAN GUARD" - case .femaleGuard: return "WOMAN GUARD" - case .ninja: return "NINJA" - case .constructionWorker: return "CONSTRUCTION WORKER" - case .maleConstructionWorker: return "MAN CONSTRUCTION WORKER" - case .femaleConstructionWorker: return "WOMAN CONSTRUCTION WORKER" - case .personWithCrown: return "PERSON WITH CROWN" - case .prince: return "PRINCE" - case .princess: return "PRINCESS" - case .manWithTurban: return "MAN WITH TURBAN" - case .manWearingTurban: return "MAN WEARING TURBAN" - case .womanWearingTurban: return "WOMAN WEARING TURBAN" - case .manWithGuaPiMao: return "MAN WITH GUA PI MAO" - case .personWithHeadscarf: return "PERSON WITH HEADSCARF" - case .personInTuxedo: return "MAN IN TUXEDO" - case .manInTuxedo: return "MAN IN TUXEDO" - case .womanInTuxedo: return "WOMAN IN TUXEDO" - case .brideWithVeil: return "BRIDE WITH VEIL" - case .manWithVeil: return "MAN WITH VEIL" - case .womanWithVeil: return "WOMAN WITH VEIL" - case .pregnantWoman: return "PREGNANT WOMAN" - case .pregnantMan: return "PREGNANT MAN" - case .pregnantPerson: return "PREGNANT PERSON" - case .breastFeeding: return "BREAST-FEEDING" - case .womanFeedingBaby: return "WOMAN FEEDING BABY" - case .manFeedingBaby: return "MAN FEEDING BABY" - case .personFeedingBaby: return "PERSON FEEDING BABY" - case .angel: return "BABY ANGEL" - case .santa: return "FATHER CHRISTMAS" - case .mrsClaus: return "MOTHER CHRISTMAS" - case .mxClaus: return "MX CLAUS" - case .superhero: return "SUPERHERO" - case .maleSuperhero: return "MAN SUPERHERO" - case .femaleSuperhero: return "WOMAN SUPERHERO" - case .supervillain: return "SUPERVILLAIN" - case .maleSupervillain: return "MAN SUPERVILLAIN" - case .femaleSupervillain: return "WOMAN SUPERVILLAIN" - case .mage: return "MAGE" - case .maleMage: return "MAN MAGE" - case .femaleMage: return "WOMAN MAGE" - case .fairy: return "FAIRY" - case .maleFairy: return "MAN FAIRY" - case .femaleFairy: return "WOMAN FAIRY" - case .vampire: return "VAMPIRE" - case .maleVampire: return "MAN VAMPIRE" - case .femaleVampire: return "WOMAN VAMPIRE" - case .merperson: return "MERPERSON" - case .merman: return "MERMAN" - case .mermaid: return "MERMAID" - case .elf: return "ELF" - case .maleElf: return "MAN ELF" - case .femaleElf: return "WOMAN ELF" - case .genie: return "GENIE" - case .maleGenie: return "MAN GENIE" - case .femaleGenie: return "WOMAN GENIE" - case .zombie: return "ZOMBIE" - case .maleZombie: return "MAN ZOMBIE" - case .femaleZombie: return "WOMAN ZOMBIE" - case .troll: return "TROLL" - case .massage: return "FACE MASSAGE" - case .manGettingMassage: return "MAN GETTING MASSAGE" - case .womanGettingMassage: return "WOMAN GETTING MASSAGE" - case .haircut: return "HAIRCUT" - case .manGettingHaircut: return "MAN GETTING HAIRCUT" - case .womanGettingHaircut: return "WOMAN GETTING HAIRCUT" - case .walking: return "PEDESTRIAN" - case .manWalking: return "MAN WALKING" - case .womanWalking: return "WOMAN WALKING" - case .standingPerson: return "STANDING PERSON" - case .manStanding: return "MAN STANDING" - case .womanStanding: return "WOMAN STANDING" - case .kneelingPerson: return "KNEELING PERSON" - case .manKneeling: return "MAN KNEELING" - case .womanKneeling: return "WOMAN KNEELING" - case .personWithProbingCane: return "PERSON WITH WHITE CANE" - case .manWithProbingCane: return "MAN WITH WHITE CANE" - case .womanWithProbingCane: return "WOMAN WITH WHITE CANE" - case .personInMotorizedWheelchair: return "PERSON IN MOTORIZED WHEELCHAIR" - case .manInMotorizedWheelchair: return "MAN IN MOTORIZED WHEELCHAIR" - case .womanInMotorizedWheelchair: return "WOMAN IN MOTORIZED WHEELCHAIR" - case .personInManualWheelchair: return "PERSON IN MANUAL WHEELCHAIR" - case .manInManualWheelchair: return "MAN IN MANUAL WHEELCHAIR" - case .womanInManualWheelchair: return "WOMAN IN MANUAL WHEELCHAIR" - case .runner: return "RUNNER" - case .manRunning: return "MAN RUNNING" - case .womanRunning: return "WOMAN RUNNING" - case .dancer: return "DANCER" - case .manDancing: return "MAN DANCING" - case .manInBusinessSuitLevitating: return "PERSON IN SUIT LEVITATING" - case .dancers: return "WOMAN WITH BUNNY EARS" - case .menWithBunnyEarsPartying: return "MEN WITH BUNNY EARS" - case .womenWithBunnyEarsPartying: return "WOMEN WITH BUNNY EARS" - case .personInSteamyRoom: return "PERSON IN STEAMY ROOM" - case .manInSteamyRoom: return "MAN IN STEAMY ROOM" - case .womanInSteamyRoom: return "WOMAN IN STEAMY ROOM" - case .personClimbing: return "PERSON CLIMBING" - case .manClimbing: return "MAN CLIMBING" - case .womanClimbing: return "WOMAN CLIMBING" - case .fencer: return "FENCER" - case .horseRacing: return "HORSE RACING" - case .skier: return "SKIER" - case .snowboarder: return "SNOWBOARDER" - case .golfer: return "PERSON GOLFING" - case .manGolfing: return "MAN GOLFING" - case .womanGolfing: return "WOMAN GOLFING" - case .surfer: return "SURFER" - case .manSurfing: return "MAN SURFING" - case .womanSurfing: return "WOMAN SURFING" - case .rowboat: return "ROWBOAT" - case .manRowingBoat: return "MAN ROWING BOAT" - case .womanRowingBoat: return "WOMAN ROWING BOAT" - case .swimmer: return "SWIMMER" - case .manSwimming: return "MAN SWIMMING" - case .womanSwimming: return "WOMAN SWIMMING" - case .personWithBall: return "PERSON BOUNCING BALL" - case .manBouncingBall: return "MAN BOUNCING BALL" - case .womanBouncingBall: return "WOMAN BOUNCING BALL" - case .weightLifter: return "PERSON LIFTING WEIGHTS" - case .manLiftingWeights: return "MAN LIFTING WEIGHTS" - case .womanLiftingWeights: return "WOMAN LIFTING WEIGHTS" - case .bicyclist: return "BICYCLIST" - case .manBiking: return "MAN BIKING" - case .womanBiking: return "WOMAN BIKING" - case .mountainBicyclist: return "MOUNTAIN BICYCLIST" - case .manMountainBiking: return "MAN MOUNTAIN BIKING" - case .womanMountainBiking: return "WOMAN MOUNTAIN BIKING" - case .personDoingCartwheel: return "PERSON DOING CARTWHEEL" - case .manCartwheeling: return "MAN CARTWHEELING" - case .womanCartwheeling: return "WOMAN CARTWHEELING" - case .wrestlers: return "WRESTLERS" - case .manWrestling: return "MEN WRESTLING" - case .womanWrestling: return "WOMEN WRESTLING" - case .waterPolo: return "WATER POLO" - case .manPlayingWaterPolo: return "MAN PLAYING WATER POLO" - case .womanPlayingWaterPolo: return "WOMAN PLAYING WATER POLO" - case .handball: return "HANDBALL" - case .manPlayingHandball: return "MAN PLAYING HANDBALL" - case .womanPlayingHandball: return "WOMAN PLAYING HANDBALL" - case .juggling: return "JUGGLING" - case .manJuggling: return "MAN JUGGLING" - case .womanJuggling: return "WOMAN JUGGLING" - case .personInLotusPosition: return "PERSON IN LOTUS POSITION" - case .manInLotusPosition: return "MAN IN LOTUS POSITION" - case .womanInLotusPosition: return "WOMAN IN LOTUS POSITION" - case .bath: return "BATH" - case .sleepingAccommodation: return "SLEEPING ACCOMMODATION" - case .peopleHoldingHands: return "PEOPLE HOLDING HANDS" - case .twoWomenHoldingHands: return "TWO WOMEN HOLDING HANDS" - case .manAndWomanHoldingHands: return "MAN AND WOMAN HOLDING HANDS" - case .twoMenHoldingHands: return "TWO MEN HOLDING HANDS" - case .personKissPerson: return "KISS" - case .womanKissMan: return "KISS: WOMAN, MAN" - case .manKissMan: return "KISS: MAN, MAN" - case .womanKissWoman: return "KISS: WOMAN, WOMAN" - case .personHeartPerson: return "COUPLE WITH HEART" - case .womanHeartMan: return "COUPLE WITH HEART: WOMAN, MAN" - case .manHeartMan: return "COUPLE WITH HEART: MAN, MAN" - case .womanHeartWoman: return "COUPLE WITH HEART: WOMAN, WOMAN" - case .family: return "FAMILY" - case .manWomanBoy: return "FAMILY: MAN, WOMAN, BOY" - case .manWomanGirl: return "FAMILY: MAN, WOMAN, GIRL" - case .manWomanGirlBoy: return "FAMILY: MAN, WOMAN, GIRL, BOY" - case .manWomanBoyBoy: return "FAMILY: MAN, WOMAN, BOY, BOY" - case .manWomanGirlGirl: return "FAMILY: MAN, WOMAN, GIRL, GIRL" - case .manManBoy: return "FAMILY: MAN, MAN, BOY" - case .manManGirl: return "FAMILY: MAN, MAN, GIRL" - case .manManGirlBoy: return "FAMILY: MAN, MAN, GIRL, BOY" - case .manManBoyBoy: return "FAMILY: MAN, MAN, BOY, BOY" - case .manManGirlGirl: return "FAMILY: MAN, MAN, GIRL, GIRL" - case .womanWomanBoy: return "FAMILY: WOMAN, WOMAN, BOY" - case .womanWomanGirl: return "FAMILY: WOMAN, WOMAN, GIRL" - case .womanWomanGirlBoy: return "FAMILY: WOMAN, WOMAN, GIRL, BOY" - case .womanWomanBoyBoy: return "FAMILY: WOMAN, WOMAN, BOY, BOY" - case .womanWomanGirlGirl: return "FAMILY: WOMAN, WOMAN, GIRL, GIRL" - case .manBoy: return "FAMILY: MAN, BOY" - case .manBoyBoy: return "FAMILY: MAN, BOY, BOY" - case .manGirl: return "FAMILY: MAN, GIRL" - case .manGirlBoy: return "FAMILY: MAN, GIRL, BOY" - case .manGirlGirl: return "FAMILY: MAN, GIRL, GIRL" - case .womanBoy: return "FAMILY: WOMAN, BOY" - case .womanBoyBoy: return "FAMILY: WOMAN, BOY, BOY" - case .womanGirl: return "FAMILY: WOMAN, GIRL" - case .womanGirlBoy: return "FAMILY: WOMAN, GIRL, BOY" - case .womanGirlGirl: return "FAMILY: WOMAN, GIRL, GIRL" - case .speakingHeadInSilhouette: return "SPEAKING HEAD" - case .bustInSilhouette: return "BUST IN SILHOUETTE" - case .bustsInSilhouette: return "BUSTS IN SILHOUETTE" - case .peopleHugging: return "PEOPLE HUGGING" - case .footprints: return "FOOTPRINTS" - case .skinTone2: return "EMOJI MODIFIER FITZPATRICK TYPE-1-2" - case .skinTone3: return "EMOJI MODIFIER FITZPATRICK TYPE-3" - case .skinTone4: return "EMOJI MODIFIER FITZPATRICK TYPE-4" - case .skinTone5: return "EMOJI MODIFIER FITZPATRICK TYPE-5" - case .skinTone6: return "EMOJI MODIFIER FITZPATRICK TYPE-6" - case .monkeyFace: return "MONKEY FACE" - case .monkey: return "MONKEY" - case .gorilla: return "GORILLA" - case .orangutan: return "ORANGUTAN" - case .dog: return "DOG FACE" - case .dog2: return "DOG" - case .guideDog: return "GUIDE DOG" - case .serviceDog: return "SERVICE DOG" - case .poodle: return "POODLE" - case .wolf: return "WOLF FACE" - case .foxFace: return "FOX FACE" - case .raccoon: return "RACCOON" - case .cat: return "CAT FACE" - case .cat2: return "CAT" - case .blackCat: return "BLACK CAT" - case .lionFace: return "LION FACE" - case .tiger: return "TIGER FACE" - case .tiger2: return "TIGER" - case .leopard: return "LEOPARD" - case .horse: return "HORSE FACE" - case .racehorse: return "HORSE" - case .unicornFace: return "UNICORN FACE" - case .zebraFace: return "ZEBRA FACE" - case .deer: return "DEER" - case .bison: return "BISON" - case .cow: return "COW FACE" - case .ox: return "OX" - case .waterBuffalo: return "WATER BUFFALO" - case .cow2: return "COW" - case .pig: return "PIG FACE" - case .pig2: return "PIG" - case .boar: return "BOAR" - case .pigNose: return "PIG NOSE" - case .ram: return "RAM" - case .sheep: return "SHEEP" - case .goat: return "GOAT" - case .dromedaryCamel: return "DROMEDARY CAMEL" - case .camel: return "BACTRIAN CAMEL" - case .llama: return "LLAMA" - case .giraffeFace: return "GIRAFFE FACE" - case .elephant: return "ELEPHANT" - case .mammoth: return "MAMMOTH" - case .rhinoceros: return "RHINOCEROS" - case .hippopotamus: return "HIPPOPOTAMUS" - case .mouse: return "MOUSE FACE" - case .mouse2: return "MOUSE" - case .rat: return "RAT" - case .hamster: return "HAMSTER FACE" - case .rabbit: return "RABBIT FACE" - case .rabbit2: return "RABBIT" - case .chipmunk: return "CHIPMUNK" - case .beaver: return "BEAVER" - case .hedgehog: return "HEDGEHOG" - case .bat: return "BAT" - case .bear: return "BEAR FACE" - case .polarBear: return "POLAR BEAR" - case .koala: return "KOALA" - case .pandaFace: return "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" - case .turkey: return "TURKEY" - case .chicken: return "CHICKEN" - case .rooster: return "ROOSTER" - case .hatchingChick: return "HATCHING CHICK" - case .babyChick: return "BABY CHICK" - case .hatchedChick: return "FRONT-FACING BABY CHICK" - case .bird: return "BIRD" - case .penguin: return "PENGUIN" - case .doveOfPeace: return "DOVE" - 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 FACE" - case .crocodile: return "CROCODILE" - case .turtle: return "TURTLE" - case .lizard: return "LIZARD" - case .snake: return "SNAKE" - case .dragonFace: return "DRAGON FACE" - case .dragon: return "DRAGON" - case .sauropod: return "SAUROPOD" - case .tRex: return "T-REX" - case .whale: return "SPOUTING WHALE" - case .whale2: return "WHALE" - case .dolphin: return "DOLPHIN" - case .seal: return "SEAL" - case .fish: return "FISH" - case .tropicalFish: return "TROPICAL FISH" - case .blowfish: return "BLOWFISH" - case .shark: return "SHARK" - case .octopus: return "OCTOPUS" - case .shell: return "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 "HONEYBEE" - case .beetle: return "BEETLE" - case .ladybug: return "LADY BEETLE" - case .cricket: return "CRICKET" - case .cockroach: return "COCKROACH" - case .spider: return "SPIDER" - case .spiderWeb: return "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 "CHERRY BLOSSOM" - case .whiteFlower: return "WHITE FLOWER" - case .lotus: return "LOTUS" - case .rosette: return "ROSETTE" - case .rose: return "ROSE" - case .wiltedFlower: return "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" - case .evergreenTree: return "EVERGREEN TREE" - case .deciduousTree: return "DECIDUOUS TREE" - case .palmTree: return "PALM TREE" - case .cactus: return "CACTUS" - case .earOfRice: return "EAR OF RICE" - case .herb: return "HERB" - case .shamrock: return "SHAMROCK" - case .fourLeafClover: return "FOUR LEAF CLOVER" - case .mapleLeaf: return "MAPLE LEAF" - case .fallenLeaf: return "FALLEN LEAF" - case .leaves: return "LEAF FLUTTERING IN WIND" - case .emptyNest: return "EMPTY NEST" - case .nestWithEggs: return "NEST WITH EGGS" - 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" - case .greenApple: return "GREEN APPLE" - 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" - case .potato: return "POTATO" - case .carrot: return "CARROT" - case .corn: return "EAR OF MAIZE" - case .hotPepper: return "HOT PEPPER" - case .bellPepper: return "BELL PEPPER" - case .cucumber: return "CUCUMBER" - case .leafyGreen: return "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" - case .flatbread: return "FLATBREAD" - case .pretzel: return "PRETZEL" - case .bagel: return "BAGEL" - case .pancakes: return "PANCAKES" - case .waffle: return "WAFFLE" - case .cheeseWedge: return "CHEESE WEDGE" - case .meatOnBone: return "MEAT ON BONE" - case .poultryLeg: return "POULTRY LEG" - case .cutOfMeat: return "CUT OF MEAT" - case .bacon: return "BACON" - case .hamburger: return "HAMBURGER" - case .fries: return "FRENCH FRIES" - case .pizza: return "SLICE OF PIZZA" - case .hotdog: return "HOT DOG" - case .sandwich: return "SANDWICH" - case .taco: return "TACO" - case .burrito: return "BURRITO" - case .tamale: return "TAMALE" - case .stuffedFlatbread: return "STUFFED FLATBREAD" - case .falafel: return "FALAFEL" - case .egg: return "EGG" - case .friedEgg: return "COOKING" - case .shallowPanOfFood: return "SHALLOW PAN OF FOOD" - case .stew: return "POT OF FOOD" - case .fondue: return "FONDUE" - case .bowlWithSpoon: return "BOWL WITH SPOON" - case .greenSalad: return "GREEN SALAD" - case .popcorn: return "POPCORN" - case .butter: return "BUTTER" - case .salt: return "SALT SHAKER" - case .cannedFood: return "CANNED FOOD" - case .bento: return "BENTO BOX" - case .riceCracker: return "RICE CRACKER" - case .riceBall: return "RICE BALL" - case .rice: return "COOKED RICE" - case .curry: return "CURRY AND RICE" - case .ramen: return "STEAMING BOWL" - case .spaghetti: return "SPAGHETTI" - case .sweetPotato: return "ROASTED SWEET POTATO" - case .oden: return "ODEN" - case .sushi: return "SUSHI" - case .friedShrimp: return "FRIED SHRIMP" - case .fishCake: return "FISH CAKE WITH SWIRL DESIGN" - case .moonCake: return "MOON CAKE" - case .dango: return "DANGO" - case .dumpling: return "DUMPLING" - case .fortuneCookie: return "FORTUNE COOKIE" - case .takeoutBox: return "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 "SOFT ICE CREAM" - case .shavedIce: return "SHAVED ICE" - case .iceCream: return "ICE CREAM" - case .doughnut: return "DOUGHNUT" - case .cookie: return "COOKIE" - case .birthday: return "BIRTHDAY CAKE" - case .cake: return "SHORTCAKE" - case .cupcake: return "CUPCAKE" - case .pie: return "PIE" - case .chocolateBar: return "CHOCOLATE BAR" - case .candy: return "CANDY" - case .lollipop: return "LOLLIPOP" - case .custard: return "CUSTARD" - case .honeyPot: return "HONEY POT" - case .babyBottle: return "BABY BOTTLE" - case .glassOfMilk: return "GLASS OF MILK" - case .coffee: return "HOT BEVERAGE" - case .teapot: return "TEAPOT" - case .tea: return "TEACUP WITHOUT HANDLE" - case .sake: return "SAKE BOTTLE AND CUP" - case .champagne: return "BOTTLE WITH POPPING CORK" - case .wineGlass: return "WINE GLASS" - case .cocktail: return "COCKTAIL GLASS" - case .tropicalDrink: return "TROPICAL DRINK" - case .beer: return "BEER MUG" - case .beers: return "CLINKING BEER MUGS" - case .clinkingGlasses: return "CLINKING GLASSES" - case .tumblerGlass: return "TUMBLER GLASS" - case .pouringLiquid: return "POURING LIQUID" - case .cupWithStraw: return "CUP WITH STRAW" - case .bubbleTea: return "BUBBLE TEA" - case .beverageBox: return "BEVERAGE BOX" - case .mateDrink: return "MATE DRINK" - case .iceCube: return "ICE CUBE" - case .chopsticks: return "CHOPSTICKS" - case .knifeForkPlate: return "FORK AND KNIFE WITH PLATE" - case .forkAndKnife: return "FORK AND KNIFE" - case .spoon: return "SPOON" - case .hocho: return "HOCHO" - case .jar: return "JAR" - case .amphora: return "AMPHORA" - case .earthAfrica: return "EARTH GLOBE EUROPE-AFRICA" - case .earthAmericas: return "EARTH GLOBE AMERICAS" - case .earthAsia: return "EARTH GLOBE ASIA-AUSTRALIA" - case .globeWithMeridians: return "GLOBE WITH MERIDIANS" - case .worldMap: return "WORLD MAP" - case .japan: return "SILHOUETTE OF JAPAN" - case .compass: return "COMPASS" - case .snowCappedMountain: return "SNOW-CAPPED MOUNTAIN" - case .mountain: return "MOUNTAIN" - case .volcano: return "VOLCANO" - case .mountFuji: return "MOUNT FUJI" - case .camping: return "CAMPING" - case .beachWithUmbrella: return "BEACH WITH UMBRELLA" - case .desert: return "DESERT" - case .desertIsland: return "DESERT ISLAND" - case .nationalPark: return "NATIONAL PARK" - case .stadium: return "STADIUM" - case .classicalBuilding: return "CLASSICAL BUILDING" - case .buildingConstruction: return "BUILDING CONSTRUCTION" - case .bricks: return "BRICK" - case .rock: return "ROCK" - case .wood: return "WOOD" - case .hut: return "HUT" - case .houseBuildings: return "HOUSES" - case .derelictHouseBuilding: return "DERELICT HOUSE" - case .house: return "HOUSE BUILDING" - case .houseWithGarden: return "HOUSE WITH GARDEN" - case .office: return "OFFICE BUILDING" - case .postOffice: return "JAPANESE POST OFFICE" - case .europeanPostOffice: return "EUROPEAN POST OFFICE" - case .hospital: return "HOSPITAL" - case .bank: return "BANK" - case .hotel: return "HOTEL" - case .loveHotel: return "LOVE HOTEL" - case .convenienceStore: return "CONVENIENCE STORE" - case .school: return "SCHOOL" - case .departmentStore: return "DEPARTMENT STORE" - case .factory: return "FACTORY" - case .japaneseCastle: return "JAPANESE CASTLE" - case .europeanCastle: return "EUROPEAN CASTLE" - case .wedding: return "WEDDING" - case .tokyoTower: return "TOKYO TOWER" - case .statueOfLiberty: return "STATUE OF LIBERTY" - case .church: return "CHURCH" - case .mosque: return "MOSQUE" - case .hinduTemple: return "HINDU TEMPLE" - case .synagogue: return "SYNAGOGUE" - case .shintoShrine: return "SHINTO SHRINE" - case .kaaba: return "KAABA" - case .fountain: return "FOUNTAIN" - case .tent: return "TENT" - case .foggy: return "FOGGY" - case .nightWithStars: return "NIGHT WITH STARS" - case .cityscape: return "CITYSCAPE" - case .sunriseOverMountains: return "SUNRISE OVER MOUNTAINS" - case .sunrise: return "SUNRISE" - case .citySunset: return "CITYSCAPE AT DUSK" - case .citySunrise: return "SUNSET OVER BUILDINGS" - case .bridgeAtNight: return "BRIDGE AT NIGHT" - case .hotsprings: return "HOT SPRINGS" - case .carouselHorse: return "CAROUSEL HORSE" - case .playgroundSlide: return "PLAYGROUND SLIDE" - case .ferrisWheel: return "FERRIS WHEEL" - case .rollerCoaster: return "ROLLER COASTER" - case .barber: return "BARBER POLE" - case .circusTent: return "CIRCUS TENT" - case .steamLocomotive: return "STEAM LOCOMOTIVE" - case .railwayCar: return "RAILWAY CAR" - case .bullettrainSide: return "HIGH-SPEED TRAIN" - case .bullettrainFront: return "HIGH-SPEED TRAIN WITH BULLET NOSE" - case .train2: return "TRAIN" - case .metro: return "METRO" - case .lightRail: return "LIGHT RAIL" - case .station: return "STATION" - case .tram: return "TRAM" - case .monorail: return "MONORAIL" - case .mountainRailway: return "MOUNTAIN RAILWAY" - case .train: return "TRAM CAR" - case .bus: return "BUS" - case .oncomingBus: return "ONCOMING BUS" - case .trolleybus: return "TROLLEYBUS" - case .minibus: return "MINIBUS" - case .ambulance: return "AMBULANCE" - case .fireEngine: return "FIRE ENGINE" - case .policeCar: return "POLICE CAR" - case .oncomingPoliceCar: return "ONCOMING POLICE CAR" - case .taxi: return "TAXI" - case .oncomingTaxi: return "ONCOMING TAXI" - case .car: return "AUTOMOBILE" - case .oncomingAutomobile: return "ONCOMING AUTOMOBILE" - case .blueCar: return "RECREATIONAL VEHICLE" - case .pickupTruck: return "PICKUP TRUCK" - case .truck: return "DELIVERY TRUCK" - case .articulatedLorry: return "ARTICULATED LORRY" - case .tractor: return "TRACTOR" - case .racingCar: return "RACING CAR" - case .racingMotorcycle: return "MOTORCYCLE" - case .motorScooter: return "MOTOR SCOOTER" - case .manualWheelchair: return "MANUAL WHEELCHAIR" - case .motorizedWheelchair: return "MOTORIZED WHEELCHAIR" - case .autoRickshaw: return "AUTO RICKSHAW" - case .bike: return "BICYCLE" - case .scooter: return "SCOOTER" - case .skateboard: return "SKATEBOARD" - case .rollerSkate: return "ROLLER SKATE" - case .busstop: return "BUS STOP" - case .motorway: return "MOTORWAY" - case .railwayTrack: return "RAILWAY TRACK" - case .oilDrum: return "OIL DRUM" - case .fuelpump: return "FUEL PUMP" - case .wheel: return "WHEEL" - case .rotatingLight: return "POLICE CARS REVOLVING LIGHT" - case .trafficLight: return "HORIZONTAL TRAFFIC LIGHT" - case .verticalTrafficLight: return "VERTICAL TRAFFIC LIGHT" - case .octagonalSign: return "OCTAGONAL SIGN" - case .construction: return "CONSTRUCTION SIGN" - case .anchor: return "ANCHOR" - case .ringBuoy: return "RING BUOY" - case .boat: return "SAILBOAT" - case .canoe: return "CANOE" - case .speedboat: return "SPEEDBOAT" - case .passengerShip: return "PASSENGER SHIP" - case .ferry: return "FERRY" - case .motorBoat: return "MOTOR BOAT" - case .ship: return "SHIP" - case .airplane: return "AIRPLANE" - case .smallAirplane: return "SMALL AIRPLANE" - case .airplaneDeparture: return "AIRPLANE DEPARTURE" - case .airplaneArriving: return "AIRPLANE ARRIVING" - case .parachute: return "PARACHUTE" - case .seat: return "SEAT" - case .helicopter: return "HELICOPTER" - case .suspensionRailway: return "SUSPENSION RAILWAY" - case .mountainCableway: return "MOUNTAIN CABLEWAY" - case .aerialTramway: return "AERIAL TRAMWAY" - case .satellite: return "SATELLITE" - case .rocket: return "ROCKET" - case .flyingSaucer: return "FLYING SAUCER" - case .bellhopBell: return "BELLHOP BELL" - case .luggage: return "LUGGAGE" - case .hourglass: return "HOURGLASS" - case .hourglassFlowingSand: return "HOURGLASS WITH FLOWING SAND" - case .watch: return "WATCH" - case .alarmClock: return "ALARM CLOCK" - case .stopwatch: return "STOPWATCH" - case .timerClock: return "TIMER CLOCK" - case .mantelpieceClock: return "MANTELPIECE CLOCK" - case .clock12: return "CLOCK FACE TWELVE OCLOCK" - case .clock1230: return "CLOCK FACE TWELVE-THIRTY" - case .clock1: return "CLOCK FACE ONE OCLOCK" - case .clock130: return "CLOCK FACE ONE-THIRTY" - case .clock2: return "CLOCK FACE TWO OCLOCK" - case .clock230: return "CLOCK FACE TWO-THIRTY" - case .clock3: return "CLOCK FACE THREE OCLOCK" - case .clock330: return "CLOCK FACE THREE-THIRTY" - case .clock4: return "CLOCK FACE FOUR OCLOCK" - case .clock430: return "CLOCK FACE FOUR-THIRTY" - case .clock5: return "CLOCK FACE FIVE OCLOCK" - case .clock530: return "CLOCK FACE FIVE-THIRTY" - case .clock6: return "CLOCK FACE SIX OCLOCK" - case .clock630: return "CLOCK FACE SIX-THIRTY" - case .clock7: return "CLOCK FACE SEVEN OCLOCK" - case .clock730: return "CLOCK FACE SEVEN-THIRTY" - case .clock8: return "CLOCK FACE EIGHT OCLOCK" - case .clock830: return "CLOCK FACE EIGHT-THIRTY" - case .clock9: return "CLOCK FACE NINE OCLOCK" - case .clock930: return "CLOCK FACE NINE-THIRTY" - case .clock10: return "CLOCK FACE TEN OCLOCK" - case .clock1030: return "CLOCK FACE TEN-THIRTY" - case .clock11: return "CLOCK FACE ELEVEN OCLOCK" - case .clock1130: return "CLOCK FACE ELEVEN-THIRTY" - case .newMoon: return "NEW MOON SYMBOL" - case .waxingCrescentMoon: return "WAXING CRESCENT MOON SYMBOL" - case .firstQuarterMoon: return "FIRST QUARTER MOON SYMBOL" - case .moon: return "WAXING GIBBOUS MOON SYMBOL" - case .fullMoon: return "FULL MOON SYMBOL" - case .waningGibbousMoon: return "WANING GIBBOUS MOON SYMBOL" - case .lastQuarterMoon: return "LAST QUARTER MOON SYMBOL" - case .waningCrescentMoon: return "WANING CRESCENT MOON SYMBOL" - case .crescentMoon: return "CRESCENT MOON" - case .newMoonWithFace: return "NEW MOON WITH FACE" - case .firstQuarterMoonWithFace: return "FIRST QUARTER MOON WITH FACE" - case .lastQuarterMoonWithFace: return "LAST QUARTER MOON WITH FACE" - case .thermometer: return "THERMOMETER" - case .sunny: return "BLACK SUN WITH RAYS" - case .fullMoonWithFace: return "FULL MOON WITH FACE" - case .sunWithFace: return "SUN WITH FACE" - case .ringedPlanet: return "RINGED PLANET" - case .star: return "WHITE MEDIUM STAR" - case .star2: return "GLOWING STAR" - case .stars: return "SHOOTING STAR" - case .milkyWay: return "MILKY WAY" - case .cloud: return "CLOUD" - case .partlySunny: return "SUN BEHIND CLOUD" - case .thunderCloudAndRain: return "CLOUD WITH LIGHTNING AND RAIN" - case .mostlySunny: return "SUN BEHIND SMALL CLOUD" - case .barelySunny: return "SUN BEHIND LARGE CLOUD" - case .partlySunnyRain: return "SUN BEHIND RAIN CLOUD" - case .rainCloud: return "CLOUD WITH RAIN" - case .snowCloud: return "CLOUD WITH SNOW" - case .lightning: return "CLOUD WITH LIGHTNING" - case .tornado: return "TORNADO" - case .fog: return "FOG" - case .windBlowingFace: return "WIND FACE" - case .cyclone: return "CYCLONE" - case .rainbow: return "RAINBOW" - case .closedUmbrella: return "CLOSED UMBRELLA" - case .umbrella: return "UMBRELLA" - case .umbrellaWithRainDrops: return "UMBRELLA WITH RAIN DROPS" - case .umbrellaOnGround: return "UMBRELLA ON GROUND" - case .zap: return "HIGH VOLTAGE SIGN" - case .snowflake: return "SNOWFLAKE" - case .snowman: return "SNOWMAN" - case .snowmanWithoutSnow: return "SNOWMAN WITHOUT SNOW" - case .comet: return "COMET" - case .fire: return "FIRE" - case .droplet: return "DROPLET" - case .ocean: return "WATER WAVE" - case .jackOLantern: return "JACK-O-LANTERN" - case .christmasTree: return "CHRISTMAS TREE" - case .fireworks: return "FIREWORKS" - case .sparkler: return "FIREWORK SPARKLER" - case .firecracker: return "FIRECRACKER" - case .sparkles: return "SPARKLES" - case .balloon: return "BALLOON" - case .tada: return "PARTY POPPER" - case .confettiBall: return "CONFETTI BALL" - case .tanabataTree: return "TANABATA TREE" - case .bamboo: return "PINE DECORATION" - case .dolls: return "JAPANESE DOLLS" - case .flags: return "CARP STREAMER" - case .windChime: return "WIND CHIME" - case .riceScene: return "MOON VIEWING CEREMONY" - case .redEnvelope: return "RED GIFT ENVELOPE" - case .ribbon: return "RIBBON" - case .gift: return "WRAPPED PRESENT" - case .reminderRibbon: return "REMINDER RIBBON" - case .admissionTickets: return "ADMISSION TICKETS" - case .ticket: return "TICKET" - case .medal: return "MILITARY MEDAL" - case .trophy: return "TROPHY" - case .sportsMedal: return "SPORTS MEDAL" - case .firstPlaceMedal: return "FIRST PLACE MEDAL" - case .secondPlaceMedal: return "SECOND PLACE MEDAL" - case .thirdPlaceMedal: return "THIRD PLACE MEDAL" - case .soccer: return "SOCCER BALL" - case .baseball: return "BASEBALL" - case .softball: return "SOFTBALL" - case .basketball: return "BASKETBALL AND HOOP" - case .volleyball: return "VOLLEYBALL" - case .football: return "AMERICAN FOOTBALL" - case .rugbyFootball: return "RUGBY FOOTBALL" - case .tennis: return "TENNIS RACQUET AND BALL" - case .flyingDisc: return "FLYING DISC" - case .bowling: return "BOWLING" - case .cricketBatAndBall: return "CRICKET BAT AND BALL" - case .fieldHockeyStickAndBall: return "FIELD HOCKEY STICK AND BALL" - case .iceHockeyStickAndPuck: return "ICE HOCKEY STICK AND PUCK" - case .lacrosse: return "LACROSSE STICK AND BALL" - case .tableTennisPaddleAndBall: return "TABLE TENNIS PADDLE AND BALL" - case .badmintonRacquetAndShuttlecock: return "BADMINTON RACQUET AND SHUTTLECOCK" - case .boxingGlove: return "BOXING GLOVE" - case .martialArtsUniform: return "MARTIAL ARTS UNIFORM" - case .goalNet: return "GOAL NET" - case .golf: return "FLAG IN HOLE" - case .iceSkate: return "ICE SKATE" - case .fishingPoleAndFish: return "FISHING POLE AND FISH" - case .divingMask: return "DIVING MASK" - case .runningShirtWithSash: return "RUNNING SHIRT WITH SASH" - case .ski: return "SKI AND SKI BOOT" - case .sled: return "SLED" - case .curlingStone: return "CURLING STONE" - case .dart: return "DIRECT HIT" - case .yoYo: return "YO-YO" - case .kite: return "KITE" - case .eightBall: return "BILLIARDS" - case .crystalBall: return "CRYSTAL BALL" - case .magicWand: return "MAGIC WAND" - case .nazarAmulet: return "NAZAR AMULET" - case .hamsa: return "HAMSA" - case .videoGame: return "VIDEO GAME" - case .joystick: return "JOYSTICK" - case .slotMachine: return "SLOT MACHINE" - case .gameDie: return "GAME DIE" - case .jigsaw: return "JIGSAW PUZZLE PIECE" - case .teddyBear: return "TEDDY BEAR" - case .pinata: return "PINATA" - case .mirrorBall: return "MIRROR BALL" - case .nestingDolls: return "NESTING DOLLS" - case .spades: return "BLACK SPADE SUIT" - case .hearts: return "BLACK HEART SUIT" - case .diamonds: return "BLACK DIAMOND SUIT" - case .clubs: return "BLACK CLUB SUIT" - case .chessPawn: return "CHESS PAWN" - case .blackJoker: return "PLAYING CARD BLACK JOKER" - case .mahjong: return "MAHJONG TILE RED DRAGON" - case .flowerPlayingCards: return "FLOWER PLAYING CARDS" - case .performingArts: return "PERFORMING ARTS" - case .frameWithPicture: return "FRAMED PICTURE" - case .art: return "ARTIST PALETTE" - case .thread: return "SPOOL OF THREAD" - case .sewingNeedle: return "SEWING NEEDLE" - case .yarn: return "BALL OF YARN" - case .knot: return "KNOT" - case .eyeglasses: return "EYEGLASSES" - case .darkSunglasses: return "SUNGLASSES" - case .goggles: return "GOGGLES" - case .labCoat: return "LAB COAT" - case .safetyVest: return "SAFETY VEST" - case .necktie: return "NECKTIE" - case .shirt: return "T-SHIRT" - 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" - case .briefs: return "BRIEFS" - case .shorts: return "SHORTS" - case .bikini: return "BIKINI" - case .womansClothes: return "WOMANS CLOTHES" - case .purse: return "PURSE" - case .handbag: return "HANDBAG" - case .pouch: return "POUCH" - case .shoppingBags: return "SHOPPING BAGS" - case .schoolSatchel: return "SCHOOL SATCHEL" - case .thongSandal: return "THONG SANDAL" - case .mansShoe: return "MANS SHOE" - case .athleticShoe: return "ATHLETIC SHOE" - case .hikingBoot: return "HIKING BOOT" - case .womansFlatShoe: return "FLAT SHOE" - case .highHeel: return "HIGH-HEELED SHOE" - case .sandal: return "WOMANS SANDAL" - case .balletShoes: return "BALLET SHOES" - case .boot: return "WOMANS BOOTS" - case .crown: return "CROWN" - case .womansHat: return "WOMANS HAT" - case .tophat: return "TOP HAT" - case .mortarBoard: return "GRADUATION CAP" - case .billedCap: return "BILLED CAP" - case .militaryHelmet: return "MILITARY HELMET" - case .helmetWithWhiteCross: return "RESCUE WORKERโ€™S HELMET" - case .prayerBeads: return "PRAYER BEADS" - case .lipstick: return "LIPSTICK" - case .ring: return "RING" - case .gem: return "GEM STONE" - case .mute: return "SPEAKER WITH CANCELLATION STROKE" - case .speaker: return "SPEAKER" - case .sound: return "SPEAKER WITH ONE SOUND WAVE" - case .loudSound: return "SPEAKER WITH THREE SOUND WAVES" - case .loudspeaker: return "PUBLIC ADDRESS LOUDSPEAKER" - case .mega: return "CHEERING MEGAPHONE" - case .postalHorn: return "POSTAL HORN" - case .bell: return "BELL" - case .noBell: return "BELL WITH CANCELLATION STROKE" - case .musicalScore: return "MUSICAL SCORE" - case .musicalNote: return "MUSICAL NOTE" - case .notes: return "MULTIPLE MUSICAL NOTES" - case .studioMicrophone: return "STUDIO MICROPHONE" - case .levelSlider: return "LEVEL SLIDER" - case .controlKnobs: return "CONTROL KNOBS" - case .microphone: return "MICROPHONE" - case .headphones: return "HEADPHONE" - case .radio: return "RADIO" - case .saxophone: return "SAXOPHONE" - case .accordion: return "ACCORDION" - case .guitar: return "GUITAR" - case .musicalKeyboard: return "MUSICAL KEYBOARD" - case .trumpet: return "TRUMPET" - case .violin: return "VIOLIN" - case .banjo: return "BANJO" - case .drumWithDrumsticks: return "DRUM WITH DRUMSTICKS" - case .longDrum: return "LONG DRUM" - case .iphone: return "MOBILE PHONE" - case .calling: return "MOBILE PHONE WITH RIGHTWARDS ARROW AT LEFT" - case .phone: return "BLACK TELEPHONE" - case .telephoneReceiver: return "TELEPHONE RECEIVER" - case .pager: return "PAGER" - case .fax: return "FAX MACHINE" - case .battery: return "BATTERY" - case .lowBattery: return "LOW BATTERY" - case .electricPlug: return "ELECTRIC PLUG" - case .computer: return "PERSONAL COMPUTER" - case .desktopComputer: return "DESKTOP COMPUTER" - case .printer: return "PRINTER" - case .keyboard: return "KEYBOARD" - case .threeButtonMouse: return "COMPUTER MOUSE" - case .trackball: return "TRACKBALL" - case .minidisc: return "MINIDISC" - case .floppyDisk: return "FLOPPY DISK" - case .cd: return "OPTICAL DISC" - case .dvd: return "DVD" - case .abacus: return "ABACUS" - case .movieCamera: return "MOVIE CAMERA" - case .filmFrames: return "FILM FRAMES" - case .filmProjector: return "FILM PROJECTOR" - case .clapper: return "CLAPPER BOARD" - case .tv: return "TELEVISION" - case .camera: return "CAMERA" - case .cameraWithFlash: return "CAMERA WITH FLASH" - case .videoCamera: return "VIDEO CAMERA" - case .vhs: return "VIDEOCASSETTE" - case .mag: return "LEFT-POINTING MAGNIFYING GLASS" - case .magRight: return "RIGHT-POINTING MAGNIFYING GLASS" - case .candle: return "CANDLE" - case .bulb: return "ELECTRIC LIGHT BULB" - case .flashlight: return "ELECTRIC TORCH" - case .izakayaLantern: return "IZAKAYA LANTERN" - case .diyaLamp: return "DIYA LAMP" - case .notebookWithDecorativeCover: return "NOTEBOOK WITH DECORATIVE COVER" - case .closedBook: return "CLOSED BOOK" - case .book: return "OPEN BOOK" - case .greenBook: return "GREEN BOOK" - case .blueBook: return "BLUE BOOK" - case .orangeBook: return "ORANGE BOOK" - case .books: return "BOOKS" - case .notebook: return "NOTEBOOK" - case .ledger: return "LEDGER" - case .pageWithCurl: return "PAGE WITH CURL" - case .scroll: return "SCROLL" - case .pageFacingUp: return "PAGE FACING UP" - case .newspaper: return "NEWSPAPER" - case .rolledUpNewspaper: return "ROLLED-UP NEWSPAPER" - case .bookmarkTabs: return "BOOKMARK TABS" - case .bookmark: return "BOOKMARK" - case .label: return "LABEL" - case .moneybag: return "MONEY BAG" - case .coin: return "COIN" - case .yen: return "BANKNOTE WITH YEN SIGN" - case .dollar: return "BANKNOTE WITH DOLLAR SIGN" - case .euro: return "BANKNOTE WITH EURO SIGN" - case .pound: return "BANKNOTE WITH POUND SIGN" - case .moneyWithWings: return "MONEY WITH WINGS" - case .creditCard: return "CREDIT CARD" - case .receipt: return "RECEIPT" - case .chart: return "CHART WITH UPWARDS TREND AND YEN SIGN" - case .email: return "ENVELOPE" - case .eMail: return "E-MAIL SYMBOL" - case .incomingEnvelope: return "INCOMING ENVELOPE" - case .envelopeWithArrow: return "ENVELOPE WITH DOWNWARDS ARROW ABOVE" - case .outboxTray: return "OUTBOX TRAY" - case .inboxTray: return "INBOX TRAY" - case .package: return "PACKAGE" - case .mailbox: return "CLOSED MAILBOX WITH RAISED FLAG" - case .mailboxClosed: return "CLOSED MAILBOX WITH LOWERED FLAG" - case .mailboxWithMail: return "OPEN MAILBOX WITH RAISED FLAG" - case .mailboxWithNoMail: return "OPEN MAILBOX WITH LOWERED FLAG" - case .postbox: return "POSTBOX" - case .ballotBoxWithBallot: return "BALLOT BOX WITH BALLOT" - case .pencil2: return "PENCIL" - case .blackNib: return "BLACK NIB" - case .lowerLeftFountainPen: return "FOUNTAIN PEN" - case .lowerLeftBallpointPen: return "PEN" - case .lowerLeftPaintbrush: return "PAINTBRUSH" - case .lowerLeftCrayon: return "CRAYON" - case .memo: return "MEMO" - case .briefcase: return "BRIEFCASE" - case .fileFolder: return "FILE FOLDER" - case .openFileFolder: return "OPEN FILE FOLDER" - case .cardIndexDividers: return "CARD INDEX DIVIDERS" - case .date: return "CALENDAR" - case .calendar: return "TEAR-OFF CALENDAR" - case .spiralNotePad: return "SPIRAL NOTEPAD" - case .spiralCalendarPad: return "SPIRAL CALENDAR" - case .cardIndex: return "CARD INDEX" - case .chartWithUpwardsTrend: return "CHART WITH UPWARDS TREND" - case .chartWithDownwardsTrend: return "CHART WITH DOWNWARDS TREND" - case .barChart: return "BAR CHART" - case .clipboard: return "CLIPBOARD" - case .pushpin: return "PUSHPIN" - case .roundPushpin: return "ROUND PUSHPIN" - case .paperclip: return "PAPERCLIP" - case .linkedPaperclips: return "LINKED PAPERCLIPS" - case .straightRuler: return "STRAIGHT RULER" - case .triangularRuler: return "TRIANGULAR RULER" - case .scissors: return "BLACK SCISSORS" - case .cardFileBox: return "CARD FILE BOX" - case .fileCabinet: return "FILE CABINET" - case .wastebasket: return "WASTEBASKET" - case .lock: return "LOCK" - case .unlock: return "OPEN LOCK" - case .lockWithInkPen: return "LOCK WITH INK PEN" - case .closedLockWithKey: return "CLOSED LOCK WITH KEY" - case .key: return "KEY" - case .oldKey: return "OLD KEY" - case .hammer: return "HAMMER" - case .axe: return "AXE" - case .pick: return "PICK" - case .hammerAndPick: return "HAMMER AND PICK" - case .hammerAndWrench: return "HAMMER AND WRENCH" - case .daggerKnife: return "DAGGER" - case .crossedSwords: return "CROSSED SWORDS" - case .gun: return "PISTOL" - case .boomerang: return "BOOMERANG" - case .bowAndArrow: return "BOW AND ARROW" - case .shield: return "SHIELD" - case .carpentrySaw: return "CARPENTRY SAW" - case .wrench: return "WRENCH" - case .screwdriver: return "SCREWDRIVER" - case .nutAndBolt: return "NUT AND BOLT" - case .gear: return "GEAR" - case .compression: return "CLAMP" - case .scales: return "BALANCE SCALE" - case .probingCane: return "PROBING CANE" - case .link: return "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" - case .petriDish: return "PETRI DISH" - case .dna: return "DNA DOUBLE HELIX" - case .microscope: return "MICROSCOPE" - case .telescope: return "TELESCOPE" - case .satelliteAntenna: return "SATELLITE ANTENNA" - case .syringe: return "SYRINGE" - case .dropOfBlood: return "DROP OF BLOOD" - case .pill: return "PILL" - case .adhesiveBandage: return "ADHESIVE BANDAGE" - case .crutch: return "CRUTCH" - case .stethoscope: return "STETHOSCOPE" - case .xRay: return "X-RAY" - 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" - case .chair: return "CHAIR" - case .toilet: return "TOILET" - case .plunger: return "PLUNGER" - case .shower: return "SHOWER" - case .bathtub: return "BATHTUB" - case .mouseTrap: return "MOUSE TRAP" - case .razor: return "RAZOR" - case .lotionBottle: return "LOTION BOTTLE" - case .safetyPin: return "SAFETY PIN" - case .broom: return "BROOM" - case .basket: return "BASKET" - case .rollOfPaper: return "ROLL OF PAPER" - case .bucket: return "BUCKET" - case .soap: return "BAR OF SOAP" - case .bubbles: return "BUBBLES" - case .toothbrush: return "TOOTHBRUSH" - case .sponge: return "SPONGE" - case .fireExtinguisher: return "FIRE EXTINGUISHER" - case .shoppingTrolley: return "SHOPPING TROLLEY" - case .smoking: return "SMOKING SYMBOL" - case .coffin: return "COFFIN" - case .headstone: return "HEADSTONE" - case .funeralUrn: return "FUNERAL URN" - case .moyai: return "MOYAI" - case .placard: return "PLACARD" - case .identificationCard: return "IDENTIFICATION CARD" - case .atm: return "AUTOMATED TELLER MACHINE" - case .putLitterInItsPlace: return "PUT LITTER IN ITS PLACE SYMBOL" - case .potableWater: return "POTABLE WATER SYMBOL" - case .wheelchair: return "WHEELCHAIR SYMBOL" - case .mens: return "MENS SYMBOL" - case .womens: return "WOMENS SYMBOL" - case .restroom: return "RESTROOM" - case .babySymbol: return "BABY SYMBOL" - case .wc: return "WATER CLOSET" - case .passportControl: return "PASSPORT CONTROL" - case .customs: return "CUSTOMS" - case .baggageClaim: return "BAGGAGE CLAIM" - case .leftLuggage: return "LEFT LUGGAGE" - case .warning: return "WARNING SIGN" - case .childrenCrossing: return "CHILDREN CROSSING" - case .noEntry: return "NO ENTRY" - case .noEntrySign: return "NO ENTRY SIGN" - case .noBicycles: return "NO BICYCLES" - case .noSmoking: return "NO SMOKING SYMBOL" - case .doNotLitter: return "DO NOT LITTER SYMBOL" - case .nonPotableWater: return "NON-POTABLE WATER SYMBOL" - case .noPedestrians: return "NO PEDESTRIANS" - case .noMobilePhones: return "NO MOBILE PHONES" - case .underage: return "NO ONE UNDER EIGHTEEN SYMBOL" - case .radioactiveSign: return "RADIOACTIVE" - case .biohazardSign: return "BIOHAZARD" - case .arrowUp: return "UPWARDS BLACK ARROW" - case .arrowUpperRight: return "NORTH EAST ARROW" - case .arrowRight: return "BLACK RIGHTWARDS ARROW" - case .arrowLowerRight: return "SOUTH EAST ARROW" - case .arrowDown: return "DOWNWARDS BLACK ARROW" - case .arrowLowerLeft: return "SOUTH WEST ARROW" - case .arrowLeft: return "LEFTWARDS BLACK ARROW" - case .arrowUpperLeft: return "NORTH WEST ARROW" - case .arrowUpDown: return "UP DOWN ARROW" - case .leftRightArrow: return "LEFT RIGHT ARROW" - case .leftwardsArrowWithHook: return "LEFTWARDS ARROW WITH HOOK" - case .arrowRightHook: return "RIGHTWARDS ARROW WITH HOOK" - case .arrowHeadingUp: return "ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS" - case .arrowHeadingDown: return "ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS" - case .arrowsClockwise: return "CLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS" - case .arrowsCounterclockwise: return "ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS" - case .back: return "BACK WITH LEFTWARDS ARROW ABOVE" - case .end: return "END WITH LEFTWARDS ARROW ABOVE" - case .on: return "ON WITH EXCLAMATION MARK WITH LEFT RIGHT ARROW ABOVE" - case .soon: return "SOON WITH RIGHTWARDS ARROW ABOVE" - case .top: return "TOP WITH UPWARDS ARROW ABOVE" - case .placeOfWorship: return "PLACE OF WORSHIP" - case .atomSymbol: return "ATOM SYMBOL" - case .omSymbol: return "OM" - case .starOfDavid: return "STAR OF DAVID" - case .wheelOfDharma: return "WHEEL OF DHARMA" - case .yinYang: return "YIN YANG" - case .latinCross: return "LATIN CROSS" - case .orthodoxCross: return "ORTHODOX CROSS" - case .starAndCrescent: return "STAR AND CRESCENT" - case .peaceSymbol: return "PEACE SYMBOL" - case .menorahWithNineBranches: return "MENORAH WITH NINE BRANCHES" - case .sixPointedStar: return "SIX POINTED STAR WITH MIDDLE DOT" - 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" - case .`repeat`: return "CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS" - case .repeatOne: return "CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY" - case .arrowForward: return "BLACK RIGHT-POINTING TRIANGLE" - case .fastForward: return "BLACK RIGHT-POINTING DOUBLE TRIANGLE" - case .blackRightPointingDoubleTriangleWithVerticalBar: return "NEXT TRACK BUTTON" - case .blackRightPointingTriangleWithDoubleVerticalBar: return "PLAY OR PAUSE BUTTON" - case .arrowBackward: return "BLACK LEFT-POINTING TRIANGLE" - case .rewind: return "BLACK LEFT-POINTING DOUBLE TRIANGLE" - case .blackLeftPointingDoubleTriangleWithVerticalBar: return "LAST TRACK BUTTON" - case .arrowUpSmall: return "UP-POINTING SMALL RED TRIANGLE" - case .arrowDoubleUp: return "BLACK UP-POINTING DOUBLE TRIANGLE" - case .arrowDownSmall: return "DOWN-POINTING SMALL RED TRIANGLE" - case .arrowDoubleDown: return "BLACK DOWN-POINTING DOUBLE TRIANGLE" - case .doubleVerticalBar: return "PAUSE BUTTON" - case .blackSquareForStop: return "STOP BUTTON" - case .blackCircleForRecord: return "RECORD BUTTON" - case .eject: return "EJECT BUTTON" - case .cinema: return "CINEMA" - case .lowBrightness: return "LOW BRIGHTNESS SYMBOL" - case .highBrightness: return "HIGH BRIGHTNESS SYMBOL" - case .signalStrength: return "ANTENNA WITH BARS" - case .vibrationMode: return "VIBRATION MODE" - case .mobilePhoneOff: return "MOBILE PHONE OFF" - case .femaleSign: return "FEMALE SIGN" - case .maleSign: return "MALE SIGN" - case .transgenderSymbol: return "TRANSGENDER SYMBOL" - case .heavyMultiplicationX: return "HEAVY MULTIPLICATION X" - case .heavyPlusSign: return "HEAVY PLUS SIGN" - case .heavyMinusSign: return "HEAVY MINUS SIGN" - case .heavyDivisionSign: return "HEAVY DIVISION SIGN" - case .heavyEqualsSign: return "HEAVY EQUALS SIGN" - case .infinity: return "INFINITY" - case .bangbang: return "DOUBLE EXCLAMATION MARK" - case .interrobang: return "EXCLAMATION QUESTION MARK" - case .question: return "BLACK QUESTION MARK ORNAMENT" - case .greyQuestion: return "WHITE QUESTION MARK ORNAMENT" - case .greyExclamation: return "WHITE EXCLAMATION MARK ORNAMENT" - case .exclamation: return "HEAVY EXCLAMATION MARK SYMBOL" - case .wavyDash: return "WAVY DASH" - case .currencyExchange: return "CURRENCY EXCHANGE" - case .heavyDollarSign: return "HEAVY DOLLAR SIGN" - case .medicalSymbol: return "MEDICAL SYMBOL" - case .recycle: return "BLACK UNIVERSAL RECYCLING SYMBOL" - case .fleurDeLis: return "FLEUR-DE-LIS" - case .trident: return "TRIDENT EMBLEM" - case .nameBadge: return "NAME BADGE" - case .beginner: return "JAPANESE SYMBOL FOR BEGINNER" - case .o: return "HEAVY LARGE CIRCLE" - case .whiteCheckMark: return "WHITE HEAVY CHECK MARK" - case .ballotBoxWithCheck: return "BALLOT BOX WITH CHECK" - case .heavyCheckMark: return "HEAVY CHECK MARK" - case .x: return "CROSS MARK" - case .negativeSquaredCrossMark: return "NEGATIVE SQUARED CROSS MARK" - case .curlyLoop: return "CURLY LOOP" - case .loop: return "DOUBLE CURLY LOOP" - case .partAlternationMark: return "PART ALTERNATION MARK" - case .eightSpokedAsterisk: return "EIGHT SPOKED ASTERISK" - case .eightPointedBlackStar: return "EIGHT POINTED BLACK STAR" - case .sparkle: return "SPARKLE" - case .copyright: return "COPYRIGHT SIGN" - case .registered: return "REGISTERED SIGN" - case .tm: return "TRADE MARK SIGN" - case .hash: return "HASH KEY" - case .keycapStar: return "KEYCAP: *" - case .zero: return "KEYCAP 0" - case .one: return "KEYCAP 1" - case .two: return "KEYCAP 2" - case .three: return "KEYCAP 3" - case .four: return "KEYCAP 4" - case .five: return "KEYCAP 5" - case .six: return "KEYCAP 6" - case .seven: return "KEYCAP 7" - case .eight: return "KEYCAP 8" - case .nine: return "KEYCAP 9" - case .keycapTen: return "KEYCAP TEN" - case .capitalAbcd: return "INPUT SYMBOL FOR LATIN CAPITAL LETTERS" - case .abcd: return "INPUT SYMBOL FOR LATIN SMALL LETTERS" - case .oneTwoThreeFour: return "INPUT SYMBOL FOR NUMBERS" - case .symbols: return "INPUT SYMBOL FOR SYMBOLS" - case .abc: return "INPUT SYMBOL FOR LATIN LETTERS" - case .a: return "NEGATIVE SQUARED LATIN CAPITAL LETTER A" - case .ab: return "NEGATIVE SQUARED AB" - case .b: return "NEGATIVE SQUARED LATIN CAPITAL LETTER B" - case .cl: return "SQUARED CL" - case .cool: return "SQUARED COOL" - case .free: return "SQUARED FREE" - case .informationSource: return "INFORMATION SOURCE" - case .id: return "SQUARED ID" - case .m: return "CIRCLED LATIN CAPITAL LETTER M" - case .new: return "SQUARED NEW" - case .ng: return "SQUARED NG" - case .o2: return "NEGATIVE SQUARED LATIN CAPITAL LETTER O" - case .ok: return "SQUARED OK" - case .parking: return "NEGATIVE SQUARED LATIN CAPITAL LETTER P" - case .sos: return "SQUARED SOS" - case .up: return "SQUARED UP WITH EXCLAMATION MARK" - case .vs: return "SQUARED VS" - case .koko: return "SQUARED KATAKANA KOKO" - case .sa: return "SQUARED KATAKANA SA" - case .u6708: return "SQUARED CJK UNIFIED IDEOGRAPH-6708" - case .u6709: return "SQUARED CJK UNIFIED IDEOGRAPH-6709" - case .u6307: return "SQUARED CJK UNIFIED IDEOGRAPH-6307" - case .ideographAdvantage: return "CIRCLED IDEOGRAPH ADVANTAGE" - case .u5272: return "SQUARED CJK UNIFIED IDEOGRAPH-5272" - case .u7121: return "SQUARED CJK UNIFIED IDEOGRAPH-7121" - case .u7981: return "SQUARED CJK UNIFIED IDEOGRAPH-7981" - case .accept: return "CIRCLED IDEOGRAPH ACCEPT" - case .u7533: return "SQUARED CJK UNIFIED IDEOGRAPH-7533" - case .u5408: return "SQUARED CJK UNIFIED IDEOGRAPH-5408" - case .u7a7a: return "SQUARED CJK UNIFIED IDEOGRAPH-7A7A" - case .congratulations: return "CIRCLED IDEOGRAPH CONGRATULATION" - case .secret: return "CIRCLED IDEOGRAPH SECRET" - case .u55b6: return "SQUARED CJK UNIFIED IDEOGRAPH-55B6" - case .u6e80: return "SQUARED CJK UNIFIED IDEOGRAPH-6E80" - case .redCircle: return "LARGE RED CIRCLE" - case .largeOrangeCircle: return "LARGE ORANGE CIRCLE" - case .largeYellowCircle: return "LARGE YELLOW CIRCLE" - case .largeGreenCircle: return "LARGE GREEN CIRCLE" - case .largeBlueCircle: return "LARGE BLUE CIRCLE" - case .largePurpleCircle: return "LARGE PURPLE CIRCLE" - case .largeBrownCircle: return "LARGE BROWN CIRCLE" - case .blackCircle: return "MEDIUM BLACK CIRCLE" - case .whiteCircle: return "MEDIUM WHITE CIRCLE" - case .largeRedSquare: return "LARGE RED SQUARE" - case .largeOrangeSquare: return "LARGE ORANGE SQUARE" - case .largeYellowSquare: return "LARGE YELLOW SQUARE" - case .largeGreenSquare: return "LARGE GREEN SQUARE" - case .largeBlueSquare: return "LARGE BLUE SQUARE" - case .largePurpleSquare: return "LARGE PURPLE SQUARE" - case .largeBrownSquare: return "LARGE BROWN SQUARE" - case .blackLargeSquare: return "BLACK LARGE SQUARE" - case .whiteLargeSquare: return "WHITE LARGE SQUARE" - case .blackMediumSquare: return "BLACK MEDIUM SQUARE" - case .whiteMediumSquare: return "WHITE MEDIUM SQUARE" - case .blackMediumSmallSquare: return "BLACK MEDIUM SMALL SQUARE" - case .whiteMediumSmallSquare: return "WHITE MEDIUM SMALL SQUARE" - case .blackSmallSquare: return "BLACK SMALL SQUARE" - case .whiteSmallSquare: return "WHITE SMALL SQUARE" - case .largeOrangeDiamond: return "LARGE ORANGE DIAMOND" - case .largeBlueDiamond: return "LARGE BLUE DIAMOND" - case .smallOrangeDiamond: return "SMALL ORANGE DIAMOND" - case .smallBlueDiamond: return "SMALL BLUE DIAMOND" - case .smallRedTriangle: return "UP-POINTING RED TRIANGLE" - case .smallRedTriangleDown: return "DOWN-POINTING RED TRIANGLE" - case .diamondShapeWithADotInside: return "DIAMOND SHAPE WITH A DOT INSIDE" - case .radioButton: return "RADIO BUTTON" - case .whiteSquareButton: return "WHITE SQUARE BUTTON" - case .blackSquareButton: return "BLACK SQUARE BUTTON" - case .checkeredFlag: return "CHEQUERED FLAG" - case .triangularFlagOnPost: return "TRIANGULAR FLAG ON POST" - case .crossedFlags: return "CROSSED FLAGS" - case .wavingBlackFlag: return "WAVING BLACK FLAG" - case .wavingWhiteFlag: return "WHITE FLAG" - case .rainbowFlag: return "RAINBOW FLAG" - case .transgenderFlag: return "TRANSGENDER FLAG" - case .pirateFlag: return "PIRATE FLAG" - case .flagAc: return "Ascension Island Flag" - case .flagAd: return "Andorra Flag" - case .flagAe: return "United Arab Emirates Flag" - case .flagAf: return "Afghanistan Flag" - case .flagAg: return "Antigua & Barbuda Flag" - case .flagAi: return "Anguilla Flag" - case .flagAl: return "Albania Flag" - case .flagAm: return "Armenia Flag" - case .flagAo: return "Angola Flag" - case .flagAq: return "Antarctica Flag" - case .flagAr: return "Argentina Flag" - case .flagAs: return "American Samoa Flag" - case .flagAt: return "Austria Flag" - case .flagAu: return "Australia Flag" - case .flagAw: return "Aruba Flag" - case .flagAx: return "ร…land Islands Flag" - case .flagAz: return "Azerbaijan Flag" - case .flagBa: return "Bosnia & Herzegovina Flag" - case .flagBb: return "Barbados Flag" - case .flagBd: return "Bangladesh Flag" - case .flagBe: return "Belgium Flag" - case .flagBf: return "Burkina Faso Flag" - case .flagBg: return "Bulgaria Flag" - case .flagBh: return "Bahrain Flag" - case .flagBi: return "Burundi Flag" - case .flagBj: return "Benin Flag" - case .flagBl: return "St. Barthรฉlemy Flag" - case .flagBm: return "Bermuda Flag" - case .flagBn: return "Brunei Flag" - case .flagBo: return "Bolivia Flag" - case .flagBq: return "Caribbean Netherlands Flag" - case .flagBr: return "Brazil Flag" - case .flagBs: return "Bahamas Flag" - case .flagBt: return "Bhutan Flag" - case .flagBv: return "Bouvet Island Flag" - case .flagBw: return "Botswana Flag" - case .flagBy: return "Belarus Flag" - case .flagBz: return "Belize Flag" - case .flagCa: return "Canada Flag" - case .flagCc: return "Cocos (Keeling) Islands Flag" - case .flagCd: return "Congo - Kinshasa Flag" - case .flagCf: return "Central African Republic Flag" - case .flagCg: return "Congo - Brazzaville Flag" - case .flagCh: return "Switzerland Flag" - case .flagCi: return "Cรดte dโ€™Ivoire Flag" - case .flagCk: return "Cook Islands Flag" - case .flagCl: return "Chile Flag" - case .flagCm: return "Cameroon Flag" - case .cn: return "China Flag" - case .flagCo: return "Colombia Flag" - case .flagCp: return "Clipperton Island Flag" - case .flagCr: return "Costa Rica Flag" - case .flagCu: return "Cuba Flag" - case .flagCv: return "Cape Verde Flag" - case .flagCw: return "Curaรงao Flag" - case .flagCx: return "Christmas Island Flag" - case .flagCy: return "Cyprus Flag" - case .flagCz: return "Czechia Flag" - case .de: return "Germany Flag" - case .flagDg: return "Diego Garcia Flag" - case .flagDj: return "Djibouti Flag" - case .flagDk: return "Denmark Flag" - case .flagDm: return "Dominica Flag" - case .flagDo: return "Dominican Republic Flag" - case .flagDz: return "Algeria Flag" - case .flagEa: return "Ceuta & Melilla Flag" - case .flagEc: return "Ecuador Flag" - case .flagEe: return "Estonia Flag" - case .flagEg: return "Egypt Flag" - case .flagEh: return "Western Sahara Flag" - case .flagEr: return "Eritrea Flag" - case .es: return "Spain Flag" - case .flagEt: return "Ethiopia Flag" - case .flagEu: return "European Union Flag" - case .flagFi: return "Finland Flag" - case .flagFj: return "Fiji Flag" - case .flagFk: return "Falkland Islands Flag" - case .flagFm: return "Micronesia Flag" - case .flagFo: return "Faroe Islands Flag" - case .fr: return "France Flag" - case .flagGa: return "Gabon Flag" - case .gb: return "United Kingdom Flag" - case .flagGd: return "Grenada Flag" - case .flagGe: return "Georgia Flag" - case .flagGf: return "French Guiana Flag" - case .flagGg: return "Guernsey Flag" - case .flagGh: return "Ghana Flag" - case .flagGi: return "Gibraltar Flag" - case .flagGl: return "Greenland Flag" - case .flagGm: return "Gambia Flag" - case .flagGn: return "Guinea Flag" - case .flagGp: return "Guadeloupe Flag" - case .flagGq: return "Equatorial Guinea Flag" - case .flagGr: return "Greece Flag" - case .flagGs: return "South Georgia & South Sandwich Islands Flag" - case .flagGt: return "Guatemala Flag" - case .flagGu: return "Guam Flag" - case .flagGw: return "Guinea-Bissau Flag" - case .flagGy: return "Guyana Flag" - case .flagHk: return "Hong Kong SAR China Flag" - case .flagHm: return "Heard & McDonald Islands Flag" - case .flagHn: return "Honduras Flag" - case .flagHr: return "Croatia Flag" - case .flagHt: return "Haiti Flag" - case .flagHu: return "Hungary Flag" - case .flagIc: return "Canary Islands Flag" - case .flagId: return "Indonesia Flag" - case .flagIe: return "Ireland Flag" - case .flagIl: return "Israel Flag" - case .flagIm: return "Isle of Man Flag" - case .flagIn: return "India Flag" - case .flagIo: return "British Indian Ocean Territory Flag" - case .flagIq: return "Iraq Flag" - case .flagIr: return "Iran Flag" - case .flagIs: return "Iceland Flag" - case .it: return "Italy Flag" - case .flagJe: return "Jersey Flag" - case .flagJm: return "Jamaica Flag" - case .flagJo: return "Jordan Flag" - case .jp: return "Japan Flag" - case .flagKe: return "Kenya Flag" - case .flagKg: return "Kyrgyzstan Flag" - case .flagKh: return "Cambodia Flag" - case .flagKi: return "Kiribati Flag" - case .flagKm: return "Comoros Flag" - case .flagKn: return "St. Kitts & Nevis Flag" - case .flagKp: return "North Korea Flag" - case .kr: return "South Korea Flag" - case .flagKw: return "Kuwait Flag" - case .flagKy: return "Cayman Islands Flag" - case .flagKz: return "Kazakhstan Flag" - case .flagLa: return "Laos Flag" - case .flagLb: return "Lebanon Flag" - case .flagLc: return "St. Lucia Flag" - case .flagLi: return "Liechtenstein Flag" - case .flagLk: return "Sri Lanka Flag" - case .flagLr: return "Liberia Flag" - case .flagLs: return "Lesotho Flag" - case .flagLt: return "Lithuania Flag" - case .flagLu: return "Luxembourg Flag" - case .flagLv: return "Latvia Flag" - case .flagLy: return "Libya Flag" - case .flagMa: return "Morocco Flag" - case .flagMc: return "Monaco Flag" - case .flagMd: return "Moldova Flag" - case .flagMe: return "Montenegro Flag" - case .flagMf: return "St. Martin Flag" - case .flagMg: return "Madagascar Flag" - case .flagMh: return "Marshall Islands Flag" - case .flagMk: return "North Macedonia Flag" - case .flagMl: return "Mali Flag" - case .flagMm: return "Myanmar (Burma) Flag" - case .flagMn: return "Mongolia Flag" - case .flagMo: return "Macao SAR China Flag" - case .flagMp: return "Northern Mariana Islands Flag" - case .flagMq: return "Martinique Flag" - case .flagMr: return "Mauritania Flag" - case .flagMs: return "Montserrat Flag" - case .flagMt: return "Malta Flag" - case .flagMu: return "Mauritius Flag" - case .flagMv: return "Maldives Flag" - case .flagMw: return "Malawi Flag" - case .flagMx: return "Mexico Flag" - case .flagMy: return "Malaysia Flag" - case .flagMz: return "Mozambique Flag" - case .flagNa: return "Namibia Flag" - case .flagNc: return "New Caledonia Flag" - case .flagNe: return "Niger Flag" - case .flagNf: return "Norfolk Island Flag" - case .flagNg: return "Nigeria Flag" - case .flagNi: return "Nicaragua Flag" - case .flagNl: return "Netherlands Flag" - case .flagNo: return "Norway Flag" - case .flagNp: return "Nepal Flag" - case .flagNr: return "Nauru Flag" - case .flagNu: return "Niue Flag" - case .flagNz: return "New Zealand Flag" - case .flagOm: return "Oman Flag" - case .flagPa: return "Panama Flag" - case .flagPe: return "Peru Flag" - case .flagPf: return "French Polynesia Flag" - case .flagPg: return "Papua New Guinea Flag" - case .flagPh: return "Philippines Flag" - case .flagPk: return "Pakistan Flag" - case .flagPl: return "Poland Flag" - case .flagPm: return "St. Pierre & Miquelon Flag" - case .flagPn: return "Pitcairn Islands Flag" - case .flagPr: return "Puerto Rico Flag" - case .flagPs: return "Palestinian Territories Flag" - case .flagPt: return "Portugal Flag" - case .flagPw: return "Palau Flag" - case .flagPy: return "Paraguay Flag" - case .flagQa: return "Qatar Flag" - case .flagRe: return "Rรฉunion Flag" - case .flagRo: return "Romania Flag" - case .flagRs: return "Serbia Flag" - case .ru: return "Russia Flag" - case .flagRw: return "Rwanda Flag" - case .flagSa: return "Saudi Arabia Flag" - case .flagSb: return "Solomon Islands Flag" - case .flagSc: return "Seychelles Flag" - case .flagSd: return "Sudan Flag" - case .flagSe: return "Sweden Flag" - case .flagSg: return "Singapore Flag" - case .flagSh: return "St. Helena Flag" - case .flagSi: return "Slovenia Flag" - case .flagSj: return "Svalbard & Jan Mayen Flag" - case .flagSk: return "Slovakia Flag" - case .flagSl: return "Sierra Leone Flag" - case .flagSm: return "San Marino Flag" - case .flagSn: return "Senegal Flag" - case .flagSo: return "Somalia Flag" - case .flagSr: return "Suriname Flag" - case .flagSs: return "South Sudan Flag" - case .flagSt: return "Sรฃo Tomรฉ & Prรญncipe Flag" - case .flagSv: return "El Salvador Flag" - case .flagSx: return "Sint Maarten Flag" - case .flagSy: return "Syria Flag" - case .flagSz: return "Eswatini Flag" - case .flagTa: return "Tristan da Cunha Flag" - case .flagTc: return "Turks & Caicos Islands Flag" - case .flagTd: return "Chad Flag" - case .flagTf: return "French Southern Territories Flag" - case .flagTg: return "Togo Flag" - case .flagTh: return "Thailand Flag" - case .flagTj: return "Tajikistan Flag" - case .flagTk: return "Tokelau Flag" - case .flagTl: return "Timor-Leste Flag" - case .flagTm: return "Turkmenistan Flag" - case .flagTn: return "Tunisia Flag" - case .flagTo: return "Tonga Flag" - case .flagTr: return "Turkey Flag" - case .flagTt: return "Trinidad & Tobago Flag" - case .flagTv: return "Tuvalu Flag" - case .flagTw: return "Taiwan Flag" - case .flagTz: return "Tanzania Flag" - case .flagUa: return "Ukraine Flag" - case .flagUg: return "Uganda Flag" - case .flagUm: return "U.S. Outlying Islands Flag" - case .flagUn: return "United Nations Flag" - case .us: return "United States Flag" - case .flagUy: return "Uruguay Flag" - case .flagUz: return "Uzbekistan Flag" - case .flagVa: return "Vatican City Flag" - case .flagVc: return "St. Vincent & Grenadines Flag" - case .flagVe: return "Venezuela Flag" - case .flagVg: return "British Virgin Islands Flag" - case .flagVi: return "U.S. Virgin Islands Flag" - case .flagVn: return "Vietnam Flag" - case .flagVu: return "Vanuatu Flag" - case .flagWf: return "Wallis & Futuna Flag" - case .flagWs: return "Samoa Flag" - case .flagXk: return "Kosovo Flag" - case .flagYe: return "Yemen Flag" - case .flagYt: return "Mayotte Flag" - case .flagZa: return "South Africa Flag" - case .flagZm: return "Zambia Flag" - case .flagZw: return "Zimbabwe Flag" - case .flagEngland: return "England Flag" - case .flagScotland: return "Scotland Flag" - case .flagWales: return "Wales Flag" + 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" } } } From bd0614532ee32bde31d7a5d043153c11f012d406 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 1 Sep 2022 11:40:34 +1000 Subject: [PATCH 132/133] feat: add cancel button for emoji search bar --- .../Conversations/Emoji Picker/EmojiPickerSheet.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift index 621d66940..f2c935f1f 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -24,7 +24,6 @@ class EmojiPickerSheet: BaseVC { result.tintColor = Colors.text result.backgroundColor = .clear result.delegate = self - result.showsCancelButton = true return result }() @@ -125,5 +124,15 @@ 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() + } } From 85fc22110f6e1bd07a97c32b30b9a03d69e10bb4 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 1 Sep 2022 15:09:02 +1000 Subject: [PATCH 133/133] fix selected emoji not shown in the popup list --- .../Conversations/Views & Modals/ReactionListSheet.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Session/Conversations/Views & Modals/ReactionListSheet.swift b/Session/Conversations/Views & Modals/ReactionListSheet.swift index 8cc1f29d0..ab501de58 100644 --- a/Session/Conversations/Views & Modals/ReactionListSheet.swift +++ b/Session/Conversations/Views & Modals/ReactionListSheet.swift @@ -132,6 +132,15 @@ final class ReactionListSheet: BaseVC { 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)