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.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 39731d7e0..03190e540 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -117,8 +117,10 @@ 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 */; }; + 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; }; 7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; }; 7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */; }; + 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 7B50D64C28AC7CF80086CCEC /* silence.aiff */; }; 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */; }; 7B7037452834BCC0000DCF35 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7037442834BCC0000DCF35 /* ReactionView.swift */; }; 7B7CB18E270D066F0079FF93 /* IncomingCallBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */; }; @@ -126,6 +128,8 @@ 7B7CB192271508AD0079FF93 /* CallRingTonePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */; }; 7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682228A4C1210069F315 /* UpdateTypes.swift */; }; 7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */; }; + 7B81682A28B6F1420069F315 /* ReactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682928B6F1420069F315 /* ReactionResponse.swift */; }; + 7B81682C28B72F480069F315 /* PendingChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B81682B28B72F480069F315 /* PendingChange.swift */; }; 7B8D5FC428332600008324D9 /* VisibleMessage+Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */; }; 7B93D06A27CF173D00811CB6 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */; }; 7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */; }; @@ -150,6 +154,7 @@ 7BAF54D427ACCF01003D12F8 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */; }; 7BAF54D827ACD0E3003D12F8 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */; }; 7BAF54DC27ACD12B003D12F8 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */; }; + 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBBDC452875600700747E59 /* DocumentTitleViewController.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, ); }; }; @@ -1191,8 +1196,10 @@ 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 = ""; }; 7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = ""; }; + 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; 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 = ""; }; + 7B50D64C28AC7CF80086CCEC /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = ""; }; 7B7037422834B81F000DCF35 /* ReactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContainerView.swift; sourceTree = ""; }; 7B7037442834BCC0000DCF35 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 7B7CB18D270D066F0079FF93 /* IncomingCallBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallBanner.swift; sourceTree = ""; }; @@ -1200,6 +1207,8 @@ 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallRingTonePlayer.swift; sourceTree = ""; }; 7B81682228A4C1210069F315 /* UpdateTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTypes.swift; sourceTree = ""; }; 7B81682728B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = ""; }; + 7B81682928B6F1420069F315 /* ReactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionResponse.swift; sourceTree = ""; }; + 7B81682B28B72F480069F315 /* PendingChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingChange.swift; sourceTree = ""; }; 7B8D5FC328332600008324D9 /* VisibleMessage+Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Reaction.swift"; sourceTree = ""; }; 7B93D06927CF173D00811CB6 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; 7B93D06E27CF194000811CB6 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; @@ -1224,6 +1233,7 @@ 7BAF54D227ACCF01003D12F8 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; 7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; 7BAF54DB27ACD12B003D12F8 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; + 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentTitleViewController.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 = ""; }; @@ -2144,6 +2154,7 @@ 34074FC5203E5435004596AE /* messageReceivedSounds */ = { isa = PBXGroup; children = ( + 7B50D64C28AC7CF80086CCEC /* silence.aiff */, 45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */, 45B74A6F2044AAB500CD42F8 /* aurora.aifc */, 45B74A5F2044AAB400CD42F8 /* bamboo-quiet.aifc */, @@ -3007,6 +3018,7 @@ FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */, 45F32C1D205718B000A300D5 /* MediaPageViewController.swift */, 454A84032059C787008B8C75 /* MediaTileViewController.swift */, + 7BBBDC452875600700747E59 /* DocumentTitleViewController.swift */, 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */, 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */, 34969559219B605E00DCFE74 /* ImagePickerController.swift */, @@ -3017,6 +3029,7 @@ 4C21D5D7223AC60F00EF8A77 /* PhotoCapture.swift */, 4C1885D1218F8E1C00B67051 /* PhotoGridViewCell.swift */, 4C4AE69F224AF21900D4AF6F /* SendMediaNavigationController.swift */, + 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */, ); path = "Media Viewing & Editing"; sourceTree = ""; @@ -4029,6 +4042,8 @@ FDC438A327BB107F00C60D73 /* UserBanRequest.swift */, FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */, FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */, + 7B81682928B6F1420069F315 /* ReactionResponse.swift */, + 7B81682B28B72F480069F315 /* PendingChange.swift */, ); path = Models; sourceTree = ""; @@ -4756,6 +4771,7 @@ 45B74A872044AAB600CD42F8 /* complete-quiet.aifc in Resources */, 45B74A772044AAB600CD42F8 /* hello.aifc in Resources */, 45B74A7C2044AAB600CD42F8 /* hello-quiet.aifc in Resources */, + 7B50D64D28AC7CF80086CCEC /* silence.aiff in Resources */, 45B74A792044AAB600CD42F8 /* input.aifc in Resources */, C3CA3ABE255CDB0D00F4C6D4 /* portuguese.txt in Resources */, 45B74A8C2044AAB600CD42F8 /* input-quiet.aifc in Resources */, @@ -5451,6 +5467,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 */, FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */, @@ -5465,6 +5482,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 */, @@ -5690,6 +5708,7 @@ FDFDE124282D04F20098B17F /* MediaDismissAnimationController.swift in Sources */, 7BA6890F27325CE300EFC32F /* SessionCallManager+CXProvider.swift in Sources */, 34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */, + 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */, C331FFFE2558FF3B00070591 /* FullConversationCell.swift in Sources */, FD52090728B49738006098F6 /* ConfirmationModal.swift in Sources */, C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */, @@ -5699,6 +5718,9 @@ B82B408E239DC00D00A248E7 /* DisplayNameVC.swift in Sources */, B835246E25C38ABF0089A44F /* ConversationVC.swift in Sources */, 7B7037432834B81F000DCF35 /* ReactionContainerView.swift in Sources */, + B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */, + B821494625D4D6FF009C0F2A /* URLModal.swift in Sources */, + 7BBBDC462875600700747E59 /* DocumentTitleViewController.swift in Sources */, B877E24226CA12910007970A /* CallVC.swift in Sources */, 7BA6890D27325CCC00EFC32F /* SessionCallManager+CXCallController.swift in Sources */, C374EEEB25DA3CA70073A857 /* ConversationTitleView.swift in Sources */, @@ -6071,7 +6093,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6144,7 +6166,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6210,7 +6232,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6284,7 +6306,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 372; + CURRENT_PROJECT_VERSION = 374; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -7222,7 +7244,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 = 374; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7294,7 +7316,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 = 374; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/Session/Calls/Call Management/SessionCall.swift b/Session/Calls/Call Management/SessionCall.swift index 1aef15a69..87a3aed2a 100644 --- a/Session/Calls/Call Management/SessionCall.swift +++ b/Session/Calls/Call Management/SessionCall.swift @@ -71,6 +71,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { var connectingDate: Date? { didSet { stateDidChange?() + resetTimeoutTimerIfNeeded() hasStartedConnectingDidChange?() } } @@ -113,12 +114,12 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { set { connectingDate = newValue ? Date() : nil } } - var hasConnected: Bool { + public var hasConnected: Bool { get { return connectedDate != nil } set { connectedDate = newValue ? Date() : nil } } - var hasEnded: Bool { + public var hasEnded: Bool { get { return endDate != nil } set { endDate = newValue ? Date() : nil } } @@ -277,55 +278,60 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { let duration: TimeInterval = self.duration let hasStartedConnecting: Bool = self.hasStartedConnecting - Storage.shared.writeAsync { db in - guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { - return - } - - let updateToMissedIfNeeded: () throws -> () = { - let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed) - - guard - let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), - let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( - CallMessage.MessageInfo.self, - from: infoMessageData - ), - messageInfo.state == .incoming, - let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) - else { return } - - _ = try interaction - .with(body: String(data: missedCallInfoData, encoding: .utf8)) - .saved(db) - } - let shouldMarkAsRead: Bool = try { - if duration > 0 { return true } - if hasStartedConnecting { return true } - - switch mode { - case .local: - try updateToMissedIfNeeded() - return true - - case .remote, .unanswered: - try updateToMissedIfNeeded() - return false - - case .answeredElsewhere: return true + Storage.shared.writeAsync( + updates: { db in + guard let interaction: Interaction = try? Interaction.fetchOne(db, id: callInteractionId) else { + return } - }() - - guard shouldMarkAsRead else { return } - - try Interaction.markAsRead( - db, - interactionId: interaction.id, - threadId: interaction.threadId, - includingOlder: false, - trySendReadReceipt: false - ) - } + + let updateToMissedIfNeeded: () throws -> () = { + let missedCallInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed) + + guard + let infoMessageData: Data = (interaction.body ?? "").data(using: .utf8), + let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( + CallMessage.MessageInfo.self, + from: infoMessageData + ), + messageInfo.state == .incoming, + let missedCallInfoData: Data = try? JSONEncoder().encode(missedCallInfo) + else { return } + + _ = try interaction + .with(body: String(data: missedCallInfoData, encoding: .utf8)) + .saved(db) + } + let shouldMarkAsRead: Bool = try { + if duration > 0 { return true } + if hasStartedConnecting { return true } + + switch mode { + case .local: + try updateToMissedIfNeeded() + return true + + case .remote, .unanswered: + try updateToMissedIfNeeded() + return false + + case .answeredElsewhere: return true + } + }() + + guard shouldMarkAsRead else { return } + + try Interaction.markAsRead( + db, + interactionId: interaction.id, + threadId: interaction.threadId, + includingOlder: false, + trySendReadReceipt: false + ) + }, + completion: { _, _ in + SessionCallManager.suspendDatabaseIfCallEndedInBackground() + } + ) } // MARK: - Renderer @@ -421,6 +427,11 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate { } } + public func resetTimeoutTimerIfNeeded() { + if self.timeOutTimer == nil { return } + setupTimeoutTimer() + } + public func invalidateTimeoutTimer() { timeOutTimer?.invalidate() timeOutTimer = nil diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index 643268bc1..38ecd7b75 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -73,13 +73,19 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // MARK: - Report calls public static func reportFakeCall(info: String) { - SessionCallManager.sharedProvider(useSystemCallLog: false) - .reportNewIncomingCall( - with: UUID(), - update: CXCallUpdate() - ) { _ in - SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") - } + let callId = UUID() + let provider = SessionCallManager.sharedProvider(useSystemCallLog: false) + provider.reportNewIncomingCall( + with: callId, + update: CXCallUpdate() + ) { _ in + SNLog("[Calls] Reported fake incoming call to CallKit due to: \(info)") + } + provider.reportCall( + with: callId, + endedAt: nil, + reason: .failed + ) } public func reportOutgoingCall(_ call: SessionCall) { @@ -98,30 +104,22 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } public func reportIncomingCall(_ call: SessionCall, callerName: String, completion: @escaping (Error?) -> Void) { - AssertIsOnMainThread() - - if let provider = provider { - // Construct a CXCallUpdate describing the incoming call, including the caller. - let update = CXCallUpdate() - update.localizedCallerName = callerName - update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) - update.hasVideo = false + let provider = provider ?? Self.sharedProvider(useSystemCallLog: false) + // Construct a CXCallUpdate describing the incoming call, including the caller. + let update = CXCallUpdate() + update.localizedCallerName = callerName + update.remoteHandle = CXHandle(type: .generic, value: call.callId.uuidString) + update.hasVideo = false - disableUnsupportedFeatures(callUpdate: update) + disableUnsupportedFeatures(callUpdate: update) - // Report the incoming call to the system - provider.reportNewIncomingCall(with: call.callId, update: update) { error in - guard error == nil else { - self.reportCurrentCallEnded(reason: .failed) - completion(error) - return - } - UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") - completion(nil) + // Report the incoming call to the system + provider.reportNewIncomingCall(with: call.callId, update: update) { error in + guard error == nil else { + self.reportCurrentCallEnded(reason: .failed) + completion(error) + return } - } - else { - SessionCallManager.reportFakeCall(info: "No CXProvider instance") UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") completion(nil) } @@ -135,7 +133,16 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { return } - guard let call = currentCall else { return } + func handleCallEnded() { + WebRTCSession.current = nil + UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + } + + guard let call = currentCall else { + handleCallEnded() + Self.suspendDatabaseIfCallEndedInBackground() + return + } if let reason = reason { self.provider?.reportCall(with: call.callId, endedAt: nil, reason: reason) @@ -153,8 +160,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { call.webRTCSession.dropConnection() self.currentCall = nil - WebRTCSession.current = nil - UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + handleCallEnded() } // MARK: - Util @@ -172,15 +178,18 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { callUpdate.supportsDTMF = false } + public static func suspendDatabaseIfCallEndedInBackground() { + if CurrentAppContext().isInBackground() { + // 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) + } + } + } + // MARK: - UI public func showCallUIForCall(caller: String, uuid: String, mode: CallMode, interactionId: Int64?) { - guard Thread.isMainThread else { - DispatchQueue.main.async { - self.showCallUIForCall(caller: caller, uuid: uuid, mode: mode, interactionId: interactionId) - } - return - } guard let call: SessionCall = Storage.shared.read({ db in SessionCall(db, for: caller, uuid: uuid, mode: mode) }) else { return } @@ -193,20 +202,23 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { } guard CurrentAppContext().isMainAppAndActive else { return } - guard let presentingVC = CurrentAppContext().frontmostViewController() else { - preconditionFailure() // FIXME: Handle more gracefully - } - if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId { - let callVC = CallVC(for: call) - callVC.conversationVC = conversationVC - conversationVC.inputAccessoryView?.isHidden = true - conversationVC.inputAccessoryView?.alpha = 0 - presentingVC.present(callVC, animated: true, completion: nil) - } - else if !Preferences.isCallKitSupported { - let incomingCallBanner = IncomingCallBanner(for: call) - incomingCallBanner.show() + DispatchQueue.main.async { + guard let presentingVC = CurrentAppContext().frontmostViewController() else { + preconditionFailure() // FIXME: Handle more gracefully + } + + if let conversationVC: ConversationVC = presentingVC as? ConversationVC, conversationVC.viewModel.threadData.threadId == call.sessionId { + let callVC = CallVC(for: call) + callVC.conversationVC = conversationVC + conversationVC.inputAccessoryView?.isHidden = true + conversationVC.inputAccessoryView?.alpha = 0 + presentingVC.present(callVC, animated: true, completion: nil) + } + else if !Preferences.isCallKitSupported { + let incomingCallBanner = IncomingCallBanner(for: call) + incomingCallBanner.show() + } } } } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 09e89f79a..894df7557 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -747,7 +747,7 @@ extension ConversationVC: .elements .firstIndex(of: cellViewModel), let cell = tableView.cellForRow(at: IndexPath(row: index, section: sectionIndex)) as? VisibleMessageCell, - let snapshot = cell.bubbleView.snapshotView(afterScreenUpdates: false), + let snapshot = cell.snContentView.snapshotView(afterScreenUpdates: false), contextMenuWindow == nil, let actions: [ContextMenuVC.Action] = ContextMenuVC.actions( for: cellViewModel, @@ -769,7 +769,7 @@ extension ConversationVC: self.contextMenuWindow = ContextMenuWindow() self.contextMenuVC = ContextMenuVC( snapshot: snapshot, - frame: cell.convert(cell.bubbleView.frame, to: keyWindow), + frame: cell.convert(cell.snContentView.frame, to: keyWindow), cellViewModel: cellViewModel, actions: actions ) { [weak self] in @@ -1145,6 +1145,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, @@ -1153,7 +1162,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 @@ -1201,29 +1216,42 @@ 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 Reaction + 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) + try pendingReaction?.insert(db) // Add it to the recent list Emoji.addRecent(db, emoji: emoji) @@ -1242,6 +1270,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, @@ -1250,9 +1286,34 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } + .catch { [weak self] _ in + OpenGroupManager.removePendingChange(pendingChange) + + self?.handleReactionSentFailure( + pendingReaction, + remove: remove + ) + + } .retainUntilComplete() } else { + let pendingChange = OpenGroupManager + .addPendingReaction( + emoji: emoji, + id: openGroupServerMessageId, + in: openGroup.roomToken, + on: openGroup.server, + type: .add + ) + OpenGroupAPI .reactionAdd( db, @@ -1261,6 +1322,21 @@ extension ConversationVC: in: openGroup.roomToken, on: openGroup.server ) + .map { _, response in + OpenGroupManager + .updatePendingChange( + pendingChange, + seqNo: response.seqNo + ) + } + .catch { [weak self] _ in + OpenGroupManager.removePendingChange(pendingChange) + + self?.handleReactionSentFailure( + pendingReaction, + remove: remove + ) + } .retainUntilComplete() } } @@ -1292,6 +1368,23 @@ 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) + } + else { + try Reaction + .filter(Reaction.Columns.interactionId == pendingReaction.interactionId) + .filter(Reaction.Columns.authorId == pendingReaction.authorId) + .filter(Reaction.Columns.emoji == pendingReaction.emoji) + .deleteAll(db) + } + } + } + func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) { hideInputAccessoryView() @@ -2113,6 +2206,35 @@ extension ConversationVC { preferredStyle: .actionSheet ) alertVC.addAction(UIAlertAction(title: "TXT_DELETE_TITLE".localized(), style: .destructive) { _ in + // Delete the request + Storage.shared.writeAsync( + updates: { db in + _ = try SessionThread + .filter(id: threadId) + .deleteAll(db) + }, + completion: { db, _ in + DispatchQueue.main.async { [weak self] in + self?.navigationController?.popViewController(animated: true) + } + } + ) + }) + alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil)) + + self.present(alertVC, animated: true, completion: nil) + } + + @objc func block() { + guard self.viewModel.threadData.threadVariant == .contact else { return } + + let threadId: String = self.viewModel.threadData.threadId + let alertVC: UIAlertController = UIAlertController( + title: "MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON".localized(), + message: nil, + preferredStyle: .actionSheet + ) + alertVC.addAction(UIAlertAction(title: "BLOCK_LIST_BLOCK_BUTTON".localized(), style: .destructive) { _ in // Delete the request Storage.shared.writeAsync( updates: { db in diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index dff584305..68eb87870 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -245,11 +245,23 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl private lazy var messageRequestDeleteButton: UIButton = { let result: OutlineButton = OutlineButton(style: .destructive, size: .medium) result.translatesAutoresizingMaskIntoConstraints = false - result.setTitle("TXT_DELETE_TITLE".localized(), for: .normal) + result.setTitle("TXT_DECLINE_TITLE".localized(), for: .normal) result.addTarget(self, action: #selector(deleteMessageRequest), for: .touchUpInside) return result }() + + private lazy var messageRequestBlockButton: UIButton = { + let result: UIButton = UIButton() + result.translatesAutoresizingMaskIntoConstraints = false + result.clipsToBounds = true + result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) + result.setTitle("TXT_BLOCK_USER_TITLE".localized(), for: .normal) + result.setTitleColor(Colors.destructive, for: .normal) + result.addTarget(self, action: #selector(block), for: .touchUpInside) + + return result + }() // MARK: - Settings @@ -305,6 +317,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl view.addSubview(scrollButton) view.addSubview(messageRequestView) + messageRequestView.addSubview(messageRequestBlockButton) messageRequestView.addSubview(messageRequestDescriptionLabel) messageRequestView.addSubview(messageRequestAcceptButton) messageRequestView.addSubview(messageRequestDeleteButton) @@ -317,7 +330,10 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl self.scrollButtonBottomConstraint?.isActive = false // Note: Need to disable this to avoid a conflict with the other bottom constraint self.scrollButtonMessageRequestsBottomConstraint = scrollButton.pin(.bottom, to: .top, of: messageRequestView, withInset: -16) - messageRequestDescriptionLabel.pin(.top, to: .top, of: messageRequestView, withInset: 10) + messageRequestBlockButton.pin(.top, to: .top, of: messageRequestView, withInset: 10) + messageRequestBlockButton.center(.horizontal, in: messageRequestView) + + messageRequestDescriptionLabel.pin(.top, to: .bottom, of: messageRequestBlockButton, withInset: 5) messageRequestDescriptionLabel.pin(.left, to: .left, of: messageRequestView, withInset: 40) messageRequestDescriptionLabel.pin(.right, to: .right, of: messageRequestView, withInset: -40) diff --git a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift index 638a0f8fa..ecf7ae71b 100644 --- a/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift +++ b/Session/Conversations/Emoji Picker/EmojiPickerSheet.swift @@ -228,5 +228,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() + } } diff --git a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift index 1c785fcf5..846e0aa72 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaAlbumView.swift @@ -8,8 +8,8 @@ public class MediaAlbumView: UIStackView { public let itemViews: [MediaView] public var moreItemsView: MediaView? - private static let kSpacingPts: CGFloat = 2 - private static let kMaxItems = 5 + private static let kSpacingPts: CGFloat = 4 + private static let kMaxItems = 3 @available(*, unavailable, message: "use other init() instead.") required public init(coder aDecoder: NSCoder) { @@ -70,8 +70,8 @@ public class MediaAlbumView: UIStackView { self.axis = .horizontal self.distribution = .fillEqually self.spacing = MediaAlbumView.kSpacingPts - - case 3: + + default: // x // X x // Big on left, 2 small on right. @@ -95,64 +95,9 @@ public class MediaAlbumView: UIStackView { ) self.axis = .horizontal self.spacing = MediaAlbumView.kSpacingPts - - case 4: - // X X - // X X - // Square - let imageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2 - - let topViews = Array(itemViews[0..<2]) - addArrangedSubview( - newRow( - rowViews: topViews, - axis: .horizontal, - viewSize: imageSize - ) - ) - - let bottomViews = Array(itemViews[2..<4]) - addArrangedSubview( - newRow( - rowViews: bottomViews, - axis: .horizontal, - viewSize: imageSize - ) - ) - - self.axis = .vertical - self.spacing = MediaAlbumView.kSpacingPts - - default: - // X X - // xxx - // 2 big on top, 3 small on bottom. - let bigImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts) / 2 - let smallImageSize = (maxMessageWidth - MediaAlbumView.kSpacingPts * 2) / 3 - - let topViews = Array(itemViews[0..<2]) - addArrangedSubview( - newRow( - rowViews: topViews, - axis: .horizontal, - viewSize: bigImageSize - ) - ) - - let bottomViews = Array(itemViews[2..<5]) - addArrangedSubview( - newRow( - rowViews: bottomViews, - axis: .horizontal, - viewSize: smallImageSize - ) - ) - - self.axis = .vertical - self.spacing = MediaAlbumView.kSpacingPts if items.count > MediaAlbumView.kMaxItems { - guard let lastView = bottomViews.last else { + guard let lastView = rightViews.last else { owsFailDebug("Missing lastView") return } @@ -267,13 +212,8 @@ public class MediaAlbumView: UIStackView { let itemCount = itemsToDisplay(forItems: items).count switch itemCount { - case 0, 1, 4: + case 0, 1: // X - // - // or - // - // XX - // XX // Square return CGSize(width: maxMessageWidth, height: maxMessageWidth) @@ -283,21 +223,13 @@ public class MediaAlbumView: UIStackView { let imageSize = (maxMessageWidth - kSpacingPts) / 2 return CGSize(width: maxMessageWidth, height: imageSize) - case 3: + default: // x // X x // Big on left, 2 small on right. let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3 let bigImageSize = smallImageSize * 2 + kSpacingPts return CGSize(width: maxMessageWidth, height: bigImageSize) - - default: - // X X - // xxx - // 2 big on top, 3 small on bottom. - let bigImageSize = (maxMessageWidth - kSpacingPts) / 2 - let smallImageSize = (maxMessageWidth - kSpacingPts * 2) / 3 - return CGSize(width: maxMessageWidth, height: bigImageSize + smallImageSize + kSpacingPts) } } diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 556c87d14..301818a8b 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -60,6 +60,8 @@ public class MediaView: UIView { themeBackgroundColor = .backgroundSecondary clipsToBounds = true + layer.masksToBounds = true + layer.cornerRadius = VisibleMessageCell.largeCornerRadius createContents() } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 9df3bc5df..02aa81f04 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -20,15 +20,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { 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 contentViewLeftConstraint1 = snContentView.pin(.left, to: .right, of: profilePictureView, withInset: VisibleMessageCell.groupThreadHSpacing) + private lazy var contentViewLeftConstraint2 = snContentView.leftAnchor.constraint(greaterThanOrEqualTo: leftAnchor, constant: VisibleMessageCell.gutterSize) + private lazy var contentViewTopConstraint = snContentView.pin(.top, to: .bottom, of: authorLabel, withInset: VisibleMessageCell.authorLabelBottomSpacing) + private lazy var contentViewRightConstraint1 = snContentView.pin(.right, to: .right, of: self, withInset: -VisibleMessageCell.contactThreadHSpacing) + private lazy var contentViewRightConstraint2 = snContentView.rightAnchor.constraint(lessThanOrEqualTo: rightAnchor, constant: -VisibleMessageCell.gutterSize) - 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 reactionContainerViewLeftConstraint = reactionContainerView.pin(.left, to: .left, of: snContentView) + private lazy var reactionContainerViewRightConstraint = reactionContainerView.pin(.right, to: .right, of: snContentView) private lazy var messageStatusImageViewTopConstraint = messageStatusImageView.pin(.top, to: .bottom, of: reactionContainerView, withInset: 0) private lazy var messageStatusImageViewWidthConstraint = messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize) @@ -46,9 +45,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // MARK: - UI Components private lazy var viewsToMoveForReply: [UIView] = [ - bubbleView, - bubbleBackgroundView, + snContentView, profilePictureView, + moderatorIconImageView, replyButton, timerView, messageStatusImageView, @@ -87,7 +86,14 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return result }() - private lazy var snContentView = UIView() + lazy var snContentView: UIStackView = { + let result = UIStackView(arrangedSubviews: []) + result.axis = .vertical + result.spacing = Values.verySmallSpacing + result.alignment = .leading + return result + }() + private lazy var reactionContainerView = ReactionContainerView() internal lazy var messageStatusImageView: UIImageView = { @@ -149,6 +155,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { return result }() + static var leftGutterSize: CGFloat { groupThreadHSpacing + profilePictureSize + groupThreadHSpacing } + // MARK: Direction & Position enum Direction { case incoming, outgoing } @@ -180,35 +188,31 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { moderatorIconImageView.pin(.trailing, to: .trailing, of: profilePictureView, withInset: 1) moderatorIconImageView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: 4.5) - // Bubble background view (used for the 'highlighted' animation) - addSubview(bubbleBackgroundView) + // Content view + addSubview(snContentView) + contentViewLeftConstraint1.isActive = true + contentViewTopConstraint.isActive = true + contentViewRightConstraint1.isActive = true + snContentView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1) - // Bubble view - addSubview(bubbleView) - bubbleViewLeftConstraint1.isActive = true - bubbleViewTopConstraint.isActive = true - bubbleViewRightConstraint1.isActive = true - bubbleView.pin(.bottom, to: .bottom, of: profilePictureView, withInset: -1) + // Bubble background view + bubbleBackgroundView.addSubview(bubbleView) bubbleBackgroundView.pin(to: bubbleView) // Timer view addSubview(timerView) - timerView.center(.vertical, in: bubbleView) + timerView.center(.vertical, in: snContentView) timerViewOutgoingMessageConstraint.isActive = true - // Content view - bubbleView.addSubview(snContentView) - snContentView.pin(to: bubbleView) - // Reaction view addSubview(reactionContainerView) - reactionContainerView.pin(.top, to: .bottom, of: bubbleView, withInset: Values.verySmallSpacing) + reactionContainerView.pin(.top, to: .bottom, of: snContentView, withInset: Values.verySmallSpacing) reactionContainerViewLeftConstraint.isActive = true // Message status image view addSubview(messageStatusImageView) messageStatusImageViewTopConstraint.isActive = true - messageStatusImageView.pin(.right, to: .right, of: bubbleView, withInset: -1) + messageStatusImageView.pin(.right, to: .right, of: snContentView, withInset: -1) messageStatusImageView.pin(.bottom, to: .bottom, of: self, withInset: -1) messageStatusImageViewWidthConstraint.isActive = true messageStatusImageViewHeightConstraint.isActive = true @@ -217,11 +221,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { addSubview(replyButton) replyButton.addSubview(replyIconImageView) replyIconImageView.center(in: replyButton) - replyButton.pin(.left, to: .right, of: bubbleView, withInset: Values.smallSpacing) - replyButton.center(.vertical, in: bubbleView) + replyButton.pin(.left, to: .right, of: snContentView, withInset: Values.smallSpacing) + replyButton.center(.vertical, in: snContentView) // Remaining constraints - authorLabel.pin(.left, to: .left, of: bubbleView, withInset: VisibleMessageCell.authorLabelInset) + authorLabel.pin(.left, to: .left, of: snContentView, withInset: VisibleMessageCell.authorLabelInset) } override func setUpGestureRecognizers() { @@ -270,15 +274,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { moderatorIconImageView.isHidden = (!cellViewModel.isSenderOpenGroupModerator || !cellViewModel.shouldShowProfile) // Bubble view - bubbleViewLeftConstraint1.isActive = ( + contentViewLeftConstraint1.isActive = ( cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardIncomingDeleted ) - bubbleViewLeftConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing) - bubbleViewLeftConstraint2.isActive = (cellViewModel.variant == .standardOutgoing) - bubbleViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing) - bubbleViewRightConstraint1.isActive = (cellViewModel.variant == .standardOutgoing) - bubbleViewRightConstraint2.isActive = ( + contentViewLeftConstraint1.constant = (isGroupThread ? VisibleMessageCell.groupThreadHSpacing : VisibleMessageCell.contactThreadHSpacing) + contentViewLeftConstraint2.isActive = (cellViewModel.variant == .standardOutgoing) + contentViewTopConstraint.constant = (cellViewModel.senderName == nil ? 0 : VisibleMessageCell.authorLabelBottomSpacing) + contentViewRightConstraint1.isActive = (cellViewModel.variant == .standardOutgoing) + contentViewRightConstraint2.isActive = ( cellViewModel.variant == .standardIncoming || cellViewModel.variant == .standardIncomingDeleted ) @@ -406,23 +410,36 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { .messageBubble_incomingText ) - snContentView.subviews.forEach { $0.removeFromSuperview() } + snContentView.alignment = (cellViewModel.variant == .standardOutgoing ? + .trailing : + .leading + ) + + for subview in snContentView.arrangedSubviews { + snContentView.removeArrangedSubview(subview) + subview.removeFromSuperview() + } + for subview in bubbleView.subviews { + subview.removeFromSuperview() + } albumView = nil bodyTappableLabel = nil // Handle the deleted state first (it's much simpler than the others) guard cellViewModel.variant != .standardIncomingDeleted else { let deletedMessageView: DeletedMessageView = DeletedMessageView(textColor: bodyLabelTextColor) - snContentView.addSubview(deletedMessageView) - deletedMessageView.pin(to: snContentView) + bubbleView.addSubview(deletedMessageView) + deletedMessageView.pin(to: bubbleView) + snContentView.addArrangedSubview(bubbleBackgroundView) return } // If it's an incoming media message and the thread isn't trusted then show the placeholder view if cellViewModel.cellType != .textOnlyMessage && cellViewModel.variant == .standardIncoming && !cellViewModel.threadIsTrusted { let mediaPlaceholderView = MediaPlaceholderView(cellViewModel: cellViewModel, textColor: bodyLabelTextColor) - snContentView.addSubview(mediaPlaceholderView) - mediaPlaceholderView.pin(to: snContentView) + bubbleView.addSubview(mediaPlaceholderView) + mediaPlaceholderView.pin(to: bubbleView) + snContentView.addArrangedSubview(bubbleBackgroundView) return } @@ -448,8 +465,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { bodyLabelTextColor: bodyLabelTextColor, lastSearchText: lastSearchText ) - snContentView.addSubview(linkPreviewView) - linkPreviewView.pin(to: snContentView) + bubbleView.addSubview(linkPreviewView) + linkPreviewView.pin(to: bubbleView, withInset: 0) + snContentView.addArrangedSubview(bubbleBackgroundView) self.bodyTappableLabel = linkPreviewView.bodyTappableLabel case .openGroupInvitation: @@ -459,9 +477,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { textColor: bodyLabelTextColor, isOutgoing: (cellViewModel.variant == .standardOutgoing) ) - - snContentView.addSubview(openGroupInvitationView) - openGroupInvitationView.pin(to: snContentView) + bubbleView.addSubview(openGroupInvitationView) + bubbleView.pin(to: openGroupInvitationView) + snContentView.addArrangedSubview(bubbleBackgroundView) } } else { @@ -504,15 +522,29 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) // Constraints - snContentView.addSubview(stackView) - stackView.pin(to: snContentView, withInset: inset) + bubbleView.addSubview(stackView) + stackView.pin(to: bubbleView, withInset: inset) + snContentView.addArrangedSubview(bubbleBackgroundView) } case .mediaMessage: - // Stack view - let stackView = UIStackView(arrangedSubviews: []) - stackView.axis = .vertical - stackView.spacing = Values.smallSpacing + // Body text view + if let body: String = cellViewModel.body, !body.isEmpty { + let inset: CGFloat = 12 + let maxWidth: CGFloat = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) + let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( + for: cellViewModel, + with: maxWidth, + textColor: bodyLabelTextColor, + searchText: lastSearchText, + delegate: self + ) + + self.bodyTappableLabel = bodyTappableLabel + bubbleView.addSubview(bodyTappableLabel) + bodyTappableLabel.pin(to: bubbleView, withInset: inset) + snContentView.addArrangedSubview(bubbleBackgroundView) + } // Album view let maxMessageWidth: CGFloat = VisibleMessageCell.getMaxWidth(for: cellViewModel) @@ -529,29 +561,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { albumView.set(.width, to: size.width) albumView.set(.height, to: size.height) albumView.loadMedia() - stackView.addArrangedSubview(albumView) - - // Body text view - if let body: String = cellViewModel.body, !body.isEmpty { - let inset: CGFloat = 12 - let maxWidth: CGFloat = (size.width - (2 * inset)) - let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( - for: cellViewModel, - with: maxWidth, - textColor: bodyLabelTextColor, - searchText: lastSearchText, - delegate: self - ) - - self.bodyTappableLabel = bodyTappableLabel - stackView.addArrangedSubview(UIView(wrapping: bodyTappableLabel, withInsets: UIEdgeInsets(top: 0, left: inset, bottom: inset, right: inset))) - } + snContentView.addArrangedSubview(albumView) + unloadContent = { albumView.unloadMedia() } - // Constraints - snContentView.addSubview(stackView) - stackView.pin(to: snContentView) - case .audio: guard let attachment: Attachment = cellViewModel.attachments?.first(where: { $0.isAudio }) else { return @@ -565,9 +578,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { playbackRate: (playbackInfo?.playbackRate ?? 1), oldPlaybackRate: (playbackInfo?.oldPlaybackRate ?? 1) ) - - snContentView.addSubview(voiceMessageView) - voiceMessageView.pin(to: snContentView) + + bubbleView.addSubview(voiceMessageView) + voiceMessageView.pin(to: bubbleView) + snContentView.addArrangedSubview(bubbleBackgroundView) self.voiceMessageView = voiceMessageView case .genericAttachment: @@ -575,7 +589,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { let inset: CGFloat = 12 let maxWidth = (VisibleMessageCell.getMaxWidth(for: cellViewModel) - 2 * inset) - + // Stack view let stackView = UIStackView(arrangedSubviews: []) stackView.axis = .vertical @@ -584,7 +598,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { // Document view let documentView = DocumentView(attachment: attachment, textColor: bodyLabelTextColor) stackView.addArrangedSubview(documentView) - + // Body text view if let body: String = cellViewModel.body, !body.isEmpty { // delegate should always be set at this point let bodyTappableLabel = VisibleMessageCell.getBodyTappableLabel( @@ -599,9 +613,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { stackView.addArrangedSubview(bodyTappableLabel) } - // Constraints - snContentView.addSubview(stackView) - stackView.pin(to: snContentView, withInset: inset) + bubbleView.addSubview(stackView) + stackView.pin(to: bubbleView, withInset: inset) + snContentView.addArrangedSubview(bubbleBackgroundView) } } @@ -652,7 +666,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { } private func updateBubbleViewCorners() { - let cornersToRound: UIRectCorner = getCornersToRound() + let cornersToRound: UIRectCorner = .allCorners bubbleBackgroundView.layer.cornerRadius = VisibleMessageCell.largeCornerRadius bubbleBackgroundView.layer.maskedCorners = getCornerMask(from: cornersToRound) @@ -830,7 +844,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { delegate?.needsLayout(for: cellViewModel, expandingReactions: false) } } - else if bubbleView.frame.contains(location) { + else if snContentView.frame.contains(location) { delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer) } } @@ -899,22 +913,6 @@ 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 - } - } - private func getCornerMask(from rectCorner: UIRectCorner) -> CACornerMask { guard !rectCorner.contains(.allCorners) else { return [ .layerMaxXMinYCorner, .layerMinXMinYCorner, .layerMaxXMaxYCorner, .layerMinXMaxYCorner] @@ -999,7 +997,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { cellViewModel.threadVariant == .openGroup || cellViewModel.threadVariant == .closedGroup ) - let leftGutterSize = (isGroupThread ? gutterSize : contactThreadHSpacing) + let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing) return (screen.width - leftGutterSize - gutterSize) diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 58915541d..fc0a4d32e 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -281,7 +281,7 @@ class ThreadSettingsViewModel: SettingsTableViewModel 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 } @@ -383,10 +407,23 @@ 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 moreReactorCount = self.reactionSummaries[lastSelectedReactionIndex].number - self.selectedReactionUserList.count + let footerCell: FooterCell = tableView.dequeue(type: FooterCell.self, for: indexPath) + footerCell.update( + moreReactorCount: moreReactorCount, + emoji: self.reactionSummaries[lastSelectedReactionIndex].emoji.rawValue + ) + footerCell.selectionStyle = .none + + return footerCell + } + let cell: UserCell = tableView.dequeue(type: UserCell.self, for: indexPath) let cellViewModel: MessageViewModel.ReactionInfo = self.selectedReactionUserList[indexPath.row] cell.update( @@ -407,6 +444,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 @@ -500,6 +539,44 @@ extension ReactionListSheet { snContentView.themeBorderColor = (isCurrentSelection ? .primary : .clear) } } + + 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 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" } } } diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 8cc99b4c4..6b5f0e1a8 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])") }() ), PagedData.ObservedChanges( diff --git a/Session/Home/Message Requests/MessageRequestsViewController.swift b/Session/Home/Message Requests/MessageRequestsViewController.swift index 410ff3416..25b6ee571 100644 --- a/Session/Home/Message Requests/MessageRequestsViewController.swift +++ b/Session/Home/Message Requests/MessageRequestsViewController.swift @@ -347,7 +347,7 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat switch section.model { case .threads: let threadId: String = section.elements[indexPath.row].threadId - let delete = UIContextualAction( + let delete: UIContextualAction = UIContextualAction( style: .destructive, title: "TXT_DELETE_TITLE".localized() ) { [weak self] _, _, completionHandler in @@ -355,8 +355,17 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat completionHandler(true) } delete.themeBackgroundColor = .conversationButton_swipeDestructive + + let block: UIContextualAction = UIContextualAction( + style: .normal, + title: "BLOCK_LIST_BLOCK_BUTTON".localized() + ) { [weak self] _, _, completionHandler in + self?.block(threadId) + completionHandler(true) + } + block.themeBackgroundColor = .conversationButton_swipeSecondary - return UISwipeActionsConfiguration(actions: [ delete ]) + return UISwipeActionsConfiguration(actions: [ delete, block ]) default: return nil } @@ -388,19 +397,6 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat _ = try SessionThread .filter(ids: threadIds) .deleteAll(db) - - try threadIds.forEach { threadId in - _ = try Contact - .fetchOrCreate(db, id: threadId) - .with( - isApproved: false, - isBlocked: true - ) - .saved(db) - } - - // Force a config sync - try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() } }) alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil)) @@ -416,6 +412,27 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat alertVC.addAction(UIAlertAction( title: "TXT_DELETE_TITLE".localized(), style: .destructive + ) { _ in + Storage.shared.write { db in + _ = try SessionThread + .filter(id: threadId) + .deleteAll(db) + } + }) + + alertVC.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil)) + self.present(alertVC, animated: true, completion: nil) + } + + private func block(_ threadId: String) { + let alertVC: UIAlertController = UIAlertController( + title: "MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON".localized(), + message: nil, + preferredStyle: .actionSheet + ) + alertVC.addAction(UIAlertAction( + title: "BLOCK_LIST_BLOCK_BUTTON".localized(), + style: .destructive ) { _ in Storage.shared.write { db in _ = try SessionThread diff --git a/Session/Media Viewing & Editing/AllMediaViewController.swift b/Session/Media Viewing & Editing/AllMediaViewController.swift new file mode 100644 index 000000000..2fbe07272 --- /dev/null +++ b/Session/Media Viewing & Editing/AllMediaViewController.swift @@ -0,0 +1,220 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import QuartzCore +import GRDB +import DifferenceKit +import SessionUIKit +import SignalUtilitiesKit + +public class AllMediaViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + private var pages: [UIViewController] = [] + private var targetVCIndex: Int? + + // MARK: Components + private lazy var tabBar: TabBar = { + let tabs = [ + TabBar.Tab(title: MediaStrings.media) { [weak self] in + guard let self = self else { return } + self.pageVC.setViewControllers([ self.pages[0] ], direction: .forward, animated: false, completion: nil) + self.updateSelectButton(updatedData: self.mediaTitleViewController.viewModel.galleryData, inBatchSelectMode: self.mediaTitleViewController.isInBatchSelectMode) + }, + TabBar.Tab(title: MediaStrings.document) { [weak self] in + guard let self = self else { return } + self.pageVC.setViewControllers([ self.pages[1] ], direction: .forward, animated: false, completion: nil) + self.endSelectMode() + self.navigationItem.rightBarButtonItem = nil + } + ] + return TabBar(tabs: tabs) + }() + + private var mediaTitleViewController: MediaTileViewController + private var documentTitleViewController: DocumentTileViewController + + init(mediaTitleViewController: MediaTileViewController, documentTitleViewController: DocumentTileViewController) { + self.mediaTitleViewController = mediaTitleViewController + self.documentTitleViewController = documentTitleViewController + + super.init(nibName: nil, bundle: nil) + + self.mediaTitleViewController.delegate = self + self.documentTitleViewController.delegate = self + + addChild(self.mediaTitleViewController) + addChild(self.documentTitleViewController) + } + + required init?(coder: NSCoder) { + notImplemented() + } + + // MARK: Lifecycle + public override func viewDidLoad() { + super.viewDidLoad() + + view.themeBackgroundColor = .backgroundPrimary + + // Add a custom back button if this is the only view controller + if self.navigationController?.viewControllers.first == self { + let backButton = OWSViewController.createOWSBackButton(withTarget: self, selector: #selector(didPressDismissButton)) + self.navigationItem.leftBarButtonItem = backButton + } + + ViewControllerUtilities.setUpDefaultSessionStyle( + for: self, + title: MediaStrings.allMedia, + hasCustomBackButton: false + ) + + // Set up page VC + pages = [ mediaTitleViewController, documentTitleViewController ] + pageVC.dataSource = self + pageVC.delegate = self + pageVC.setViewControllers([ mediaTitleViewController ], direction: .forward, animated: false, completion: nil) + addChild(pageVC) + + // Set up tab bar + view.addSubview(tabBar) + tabBar.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: view) + // Set up page VC constraints + let pageVCView = pageVC.view! + view.addSubview(pageVCView) + pageVCView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.bottom ], to: view) + pageVCView.pin(.top, to: .bottom, of: tabBar) + } + + // MARK: General + public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let index = pages.firstIndex(of: viewController), index != 0 else { return nil } + return pages[index - 1] + } + + public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let index = pages.firstIndex(of: viewController), index != (pages.count - 1) else { return nil } + return pages[index + 1] + } + + // MARK: Updating + public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { + guard let targetVC = pendingViewControllers.first, let index = pages.firstIndex(of: targetVC) else { return } + targetVCIndex = index + } + + public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating isFinished: Bool, previousViewControllers: [UIViewController], transitionCompleted isCompleted: Bool) { + guard isCompleted, let index = targetVCIndex else { return } + tabBar.selectTab(at: index) + } + + // MARK: Interaction + @objc public func didPressDismissButton() { + dismiss(animated: true, completion: nil) + } + + // MARK: Batch Selection + @objc func didTapSelect(_ sender: Any) { + self.mediaTitleViewController.didTapSelect(sender) + + // Don't allow the user to leave mid-selection, so they realized they have + // to cancel (lose) their selection if they leave. + self.navigationItem.hidesBackButton = true + } + + @objc func didCancelSelect(_ sender: Any) { + endSelectMode() + } + + func endSelectMode() { + self.mediaTitleViewController.endSelectMode() + self.navigationItem.hidesBackButton = false + } +} + +// MARK: - UIDocumentInteractionControllerDelegate + +extension AllMediaViewController: UIDocumentInteractionControllerDelegate { + public func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { + return self + } +} + +// MARK: - DocumentTitleViewControllerDelegate + +extension AllMediaViewController: DocumentTileViewControllerDelegate { + public func share(fileUrl: URL) { + let shareVC = UIActivityViewController(activityItems: [ fileUrl ], applicationActivities: nil) + + if UIDevice.current.isIPad { + shareVC.excludedActivityTypes = [] + shareVC.popoverPresentationController?.permittedArrowDirections = [] + shareVC.popoverPresentationController?.sourceView = self.view + shareVC.popoverPresentationController?.sourceRect = self.view.bounds + } + + navigationController?.present(shareVC, animated: true, completion: nil) + } + + public func preview(fileUrl: URL) { + let interactionController: UIDocumentInteractionController = UIDocumentInteractionController(url: fileUrl) + interactionController.delegate = self + interactionController.presentPreview(animated: true) + } +} + +// MARK: - DocumentTitleViewControllerDelegate + +extension AllMediaViewController: MediaTileViewControllerDelegate { + public func presentdetailViewController(_ detailViewController: UIViewController, animated: Bool) { + self.present(detailViewController, animated: animated) + } + + public func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool) { + guard !updatedData.isEmpty else { + self.navigationItem.rightBarButtonItem = nil + return + } + + if inBatchSelectMode { + self.navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .cancel, + target: self, + action: #selector(didCancelSelect) + ) + } + else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem( + title: "BUTTON_SELECT".localized(), + style: .plain, + target: self, + action: #selector(didTapSelect) + ) + } + } +} + +// MARK: - UIViewControllerTransitioningDelegate + +extension AllMediaViewController: UIViewControllerTransitioningDelegate { + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return self.mediaTitleViewController.animationController(forPresented: presented, presenting: presenting, source: source) + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return self.mediaTitleViewController.animationController(forDismissed: dismissed) + } +} + + +// MARK: - MediaPresentationContextProvider + +extension AllMediaViewController: MediaPresentationContextProvider { + func mediaPresentationContext(mediaItem: Media, in coordinateSpace: UICoordinateSpace) -> MediaPresentationContext? { + return self.mediaTitleViewController.mediaPresentationContext(mediaItem: mediaItem, in: coordinateSpace) + } + + func snapshotOverlayView(in coordinateSpace: UICoordinateSpace) -> (UIView, CGRect)? { + return self.mediaTitleViewController.snapshotOverlayView(in: coordinateSpace) + } +} + diff --git a/Session/Media Viewing & Editing/DocumentTitleViewController.swift b/Session/Media Viewing & Editing/DocumentTitleViewController.swift new file mode 100644 index 000000000..af790fb89 --- /dev/null +++ b/Session/Media Viewing & Editing/DocumentTitleViewController.swift @@ -0,0 +1,503 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import QuartzCore +import GRDB +import DifferenceKit +import SessionUIKit +import SignalUtilitiesKit + +public class DocumentTileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { + + /// This should be larger than one screen size so we don't have to call it multiple times in rapid succession, but not + /// so large that loading get's really chopping + static let itemPageSize: Int = Int(11 * itemsPerPortraitRow) + static let itemsPerPortraitRow: CGFloat = 4 + static let interItemSpacing: CGFloat = 2 + static let footerBarHeight: CGFloat = 40 + static let loadMoreHeaderHeight: CGFloat = 100 + + private let viewModel: MediaGalleryViewModel + private var hasLoadedInitialData: Bool = false + private var didFinishInitialLayout: Bool = false + private var isAutoLoadingNextPage: Bool = false + private var currentTargetOffset: CGPoint? + + public var delegate: DocumentTileViewControllerDelegate? + + // MARK: - Initialization + + init(viewModel: MediaGalleryViewModel) { + self.viewModel = viewModel + Storage.shared.addObserver(viewModel.pagedDataObserver) + + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder aDecoder: NSCoder) { + notImplemented() + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - UI + + override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .allButUpsideDown + } + + lazy var tableView: UITableView = { + let result = UITableView(frame: .zero, style: .grouped) + result.backgroundColor = Colors.navigationBarBackground + result.separatorStyle = .none + result.showsVerticalScrollIndicator = false + result.register(view: DocumentCell.self) + result.delegate = self + result.dataSource = self + // Feels a bit weird to have content smashed all the way to the bottom edge. + result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) + + return result + }() + + // MARK: - Lifecycle + + override public func viewDidLoad() { + super.viewDidLoad() + + // Add a custom back button if this is the only view controller + if self.navigationController?.viewControllers.first == self { + let backButton = OWSViewController.createOWSBackButton(withTarget: self, selector: #selector(didPressDismissButton)) + self.navigationItem.leftBarButtonItem = backButton + } + + ViewControllerUtilities.setUpDefaultSessionStyle( + for: self, + title: MediaStrings.document, + hasCustomBackButton: false + ) + + view.addSubview(self.tableView) + tableView.autoPin(toEdgesOf: view) + + // Notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidBecomeActive(_:)), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidResignActive(_:)), + name: UIApplication.didEnterBackgroundNotification, object: nil + ) + } + + public override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + startObservingChanges() + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.didFinishInitialLayout = true + } + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + stopObservingChanges() + } + + @objc func applicationDidBecomeActive(_ notification: Notification) { + startObservingChanges() + } + + @objc func applicationDidResignActive(_ notification: Notification) { + stopObservingChanges() + } + + // MARK: - Updating + + private func performInitialScrollIfNeeded() { + // Ensure this hasn't run before and that we have data (The 'galleryData' will always + // contain something as the 'empty' state is a section within 'galleryData') + guard !self.didFinishInitialLayout && self.hasLoadedInitialData else { return } + + // If we have a focused item then we want to scroll to it + guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return } + + Logger.debug("scrolling to focused item at indexPath: \(focusedIndexPath)") + self.view.layoutIfNeeded() + self.tableView.scrollToRow(at: focusedIndexPath, at: .middle, animated: false) + + // Now that the data has loaded we need to check if either of the "load more" sections are + // visible and trigger them if so + // + // Note: We do it this way as we want to trigger the load behaviour for the first section + // if it has one before trying to trigger the load behaviour for the last section + self.autoLoadNextPageIfNeeded() + } + + private func autoLoadNextPageIfNeeded() { + guard !self.isAutoLoadingNextPage else { return } + + self.isAutoLoadingNextPage = true + + DispatchQueue.main.asyncAfter(deadline: .now() + PagedData.autoLoadNextPageDelay) { [weak self] in + self?.isAutoLoadingNextPage = false + + // Note: We sort the headers as we want to prioritise loading newer pages over older ones + let sortedVisibleIndexPaths: [IndexPath] = (self?.tableView.indexPathsForVisibleRows ?? []).sorted() + + for headerIndexPath in sortedVisibleIndexPaths { + let section: MediaGalleryViewModel.SectionModel? = self?.viewModel.galleryData[safe: headerIndexPath.section] + + switch section?.model { + case .loadNewer, .loadOlder: + // Attachments are loaded in descending order so 'loadOlder' actually corresponds with + // 'pageAfter' in this case + self?.viewModel.pagedDataObserver?.load(section?.model == .loadOlder ? + .pageAfter : + .pageBefore + ) + return + + default: continue + } + } + } + } + + private func startObservingChanges() { + // Start observing for data changes (will callback on the main thread) + self.viewModel.onGalleryChange = { [weak self] updatedGalleryData in + self?.handleUpdates(updatedGalleryData) + } + } + + private func stopObservingChanges() { + // Note: The 'pagedDataObserver' will continue to get changes but + // we don't want to trigger any UI updates + self.viewModel.onGalleryChange = nil + } + + private func handleUpdates(_ updatedGalleryData: [MediaGalleryViewModel.SectionModel]) { + // Ensure the first load runs without animations (if we don't do this the cells will animate + // in from a frame of CGRect.zero) + guard hasLoadedInitialData else { + self.hasLoadedInitialData = true + self.viewModel.updateGalleryData(updatedGalleryData) + + UIView.performWithoutAnimation { + self.tableView.reloadData() + self.performInitialScrollIfNeeded() + } + return + } + + let isInsertingAtTop: Bool = { + let oldFirstSectionIsLoadMore: Bool = ( + self.viewModel.galleryData.first?.model == .loadNewer || + self.viewModel.galleryData.first?.model == .loadOlder + ) + let oldTargetSectionIndex: Int = (oldFirstSectionIsLoadMore ? 1 : 0) + + guard + let newTargetSectionIndex = updatedGalleryData + .firstIndex(where: { $0.model == self.viewModel.galleryData[safe: oldTargetSectionIndex]?.model }), + let oldFirstItem: MediaGalleryViewModel.Item = self.viewModel.galleryData[safe: oldTargetSectionIndex]?.elements.first, + let newFirstItemIndex = updatedGalleryData[safe: newTargetSectionIndex]?.elements.firstIndex(of: oldFirstItem) + else { return false } + + return (newTargetSectionIndex > oldTargetSectionIndex || newFirstItemIndex > 0) + }() + + CATransaction.begin() + + if isInsertingAtTop { CATransaction.setDisableActions(true) } + + self.tableView.reload( + using: StagedChangeset(source: self.viewModel.galleryData, target: updatedGalleryData), + with: .automatic, + interrupt: { $0.changeCount > MediaTileViewController.itemPageSize } + ) { [weak self] updatedData in + self?.viewModel.updateGalleryData(updatedData) + } + + CATransaction.setCompletionBlock { [weak self] in + // If one of the "load more" sections is still visible once the animation completes then + // trigger another "load more" (after a small delay to minimize animation bugginess) + self?.autoLoadNextPageIfNeeded() + } + CATransaction.commit() + + } + + // MARK: - Interactions + + @objc public func didPressDismissButton() { + let presentedNavController: UINavigationController? = (self.presentingViewController as? UINavigationController) + let mediaPageViewController: MediaPageViewController? = ( + (presentedNavController?.viewControllers.last as? MediaPageViewController) ?? + (self.presentingViewController as? MediaPageViewController) + ) + + // If the album was presented from a 'MediaPageViewController' and it has no more data (ie. + // all album items had been deleted) then dismiss to the screen before that one + guard mediaPageViewController?.viewModel.albumData.isEmpty != true else { + presentedNavController?.presentingViewController?.dismiss(animated: true, completion: nil) + return + } + + dismiss(animated: true, completion: nil) + } + + // MARK: - UITableViewDataSource + + public func numberOfSections(in tableView: UITableView) -> Int { + return self.viewModel.galleryData.count + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.viewModel.galleryData[section].elements.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: DocumentCell = tableView.dequeue(type: DocumentCell.self, for: indexPath) + cell.update(with: self.viewModel.galleryData[indexPath.section].elements[indexPath.row]) + return cell + } + + // MARK: - UITableViewDelegate + + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section] + + switch section.model { + case .emptyGallery, .loadOlder, .loadNewer: + let headerView: DocumentStaticHeaderView = DocumentStaticHeaderView() + headerView.configure( + title: { + switch section.model { + case .emptyGallery: return "DOCUMENT_TILES_EMPTY_DOCUMENT".localized() + case .loadOlder: return "DOCUMENT_TILES_LOADING_OLDER_LABEL".localized() + case .loadNewer: return "DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL".localized() + case .galleryMonth: return "" // Impossible case + } + }() + ) + return headerView + + case .galleryMonth(let date): + let headerView: DocumentSectionHeaderView = DocumentSectionHeaderView() + headerView.configure(title: date.localizedString) + return headerView + } + } + + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + let section: MediaGalleryViewModel.SectionModel = self.viewModel.galleryData[section] + + switch section.model { + case .emptyGallery, .loadOlder, .loadNewer: + return MediaTileViewController.loadMoreHeaderHeight + + case .galleryMonth: + return 50 + } + } + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: false) + let attachment: Attachment = self.viewModel.galleryData[indexPath.section].elements[indexPath.row].attachment + guard let originalFilePath: String = attachment.originalFilePath else { return } + + let fileUrl: URL = URL(fileURLWithPath: originalFilePath) + + // Open a preview of the document for text, pdf or microsoft files + if + attachment.isText || + attachment.isMicrosoftDoc || + attachment.contentType == OWSMimeTypeApplicationPdf + { + + delegate?.preview(fileUrl: fileUrl) + return + } + + // Otherwise share the file + delegate?.share(fileUrl: fileUrl) + } +} + +// MARK: - View + +class DocumentCell: UITableViewCell { + + // MARK: - Initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setUpViewHierarchy() + setupLayout() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + + setUpViewHierarchy() + setupLayout() + } + + // MARK: - UI + + private static let iconImageViewSize: CGSize = CGSize(width: 31, height: 40) + + private let iconImageView: UIImageView = { + let result: UIImageView = UIImageView(image: #imageLiteral(resourceName: "File").withRenderingMode(.alwaysTemplate)) + result.translatesAutoresizingMaskIntoConstraints = false + result.tintColor = Colors.text + + return result + }() + + private let titleLabel: UILabel = { + let result: UILabel = UILabel() + result.translatesAutoresizingMaskIntoConstraints = false + result.setContentHuggingPriority(.defaultHigh, for: .horizontal) + result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + result.font = .boldSystemFont(ofSize: Values.smallFontSize) + result.textColor = Colors.text + result.lineBreakMode = .byTruncatingTail + + return result + }() + + private let detailLabel: UILabel = { + let result: UILabel = UILabel() + result.translatesAutoresizingMaskIntoConstraints = false + result.setContentHuggingPriority(.defaultHigh, for: .horizontal) + result.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + result.font = .systemFont(ofSize: Values.smallFontSize) + result.textColor = Colors.text + result.lineBreakMode = .byTruncatingTail + + return result + }() + + private func setUpViewHierarchy() { + backgroundColor = Colors.cellBackground + selectedBackgroundView = UIView() + selectedBackgroundView?.backgroundColor = Colors.cellSelected + + + contentView.addSubview(iconImageView) + contentView.addSubview(titleLabel) + contentView.addSubview(detailLabel) + } + + // MARK: - Layout + + private func setupLayout() { + NSLayoutConstraint.activate([ + contentView.heightAnchor.constraint(equalToConstant: 68), + + iconImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: Values.mediumSpacing), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.widthAnchor.constraint(equalToConstant: Self.iconImageViewSize.width), + iconImageView.heightAnchor.constraint(equalToConstant: Self.iconImageViewSize.height), + + titleLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing), + titleLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing), + titleLabel.topAnchor.constraint(equalTo: iconImageView.topAnchor), + + detailLabel.leftAnchor.constraint(equalTo: iconImageView.rightAnchor, constant: Values.mediumSpacing), + detailLabel.rightAnchor.constraint(lessThanOrEqualTo: contentView.rightAnchor, constant: -Values.mediumSpacing), + detailLabel.bottomAnchor.constraint(equalTo: iconImageView.bottomAnchor), + ]) + } + + // MARK: - Content + + func update(with item: MediaGalleryViewModel.Item) { + let attachment = item.attachment + titleLabel.text = attachment.sourceFilename ?? "File" + detailLabel.text = "\(OWSFormat.formatFileSize(UInt(attachment.byteCount)))" + } +} + +class DocumentSectionHeaderView: UIView { + + let label: UILabel + + override init(frame: CGRect) { + label = UILabel() + label.textColor = Colors.text + + let blurEffect = UIBlurEffect(style: .dark) + let blurEffectView = UIVisualEffectView(effect: blurEffect) + + blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + + super.init(frame: frame) + + self.backgroundColor = isLightMode ? Colors.cellBackground : UIColor.ows_black.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor) + + self.addSubview(blurEffectView) + self.addSubview(label) + + blurEffectView.autoPinEdgesToSuperviewEdges() + blurEffectView.isHidden = isLightMode + label.autoPinEdge(toSuperviewMargin: .trailing) + label.autoPinEdge(toSuperviewMargin: .leading) + label.autoVCenterInSuperview() + } + + @available(*, unavailable, message: "Unimplemented") + required init?(coder aDecoder: NSCoder) { + notImplemented() + } + + public func configure(title: String) { + self.label.text = title + } +} + +class DocumentStaticHeaderView: UIView { + + let label = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(label) + + label.textColor = Colors.text + label.textAlignment = .center + label.numberOfLines = 0 + label.autoPinEdgesToSuperviewMargins(with: UIEdgeInsets(top: 0, leading: Values.largeSpacing, bottom: 0, trailing: Values.largeSpacing)) + } + + @available(*, unavailable, message: "Unimplemented") + required public init?(coder aDecoder: NSCoder) { + notImplemented() + } + + public func configure(title: String) { + self.label.text = title + } +} + +// MARK: - DocumentTitleViewControllerDelegate + +public protocol DocumentTileViewControllerDelegate: AnyObject { + func share(fileUrl: URL) + func preview(fileUrl: URL) +} diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index bad7fc350..a4d760280 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -18,12 +18,19 @@ public class MediaGalleryViewModel { case loadNewer } + // MARK: Media type + public enum MediaType { + case media + case document + } + // MARK: - Variables public let threadId: String public let threadVariant: SessionThread.Variant private var focusedAttachmentId: String? public private(set) var focusedIndexPath: IndexPath? + public var mediaType: MediaType /// This value is the current state of an album view private var cachedInteractionIdBefore: Atomic<[Int64: Int64]> = Atomic([:]) @@ -54,6 +61,7 @@ public class MediaGalleryViewModel { threadId: String, threadVariant: SessionThread.Variant, isPagedData: Bool, + mediaType: MediaType, pageSize: Int = 1, focusedAttachmentId: String? = nil, performInitialQuerySync: Bool = false @@ -62,6 +70,7 @@ public class MediaGalleryViewModel { self.threadVariant = threadVariant self.focusedAttachmentId = focusedAttachmentId self.pagedDataObserver = nil + self.mediaType = mediaType guard isPagedData else { return } @@ -80,7 +89,7 @@ public class MediaGalleryViewModel { ) ], joinSQL: Item.joinSQL, - filterSQL: Item.filterSQL(threadId: threadId), + filterSQL: Item.filterSQL(threadId: threadId, mediaType: self.mediaType), orderSQL: Item.galleryOrderSQL, dataQuery: Item.baseQuery(orderSQL: Item.galleryOrderSQL), onChangeUnsorted: { [weak self] updatedData, updatedPageInfo in @@ -243,15 +252,27 @@ public class MediaGalleryViewModel { """ }() - fileprivate static func filterSQL(threadId: String) -> SQL { + fileprivate static func filterSQL(threadId: String, mediaType: MediaType) -> SQL { let interaction: TypedTableAlias = TypedTableAlias() let attachment: TypedTableAlias = TypedTableAlias() - return SQL(""" - \(attachment[.isVisualMedia]) = true AND - \(attachment[.isValid]) = true AND - \(interaction[.threadId]) = \(threadId) - """) + switch (mediaType) { + case .media: + return SQL(""" + \(attachment[.isVisualMedia]) = true AND + \(attachment[.isValid]) = true AND + \(interaction[.threadId]) = \(threadId) + """) + case .document: + // FIXME: Remove "\(attachment[.sourceFilename]) <> 'session-audio-message'" when all platforms send the voice message properly + return SQL(""" + \(attachment[.isVisualMedia]) = false AND + \(attachment[.isValid]) = true AND + \(interaction[.threadId]) = \(threadId) AND + \(attachment[.variant]) = \(Attachment.Variant.standard) AND + \(attachment[.sourceFilename]) <> 'session-audio-message' + """) + } } fileprivate static let galleryOrderSQL: SQL = { @@ -509,7 +530,8 @@ public class MediaGalleryViewModel { let viewModel: MediaGalleryViewModel = MediaGalleryViewModel( threadId: threadId, threadVariant: threadVariant, - isPagedData: false + isPagedData: false, + mediaType: .media ) viewModel.loadAndCacheAlbumData(for: interactionId, in: threadId) viewModel.replaceAlbumObservation(toObservationFor: interactionId) @@ -534,7 +556,7 @@ public class MediaGalleryViewModel { return navController } - public static func createTileViewController( + public static func createMediaTileViewController( threadId: String, threadVariant: SessionThread.Variant, focusedAttachmentId: String?, @@ -544,6 +566,7 @@ public class MediaGalleryViewModel { threadId: threadId, threadVariant: threadVariant, isPagedData: true, + mediaType: .media, pageSize: MediaTileViewController.itemPageSize, focusedAttachmentId: focusedAttachmentId, performInitialQuerySync: performInitialQuerySync @@ -553,6 +576,53 @@ public class MediaGalleryViewModel { viewModel: viewModel ) } + + public static func createDocumentTitleViewController( + threadId: String, + threadVariant: SessionThread.Variant, + focusedAttachmentId: String?, + performInitialQuerySync: Bool = false + ) -> DocumentTileViewController { + let viewModel: MediaGalleryViewModel = MediaGalleryViewModel( + threadId: threadId, + threadVariant: threadVariant, + isPagedData: true, + mediaType: .document, + pageSize: MediaTileViewController.itemPageSize, + focusedAttachmentId: focusedAttachmentId, + performInitialQuerySync: performInitialQuerySync + ) + + return DocumentTileViewController( + viewModel: viewModel + ) + } + + public static func createAllMediaViewController( + threadId: String, + threadVariant: SessionThread.Variant, + focusedAttachmentId: String?, + performInitialQuerySync: Bool = false + ) -> AllMediaViewController { + let mediaTitleViewController = createMediaTileViewController( + threadId: threadId, + threadVariant: threadVariant, + focusedAttachmentId: focusedAttachmentId, + performInitialQuerySync: performInitialQuerySync + ) + + let documentTitleViewController = createDocumentTitleViewController( + threadId: threadId, + threadVariant: threadVariant, + focusedAttachmentId: focusedAttachmentId, + performInitialQuerySync: performInitialQuerySync + ) + + return AllMediaViewController( + mediaTitleViewController: mediaTitleViewController, + documentTitleViewController: documentTitleViewController + ) + } } // MARK: - Objective-C Support @@ -564,7 +634,7 @@ public class SNMediaGallery: NSObject { @objc(pushTileViewWithSliderEnabledForThreadId:isClosedGroup:isOpenGroup:fromNavController:) static func pushTileView(threadId: String, isClosedGroup: Bool, isOpenGroup: Bool, fromNavController: OWSNavigationController) { fromNavController.pushViewController( - MediaGalleryViewModel.createTileViewController( + MediaGalleryViewModel.createAllMediaViewController( threadId: threadId, threadVariant: { if isClosedGroup { return .closedGroup } diff --git a/Session/Media Viewing & Editing/MediaPageViewController.swift b/Session/Media Viewing & Editing/MediaPageViewController.swift index ec290ea7e..061dd010d 100644 --- a/Session/Media Viewing & Editing/MediaPageViewController.swift +++ b/Session/Media Viewing & Editing/MediaPageViewController.swift @@ -458,7 +458,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // MediaTileViewController then just pop/dismiss the screen guard let presentingNavController: UINavigationController = (self.presentingViewController as? UINavigationController), - !(presentingNavController.viewControllers.last is MediaTileViewController) + !(presentingNavController.viewControllers.last is AllMediaViewController) else { guard self.navigationController?.viewControllers.count == 1 else { self.navigationController?.popViewController(animated: true) @@ -471,7 +471,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou // Otherwise if we came via the conversation screen we need to push a new // instance of MediaTileViewController - let tileViewController: MediaTileViewController = MediaGalleryViewModel.createTileViewController( + let allMediaViewController: AllMediaViewController = MediaGalleryViewModel.createAllMediaViewController( threadId: self.viewModel.threadId, threadVariant: self.viewModel.threadVariant, focusedAttachmentId: currentItem.attachment.id, @@ -479,9 +479,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou ) let navController: MediaGalleryNavigationController = MediaGalleryNavigationController() - navController.viewControllers = [tileViewController] + navController.viewControllers = [allMediaViewController] navController.modalPresentationStyle = .overFullScreen - navController.transitioningDelegate = tileViewController + navController.transitioningDelegate = allMediaViewController self.navigationController?.present(navController, animated: true) } diff --git a/Session/Media Viewing & Editing/MediaTileViewController.swift b/Session/Media Viewing & Editing/MediaTileViewController.swift index fb0f2c3e2..44d1bb4e3 100644 --- a/Session/Media Viewing & Editing/MediaTileViewController.swift +++ b/Session/Media Viewing & Editing/MediaTileViewController.swift @@ -17,12 +17,14 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour static let footerBarHeight: CGFloat = 40 static let loadMoreHeaderHeight: CGFloat = 100 - private let viewModel: MediaGalleryViewModel + public let viewModel: MediaGalleryViewModel private var hasLoadedInitialData: Bool = false private var didFinishInitialLayout: Bool = false private var isAutoLoadingNextPage: Bool = false private var currentTargetOffset: CGPoint? + public var delegate: MediaTileViewControllerDelegate? + var isInBatchSelectMode = false { didSet { collectionView.allowsMultipleSelection = isInBatchSelectMode @@ -199,8 +201,35 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return } Logger.debug("scrolling to focused item at indexPath: \(focusedIndexPath)") + + // Note: For some reason 'scrollToItem' doesn't always work properly so we need to manually + // calculate what the offset should be to do the initial scroll self.view.layoutIfNeeded() - self.collectionView.scrollToItem(at: focusedIndexPath, at: .centeredVertically, animated: false) + + let availableHeight: CGFloat = { + // Note: This height will be set before we have properly performed a layout and fitted + // this screen within it's parent UIPagedViewController so we need to try to calculate + // the "actual" height of the collection view + var finalHeight: CGFloat = self.collectionView.frame.height + + if let navController: UINavigationController = self.parent?.navigationController { + finalHeight -= navController.navigationBar.frame.height + finalHeight -= (UIApplication.shared.keyWindow?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0) + } + + if let tabBar: TabBar = self.parent?.parent?.view.subviews.first as? TabBar { + finalHeight -= tabBar.frame.height + } + + return finalHeight + }() + let focusedRect: CGRect = (self.collectionView.layoutAttributesForItem(at: focusedIndexPath)?.frame) + .defaulting(to: .zero) + self.collectionView.contentOffset = CGPoint( + x: 0, + y: (focusedRect.origin.y - (availableHeight / 2) + (focusedRect.height / 2)) + ) + self.collectionView.collectionViewLayout.invalidateLayout() // Now that the data has loaded we need to check if either of the "load more" sections are // visible and trigger them if so @@ -269,6 +298,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour guard hasLoadedInitialData else { self.hasLoadedInitialData = true self.viewModel.updateGalleryData(updatedGalleryData) + self.updateSelectButton(updatedData: updatedGalleryData, inBatchSelectMode: isInBatchSelectMode) UIView.performWithoutAnimation { self.collectionView.reloadData() @@ -492,12 +522,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour // [ConversationSettingsView] // [ConversationView] // - guard - let viewControllers: [UIViewController] = self.navigationController?.viewControllers, - viewControllers.count > 1, - (viewControllers[viewControllers.count - 2] as? SettingsViewModelAccessible)?.viewModelType == ThreadSettingsViewModel.self - else { return } - let detailViewController: UIViewController? = MediaGalleryViewModel.createDetailViewController( for: self.viewModel.threadId, threadVariant: self.viewModel.threadVariant, @@ -508,7 +532,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour guard let detailViewController: UIViewController = detailViewController else { return } - self.present(detailViewController, animated: true) + delegate?.presentdetailViewController(detailViewController, animated: true) return } @@ -590,26 +614,7 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour } func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool) { - guard !updatedData.isEmpty else { - self.navigationItem.rightBarButtonItem = nil - return - } - - if inBatchSelectMode { - self.navigationItem.rightBarButtonItem = UIBarButtonItem( - barButtonSystemItem: .cancel, - target: self, - action: #selector(didCancelSelect) - ) - } - else { - self.navigationItem.rightBarButtonItem = UIBarButtonItem( - title: "BUTTON_SELECT".localized(), - style: .plain, - target: self, - action: #selector(didTapSelect) - ) - } + delegate?.updateSelectButton(updatedData: updatedData, inBatchSelectMode: inBatchSelectMode) } @objc func didTapSelect(_ sender: Any) { @@ -624,13 +629,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour // Ensure toolbar doesn't cover bottom row. self?.collectionView.contentInset.bottom += MediaTileViewController.footerBarHeight }, completion: nil) - - // disabled until at least one item is selected - self.deleteButton.isEnabled = false - - // Don't allow the user to leave mid-selection, so they realized they have - // to cancel (lose) their selection if they leave. - self.navigationItem.hidesBackButton = true } @objc func didCancelSelect(_ sender: Any) { @@ -650,8 +648,6 @@ public class MediaTileViewController: UIViewController, UICollectionViewDataSour self?.collectionView.contentInset.bottom -= MediaTileViewController.footerBarHeight }, completion: nil) - self.navigationItem.hidesBackButton = false - // Deselect any selected collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false)} } @@ -863,7 +859,12 @@ class GalleryGridCellItem: PhotoGridItem { extension MediaTileViewController: UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - guard self == presented || self.navigationController == presented else { return nil } + guard + self == presented || + self.navigationController == presented || + self.parent == presented || + self.parent?.navigationController == presented + else { return nil } guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return nil } return MediaDismissAnimationController( @@ -872,7 +873,12 @@ extension MediaTileViewController: UIViewControllerTransitioningDelegate { } public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - guard self == dismissed || self.navigationController == dismissed else { return nil } + guard + self == dismissed || + self.navigationController == dismissed || + self.parent == dismissed || + self.parent?.navigationController == dismissed + else { return nil } guard let focusedIndexPath: IndexPath = self.viewModel.focusedIndexPath else { return nil } return MediaZoomAnimationController( @@ -923,3 +929,10 @@ extension MediaTileViewController: MediaPresentationContextProvider { return self.navigationController?.navigationBar.generateSnapshot(in: coordinateSpace) } } + +// MARK: - MediaTileViewControllerDelegate + +public protocol MediaTileViewControllerDelegate: AnyObject { + func presentdetailViewController(_ detailViewController: UIViewController, animated: Bool) + func updateSelectButton(updatedData: [MediaGalleryViewModel.SectionModel], inBatchSelectMode: Bool) +} diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index ef84f5c0d..d1d66c44b 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -79,6 +79,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } ) + SNAppearance.switchToSessionAppearance() + + if Environment.shared?.callManager.wrappedValue?.currentCall == nil { + UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") + } + // No point continuing if we are running tests guard !CurrentAppContext().isRunningTests else { return true } @@ -131,21 +137,23 @@ 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()) + stopPollers(shouldStopUserPoller: !self.hasCallOngoing()) // 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) + if !self.hasCallOngoing() { + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + } } } func applicationDidReceiveMemoryWarning(_ application: UIApplication) { Logger.info("applicationDidReceiveMemoryWarning") } - + func applicationWillTerminate(_ application: UIApplication) { DDLog.flushLog() - + stopPollers() } @@ -638,6 +646,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return !call.hasStartedConnecting } + func hasCallOngoing() -> Bool { + guard let call = AppEnvironment.shared.callManager.currentCall else { return false } + + return !call.hasEnded + } + func handleAppActivatedWithOngoingCallIfNeeded() { guard let call: SessionCall = (AppEnvironment.shared.callManager.currentCall as? SessionCall), diff --git a/Session/Meta/AudioFiles/messageReceivedSounds/silence.aiff b/Session/Meta/AudioFiles/messageReceivedSounds/silence.aiff new file mode 100644 index 000000000..c968a3c03 Binary files /dev/null and b/Session/Meta/AudioFiles/messageReceivedSounds/silence.aiff differ diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 418fc2e98..c9e0c65c4 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Möchten Sie wirklich alle Nachrichten löschen?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Fehler"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 849f8568f..ee9fa08e2 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Error"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index ad5befb72..a7ed7fa4f 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Fallo"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 6c6ba1ded..b681c5fb1 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "خطاء"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 7caa36964..ac94fb0fb 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Oletko varma että haluat poistaa kaikki viestipyynnöt?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Poista"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Oletko varma että haluat poistaa tämän viestipyynnön?"; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; "MESSAGE_REQUESTS_INFO" = "Viestin lähettäminen tälle henkilölle hyväksyy automaattisesti viestipyynnön."; "MESSAGE_REQUESTS_ACCEPTED" = "Viestipyyntösi hyväksyttiin."; "MESSAGE_REQUESTS_NOTIFICATION" = "Sinulla on uusi viestipyyntö"; "TXT_HIDE_TITLE" = "Piilota"; "TXT_DELETE_ACCEPT" = "Hyväksy"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Virhe"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Avoin ryhmä"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Yksityisviesti"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 2bcaf674a..b6528d682 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Êtes-vous sûr de vouloir supprimer toutes les demandes de messages ?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Effacer"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Êtes-vous sûr de vouloir supprimer cette demande de message ?"; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; "MESSAGE_REQUESTS_INFO" = "Envoyer un message à cet utilisateur acceptera automatiquement sa demande de message."; "MESSAGE_REQUESTS_ACCEPTED" = "Votre demande de message a été réceptionnée."; "MESSAGE_REQUESTS_NOTIFICATION" = "Vous avez une nouvelle demande de message"; "TXT_HIDE_TITLE" = "Masquer"; "TXT_DELETE_ACCEPT" = "Accepter"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Erreur"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Groupe public"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Message privé"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index 525521ace..5d8036518 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Error"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 011864002..e12a1731e 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Greška"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 1709b02dc..0ddf7e679 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Galat"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 7b633915f..f28c2eb4f 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Eliminare veramente tutte le richieste di messaggio?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Cancella"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Sei sicuro di voler eliminare questa richiesta di messaggio?"; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; "MESSAGE_REQUESTS_INFO" = "L'invio di un messaggio a questo utente accetterà automaticamente la richiesta di messaggio."; "MESSAGE_REQUESTS_ACCEPTED" = "La tua richiesta di messaggio è stata accettata."; "MESSAGE_REQUESTS_NOTIFICATION" = "Hai una nuova richiesta di messaggio"; "TXT_HIDE_TITLE" = "Nascondi"; "TXT_DELETE_ACCEPT" = "Accetta"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Errore"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Gruppo Aperto"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Messaggio Privato"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index b938cc8ca..e266d86a1 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "本当に全てのリクエストを消去しますか?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "消去"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "本当にこのリクエストを削除しますか?"; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; "MESSAGE_REQUESTS_INFO" = "このユーザーにメッセージを送信すると、自動的にリクエストが承認されます。"; "MESSAGE_REQUESTS_ACCEPTED" = "リクエストが承認されました"; "MESSAGE_REQUESTS_NOTIFICATION" = "新しいリクエストがあります"; "TXT_HIDE_TITLE" = "非表示"; "TXT_DELETE_ACCEPT" = "許可"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "エラー"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "公開グループ"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "ダイレクトメッセージ"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 88734066b..159e44437 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Fout"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 06a6c9e9b..07f572baf 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Czy na pewno chcesz wyczyścić wszystkie żądania wiadomości?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Wyczyść"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Czy na pewno chcesz usunąć to żądanie wiadomości?"; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; "MESSAGE_REQUESTS_INFO" = "Wysyłanie wiadomości do tego użytkownika automatycznie zaakceptuje ich żądanie wiadomości."; "MESSAGE_REQUESTS_ACCEPTED" = "Twoje żądanie wiadomości zostało zaakceptowane."; "MESSAGE_REQUESTS_NOTIFICATION" = "Masz nowe żądanie wiadomości"; "TXT_HIDE_TITLE" = "Ukryj"; "TXT_DELETE_ACCEPT" = "Zaakceptuj"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Błąd"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Otwórz grupę"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Wiadomość prywatna"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 5932c113c..1808c045f 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Erro"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 6cdf42210..00b9cab2c 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Вы уверены, что хотите очистить все запросы сообщений?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Очистить"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Вы уверены, что хотите удалить это сообщение?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Скрыть"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Ошибка"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 1dabbbe49..012695783 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Error"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 30cfe8de4..a9508fe2b 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Naozaj chcete vymazať všetky žiadosti o správu?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Vymazať"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Naozaj chcete vymazať túto žiadosť o správu?"; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; "MESSAGE_REQUESTS_INFO" = "Poslanie správy tomuto používateľovi automaticky príjme ich žiadosť o správu."; "MESSAGE_REQUESTS_ACCEPTED" = "Vaša žiadosť o správu bola prijatá."; "MESSAGE_REQUESTS_NOTIFICATION" = "Máte novú žiadosť o správu"; "TXT_HIDE_TITLE" = "Skryť"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Error"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 3ec3f2071..b642791ed 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Fel"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index bc06b4290..ba8a06302 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "ข้อผิดพลาด"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index e15f6dd2d..31c80a043 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Error"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index d90342854..7175fb70a 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Clear"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this message request?"; -"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request."; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; +"MESSAGE_REQUESTS_INFO" = "Sending a message to this user will automatically accept their message request and reveal your Session ID."; "MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted."; "MESSAGE_REQUESTS_NOTIFICATION" = "You have a new message request"; "TXT_HIDE_TITLE" = "Hide"; "TXT_DELETE_ACCEPT" = "Accept"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "Error"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "Open Group"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "Direct Message"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 410588d66..dc572730c 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -632,11 +632,14 @@ "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "您确定要清除所有消息请求吗?"; "MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "清除"; "MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "您确定要删除此消息请求吗?"; +"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block this contact?"; "MESSAGE_REQUESTS_INFO" = "发送消息给此用户将自动接受他们的消息请求。"; "MESSAGE_REQUESTS_ACCEPTED" = "您的消息请求已被接受。"; "MESSAGE_REQUESTS_NOTIFICATION" = "您有一个新的消息请求"; "TXT_HIDE_TITLE" = "隐藏"; "TXT_DELETE_ACCEPT" = "接受"; +"TXT_DECLINE_TITLE" = "Decline"; +"TXT_BLOCK_USER_TITLE" = "Block User"; "ALERT_ERROR_TITLE" = "错误"; "NEW_CONVERSATION_MENU_OPEN_GROUP" = "公开群组"; "NEW_CONVERSATION_MENU_DIRECT_MESSAGE" = "私信"; @@ -678,6 +681,11 @@ "SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send."; "INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again."; "INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again."; +"MEDIA_TAB_TITLE" = "Media"; +"DOCUMENT_TAB_TITLE" = "Documents"; +"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation."; +"DOCUMENT_TILES_LOADING_MORE_RECENT_LABEL" = "Loading Newer Document…"; +"DOCUMENT_TILES_LOADING_OLDER_LABEL" = "Loading Older Document…"; /* The name for the emoji category 'Activities' */ "EMOJI_CATEGORY_ACTIVITIES_NAME" = "Activities"; /* The name for the emoji category 'Animals & Nature' */ @@ -696,7 +704,9 @@ "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_ONE" = "And 1 other has reacted %@ to this message."; +"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message."; "PRIVACY_TITLE" = "Privacy"; "PRIVACY_SECTION_SCREEN_SECURITY" = "Screen Security"; "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_TITLE" = "Lock Session"; diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 74dc92d66..0d9284a3a 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -293,21 +293,25 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { AppNotificationUserInfoKey.threadId: thread.id ] - 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? + let notificationTitle: String = "Session" + let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + let notificationBody: String? = { + switch messageInfo.state { + case .permissionDenied: + return String( + format: "modal_call_missed_tips_explanation".localized(), + senderName + ) + case .missed: + return String( + format: "call_missed".localized(), + senderName + ) + default: + return nil + } + }() - if messageInfo.state == .permissionDenied { - notificationBody = String( - format: "modal_call_missed_tips_explanation".localized(), - threadName - ) - } let fallbackSound: Preferences.Sound = db[.defaultNotificationSound] .defaulting(to: Preferences.Sound.defaultNotificationSound) @@ -325,7 +329,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol { previewType: previewType, sound: sound, threadVariant: thread.variant, - threadName: threadName, + threadName: senderName, replacingIdentifier: UUID().uuidString ) } diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 1cd8d71cf..c2df1a4f9 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -6,7 +6,7 @@ import Foundation import PromiseKit import PushKit import SignalUtilitiesKit -import SignalUtilitiesKit +import GRDB public enum PushRegistrationError: Error { case assertionError(description: String) @@ -251,6 +251,9 @@ public enum PushRegistrationError: Error { return } + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + let maybeCall: SessionCall? = Storage.shared.write { db in let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo( state: (caller == getUserHexEncodedPublicKey(db) ? @@ -259,7 +262,13 @@ public enum PushRegistrationError: Error { ) ) - guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil } + let messageInfoString: String? = { + if let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) { + return String(data: messageInfoData, encoding: .utf8) + } else { + return "Incoming call." // TODO: We can do better here. + } + }() let call: SessionCall = SessionCall(db, for: caller, uuid: uuid, mode: .answer) let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: caller, variant: .contact) @@ -269,7 +278,7 @@ public enum PushRegistrationError: Error { threadId: thread.id, authorId: caller, variant: .infoCall, - body: String(data: messageInfoData, encoding: .utf8), + body: messageInfoString, timestampMs: timestampMs ).inserted(db) call.callInteractionId = interaction.id diff --git a/Session/Onboarding/LandingVC.swift b/Session/Onboarding/LandingVC.swift index 01978a734..dd32d0318 100644 --- a/Session/Onboarding/LandingVC.swift +++ b/Session/Onboarding/LandingVC.swift @@ -74,7 +74,7 @@ final class LandingVC: BaseVC { 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 diff --git a/SessionMessagingKit/Calls/CurrentCallProtocol.swift b/SessionMessagingKit/Calls/CurrentCallProtocol.swift index 6968116db..8cdaf7ff7 100644 --- a/SessionMessagingKit/Calls/CurrentCallProtocol.swift +++ b/SessionMessagingKit/Calls/CurrentCallProtocol.swift @@ -9,6 +9,7 @@ public protocol CurrentCallProtocol { var callId: UUID { get } var webRTCSession: WebRTCSession { get } var hasStartedConnecting: Bool { get set } + var hasEnded: Bool { get set } func updateCallMessage(mode: EndCallMode) func didReceiveRemoteSDP(sdp: RTCSessionDescription) diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index fe0830dcb..a80470272 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) } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 9dec50347..dbd6acc86 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,16 +365,59 @@ 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 ignore all reactions + let pendingChangeRemoveAllReaction: Bool = associatedPendingChanges.contains { pendingChange in + if case .reaction(_, let emoji, let action) = pendingChange.metadata { + return emoji == decodedEmoji && action == .removeAll + } + return false + } + + // Decide whether we need to add an extra reaction from current user + let pendingChangeSelfReaction: Bool? = { + // Find the newest 'PendingChange' entry with a matching emoji, if one exists, and + // set the "self reaction" value based on it's action + let maybePendingChange: OpenGroupAPI.PendingChange? = associatedPendingChanges + .sorted(by: { lhs, rhs -> Bool in (lhs.seqNo ?? Int64.max) >= (rhs.seqNo ?? Int64.max) }) + .first { pendingChange in + if case .reaction(_, let emoji, _) = pendingChange.metadata { + return emoji == decodedEmoji + } + + return false + } + + // If there is no pending change for this reaction then return nil + guard + let pendingChange: OpenGroupAPI.PendingChange = maybePendingChange, + case .reaction(_, _, let action) = pendingChange.metadata + else { return nil } + + // Otherwise add/remove accordingly + return action == .add + }() + let shouldAddSelfReaction: Bool = ( + pendingChangeSelfReaction ?? + ((rawReaction.you || reactors.contains(userPublicKey)) && !pendingChangeRemoveAllReaction) + ) + + let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count + let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000))) + let maxLength: Int = shouldAddSelfReaction ? 4 : 5 let desiredReactorIds: [String] = reactors - .filter { $0 != blindedUserPublicKey } + .filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed + .prefix(maxLength) + .map{ $0 } results = results .appending( // Add the first reaction (with the count) + pendingChangeRemoveAllReaction ? + nil : desiredReactorIds.first .map { reactor in Reaction( @@ -381,14 +425,14 @@ public extension Message { serverHash: nil, timestampMs: timestampMs, authorId: reactor, - emoji: emoji, - count: rawReaction.count, + emoji: decodedEmoji, + count: count, sortId: rawReaction.index ) } ) .appending( // Add all other reactions - contentsOf: desiredReactorIds.count <= 1 ? + contentsOf: desiredReactorIds.count <= 1 || pendingChangeRemoveAllReaction ? [] : desiredReactorIds .suffix(from: 1) @@ -398,22 +442,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..dd5af98b5 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/PendingChange.swift @@ -0,0 +1,47 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPI { + public struct PendingChange: Equatable { + public enum ChangeType { + case reaction + } + + public enum ReactAction: Equatable { + case add + case remove + case removeAll + } + + enum Metadata { + case reaction(messageId: Int64, emoji: String, action: ReactAction) + } + + let server: String + let room: String + let changeType: ChangeType + var seqNo: Int64? + let metadata: Metadata + + public static func == (lhs: OpenGroupAPI.PendingChange, rhs: OpenGroupAPI.PendingChange) -> Bool { + guard lhs.server == rhs.server && + lhs.room == rhs.room && + lhs.changeType == rhs.changeType && + lhs.seqNo == rhs.seqNo + else { + return false + } + + switch lhs.changeType { + case .reaction: + if case .reaction(let lhsMessageId, let lhsEmoji, let lhsAction) = lhs.metadata, + case .reaction(let rhsMessageId, let rhsEmoji, let rhsAction) = rhs.metadata { + return lhsMessageId == rhsMessageId && lhsEmoji == rhsEmoji && lhsAction == rhsAction + } else { + return false + } + } + } + } +} diff --git a/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift b/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift new file mode 100644 index 000000000..cfded186d --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/ReactionResponse.swift @@ -0,0 +1,44 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPI { + public struct ReactionAddResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case added + case seqNo = "seqno" + } + + /// This field indicates whether the reaction was added (true) or already present (false). + public let added: Bool + + /// The seqNo after the reaction is added. + public let seqNo: Int64? + } + + public struct ReactionRemoveResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case removed + case seqNo = "seqno" + } + + /// This field indicates whether the reaction was removed (true) or was not present to begin with (false). + public let removed: Bool + + /// The seqNo after the reaction is removed. + public let seqNo: Int64? + } + + public struct ReactionRemoveAllResponse: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case removed + case seqNo = "seqno" + } + + /// This field shows the total number of reactions that were deleted. + public let removed: Int64 + + /// The seqNo after the reactions is all removed. + public let seqNo: Int64? + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index bcef9def5..04cf63b57 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 @@ -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 { @@ -718,7 +718,7 @@ public enum OpenGroupAPI { ), using: dependencies ) - .map { responseInfo, _ in responseInfo } + .decoded(as: ReactionAddResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) } public static func reactionDelete( @@ -728,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 { @@ -745,7 +745,7 @@ public enum OpenGroupAPI { ), using: dependencies ) - .map { responseInfo, _ in responseInfo } + .decoded(as: ReactionRemoveResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) } public static func reactionDeleteAll( @@ -755,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 { @@ -772,7 +772,7 @@ public enum OpenGroupAPI { ), using: dependencies ) - .map { responseInfo, _ in responseInfo } + .decoded(as: ReactionRemoveAllResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) } // MARK: - Pinning diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 553fe0786..7afea37a0 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -19,6 +19,8 @@ public protocol OGMCacheType { var hasPerformedInitialPoll: [String: Bool] { get set } var timeSinceLastPoll: [String: TimeInterval] { get set } + var pendingChanges: [OpenGroupAPI.PendingChange] { get set } + func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval } @@ -53,6 +55,8 @@ public final class OpenGroupManager: NSObject { _timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen) return dependencies.date.timeIntervalSince(lastOpen) } + + public var pendingChanges: [OpenGroupAPI.PendingChange] = [] } // MARK: - Variables @@ -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,11 +599,23 @@ 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 ) try MessageReceiver.handleOpenGroupReactions( db, + threadId: openGroup.threadId, openGroupMessageServerId: message.id, openGroupReactions: reactions ) @@ -737,6 +759,55 @@ public final class OpenGroupManager: NSObject { // MARK: - Convenience + public static func addPendingReaction( + emoji: String, + id: Int64, + in roomToken: String, + on server: String, + type: OpenGroupAPI.PendingChange.ReactAction, + using dependencies: OGMDependencies = OGMDependencies() + ) -> OpenGroupAPI.PendingChange { + let pendingChange = OpenGroupAPI.PendingChange( + server: server, + room: roomToken, + changeType: .reaction, + metadata: .reaction( + messageId: id, + emoji: emoji, + action: type + ) + ) + + dependencies.mutableCache.mutate { + $0.pendingChanges.append(pendingChange) + } + + return pendingChange + } + + public static func updatePendingChange( + _ pendingChange: OpenGroupAPI.PendingChange, + seqNo: Int64?, + using dependencies: OGMDependencies = OGMDependencies() + ) { + dependencies.mutableCache.mutate { + if let index = $0.pendingChanges.firstIndex(of: pendingChange) { + $0.pendingChanges[index].seqNo = seqNo + } + } + } + + public static func removePendingChange( + _ pendingChange: OpenGroupAPI.PendingChange, + using dependencies: OGMDependencies = OGMDependencies() + ) { + dependencies.mutableCache.mutate { + if let index = $0.pendingChanges.firstIndex(of: pendingChange) { + $0.pendingChanges.remove(at: index) + } + } + } + /// This method specifies if the given capability is supported on a specified Open Group public static func isOpenGroupSupport( _ capability: Capability.Variant, @@ -1039,10 +1110,10 @@ public final class OpenGroupManager: NSObject { extension OpenGroupManager { public class OGMDependencies: SMKDependencies { - internal var _mutableCache: Atomic? + internal var _mutableCache: Atomic?> public var mutableCache: Atomic { get { Dependencies.getValueSettingIfNull(&_mutableCache) { OpenGroupManager.shared.mutableCache } } - set { _mutableCache = newValue } + set { _mutableCache.mutate { $0 = newValue } } } public var cache: OGMCacheType { return mutableCache.wrappedValue } @@ -1063,7 +1134,7 @@ extension OpenGroupManager { standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { - _mutableCache = cache + _mutableCache = Atomic(cache) super.init( onionApi: onionApi, diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 10018d5e1..12e15fc9c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -340,13 +340,14 @@ extension MessageReceiver { sortId: sortId ) try reaction.insert(db) - Environment.shared?.notificationsManager.wrappedValue? - .notifyUser( - db, - forReaction: reaction, - in: thread - ) - + if sender != getUserHexEncodedPublicKey(db) { + Environment.shared?.notificationsManager.wrappedValue? + .notifyUser( + db, + forReaction: reaction, + in: thread + ) + } case .remove: try Reaction .filter(Reaction.Columns.interactionId == interactionId) 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) 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 } 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/Preferences.swift b/SessionMessagingKit/Utilities/Preferences.swift index c5d7c7301..cfb9a4681 100644 --- a/SessionMessagingKit/Utilities/Preferences.swift +++ b/SessionMessagingKit/Utilities/Preferences.swift @@ -244,7 +244,7 @@ public enum Preferences { // Other case .messageSent: return "message_sent.aiff" - case .none: return nil + case .none: return "silence.aiff" } } 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/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index 48598cd03..d581ac276 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -230,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 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/MockOGMCache.swift b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift index 31bace48f..02caa5e85 100644 --- a/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift +++ b/SessionMessagingKitTests/_TestUtilities/MockOGMCache.swift @@ -37,6 +37,11 @@ class MockOGMCache: Mock, OGMCacheType { set { accept(args: [newValue]) } } + var pendingChanges: [OpenGroupAPI.PendingChange] { + get { return accept() as! [OpenGroupAPI.PendingChange] } + set { accept(args: [newValue]) } + } + func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval { return accept(args: [dependencies]) as! TimeInterval } diff --git a/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift b/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift index d559bdfec..0d2f8cee9 100644 --- a/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift +++ b/SessionMessagingKitTests/_TestUtilities/OGMDependencyExtensions.swift @@ -24,20 +24,20 @@ extension OpenGroupManager.OGMDependencies { date: Date? = nil ) -> OpenGroupManager.OGMDependencies { return OpenGroupManager.OGMDependencies( - cache: (cache ?? self._mutableCache), - onionApi: (onionApi ?? self._onionApi), - generalCache: (generalCache ?? self._generalCache), - storage: (storage ?? self._storage), - sodium: (sodium ?? self._sodium), - box: (box ?? self._box), - genericHash: (genericHash ?? self._genericHash), - sign: (sign ?? self._sign), - aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf), - ed25519: (ed25519 ?? self._ed25519), - nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16), - nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24), - standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults), - date: (date ?? self._date) + cache: (cache ?? self._mutableCache.wrappedValue), + onionApi: (onionApi ?? self._onionApi.wrappedValue), + generalCache: (generalCache ?? self._generalCache.wrappedValue), + storage: (storage ?? self._storage.wrappedValue), + sodium: (sodium ?? self._sodium.wrappedValue), + box: (box ?? self._box.wrappedValue), + genericHash: (genericHash ?? self._genericHash.wrappedValue), + sign: (sign ?? self._sign.wrappedValue), + aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf.wrappedValue), + ed25519: (ed25519 ?? self._ed25519.wrappedValue), + nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16.wrappedValue), + nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24.wrappedValue), + standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults.wrappedValue), + date: (date ?? self._date.wrappedValue) ) } } diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 60b7f46af..eb74f0f77 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -157,18 +157,15 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { notificationContent.badge = NSNumber(value: newBadgeNumber) CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber") - notificationContent.title = interaction.previewText(db) + notificationContent.title = "Session" notificationContent.body = "" + let senderName: String = Profile.displayName(db, id: interaction.authorId, threadVariant: thread.variant) + if messageInfo.state == .permissionDenied { notificationContent.body = String( format: "modal_call_missed_tips_explanation".localized(), - SessionThread.displayName( - threadId: thread.id, - variant: thread.variant, - closedGroupName: nil, // Not supported - openGroupName: nil // Not supported - ) + senderName ) } diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 5705a4661..7cfeae747 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -25,6 +25,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension self.contentHandler = contentHandler self.request = request + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) + guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { return self.completeSilenty() } @@ -237,6 +240,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension private func completeSilenty() { SNLog("Complete silenty") + + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + self.contentHandler!(.init()) } @@ -298,11 +305,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension SNLog("Add remote notification request") } - private func handleSuccess(for content: UNMutableNotificationContent) { - contentHandler!(content) - } - private func handleFailure(for content: UNMutableNotificationContent) { + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) + content.body = "You've got a new message" content.title = "Session" let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 5ffc7ae20..68c706db3 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -180,6 +180,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView shareVC?.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in + // Resume database + NotificationCenter.default.post(name: Database.resumeNotification, object: self) Storage.shared .writeAsync { [weak self] db -> Promise in guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { @@ -231,10 +233,14 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView ) } .done { [weak self] _ in + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) activityIndicator.dismiss { } self?.shareVC?.shareViewWasCompleted() } .catch { [weak self] error in + // Suspend the database + NotificationCenter.default.post(name: Database.suspendNotification, object: self) activityIndicator.dismiss { } self?.shareVC?.shareViewFailed(error: error) } 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/SessionUIKit/Style Guide/Colors.swift b/SessionUIKit/Style Guide/Colors.swift index 3059b4518..aba3a004a 100644 --- a/SessionUIKit/Style Guide/Colors.swift +++ b/SessionUIKit/Style Guide/Colors.swift @@ -49,5 +49,6 @@ 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 blockActionBackground: UIColor { UIColor(named: "session_block_action_background")! } @objc public static var sessionEmojiPlusButtonBackground: UIColor { UIColor(named: "session_emoji_plus_button_background")! } } diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_block_action_background.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_block_action_background.colorset/Contents.json new file mode 100644 index 000000000..92837aec8 --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_block_action_background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x6D", + "green" : "0x6D", + "red" : "0x6D" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x2D", + "green" : "0x2D", + "red" : "0x2D" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_cell_pinned.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_cell_pinned.colorset/Contents.json index e871732bc..f5227c201 100644 --- a/SessionUIKit/Style Guide/Colors.xcassets/session_cell_pinned.colorset/Contents.json +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_cell_pinned.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "28", - "green" : "28", - "red" : "28" + "blue" : "0x1C", + "green" : "0x1C", + "red" : "0x1C" } }, "idiom" : "universal" diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_destructive.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_destructive.colorset/Contents.json index afefc4599..f79b1cb0f 100644 --- a/SessionUIKit/Style Guide/Colors.xcassets/session_destructive.colorset/Contents.json +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_destructive.colorset/Contents.json @@ -6,7 +6,7 @@ "components" : { "alpha" : "1.000", "blue" : "0x3A", - "green" : "0x45", + "green" : "0x3A", "red" : "0xFF" } }, diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_navigation_bar_background.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_navigation_bar_background.colorset/Contents.json index 9cece1931..a58fd37fe 100644 --- a/SessionUIKit/Style Guide/Colors.xcassets/session_navigation_bar_background.colorset/Contents.json +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_navigation_bar_background.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "22", - "green" : "22", - "red" : "22" + "blue" : "0x16", + "green" : "0x16", + "red" : "0x16" } }, "idiom" : "universal" diff --git a/SessionUtilitiesKit/General/Dependencies.swift b/SessionUtilitiesKit/General/Dependencies.swift index 5ac20c999..a0c3f132c 100644 --- a/SessionUtilitiesKit/General/Dependencies.swift +++ b/SessionUtilitiesKit/General/Dependencies.swift @@ -3,28 +3,28 @@ import Foundation open class Dependencies { - public var _generalCache: Atomic? + public var _generalCache: Atomic?> public var generalCache: Atomic { get { Dependencies.getValueSettingIfNull(&_generalCache) { General.cache } } - set { _generalCache = newValue } + set { _generalCache.mutate { $0 = newValue } } } - public var _storage: Storage? + public var _storage: Atomic public var storage: Storage { get { Dependencies.getValueSettingIfNull(&_storage) { Storage.shared } } - set { _storage = newValue } + set { _storage.mutate { $0 = newValue } } } - public var _standardUserDefaults: UserDefaultsType? + public var _standardUserDefaults: Atomic public var standardUserDefaults: UserDefaultsType { get { Dependencies.getValueSettingIfNull(&_standardUserDefaults) { UserDefaults.standard } } - set { _standardUserDefaults = newValue } + set { _standardUserDefaults.mutate { $0 = newValue } } } - public var _date: Date? + public var _date: Atomic public var date: Date { get { Dependencies.getValueSettingIfNull(&_date) { Date() } } - set { _date = newValue } + set { _date.mutate { $0 = newValue } } } // MARK: - Initialization @@ -35,21 +35,29 @@ open class Dependencies { standardUserDefaults: UserDefaultsType? = nil, date: Date? = nil ) { - _generalCache = generalCache - _storage = storage - _standardUserDefaults = standardUserDefaults - _date = date + _generalCache = Atomic(generalCache) + _storage = Atomic(storage) + _standardUserDefaults = Atomic(standardUserDefaults) + _date = Atomic(date) } // MARK: - Convenience - - public static func getValueSettingIfNull(_ maybeValue: inout T?, _ valueGenerator: () -> T) -> T { - guard let value: T = maybeValue else { + + public static func getValueSettingIfNull(_ maybeValue: inout Atomic, _ valueGenerator: () -> T) -> T { + guard let value: T = maybeValue.wrappedValue else { let value: T = valueGenerator() - maybeValue = value + maybeValue.mutate { $0 = value } return value } return value } + +// 0 libswiftCore.dylib 0x00000001999fd40c _swift_release_dealloc + 32 (HeapObject.cpp:703) +// 1 SessionMessagingKit 0x0000000106aa958c 0x106860000 + 2397580 +// 2 libswiftCore.dylib 0x00000001999fd424 _swift_release_dealloc + 56 (HeapObject.cpp:703) +// 3 SessionUtilitiesKit 0x0000000106cbd980 static Dependencies.getValueSettingIfNull(_:_:) + 264 (Dependencies.swift:49) +// 4 SessionMessagingKit 0x0000000106aa90f4 closure #1 in SMKDependencies.sign.getter + 112 (SMKDependencies.swift:17) +// 5 SessionUtilitiesKit 0x0000000106cbd974 static Dependencies.getValueSettingIfNull(_:_:) + 252 (Dependencies.swift:48) +// 6 SessionMessagingKit 0x000000010697aef8 specialized static OpenGroupAPI.sign(_:messageBytes:for:fallbackSigningType:using:) + 1158904 (OpenGroupAPI.swift:1190) } diff --git a/SignalUtilitiesKit/Utilities/CommonStrings.swift b/SignalUtilitiesKit/Utilities/CommonStrings.swift index b3c7f6c88..d67f49534 100644 --- a/SignalUtilitiesKit/Utilities/CommonStrings.swift +++ b/SignalUtilitiesKit/Utilities/CommonStrings.swift @@ -58,4 +58,8 @@ public class NotificationStrings: NSObject { @objc public class MediaStrings: NSObject { @objc static public let allMedia = NSLocalizedString("MEDIA_DETAIL_VIEW_ALL_MEDIA_BUTTON", comment: "nav bar button item") + @objc + static public let media = NSLocalizedString("MEDIA_TAB_TITLE", comment: "media tab title") + @objc + static public let document = NSLocalizedString("DOCUMENT_TAB_TITLE", comment: "document tab title") }