diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 02ce20a3b9..b3a2b5a1fe 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6045E825AE900A92D61FEFF0 /* ImageAnonymizerTests.swift */; }; + 0B4F67EF7AD6264047613E0B /* EmojiProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E31F530ED515C10A5915D1B9 /* EmojiProviderTests.swift */; }; 0BEFE400B4802FE8C9DB39B3 /* FilePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62BDF0FF4F59AF6EA858B70B /* FilePreviewViewModel.swift */; }; 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */; }; 0C38C3E771B472E27295339D /* SessionVerificationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */; }; @@ -61,6 +62,7 @@ 1AE4AEA0FA8DEF52671832E0 /* RoomTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */; }; 1B4B3E847BF944DB2C1C217F /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 1CF18DE71D5D23C61BD88852 /* DebugScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9238D3A3A00F45E841FE4EFF /* DebugScreen.swift */; }; + 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */; }; 1E59B77A0B2CE83DCC1B203C /* LoginViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A05707BF550D770168A406DB /* LoginViewModelTests.swift */; }; 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */; }; 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; }; @@ -86,6 +88,7 @@ 2B9AEEC12B1BBE5BD61D0F5E /* UserSessionFlowCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */; }; 2BA59D0AEFB4B82A2EC2A326 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; 2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; }; + 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; }; 2CA8AD07773A38BA4662098B /* MediaProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */; }; 2CB6787E25B11711518E9588 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6281B199D8A8F0892490C2E /* OnboardingCoordinator.swift */; }; 2D794361CFE790C8FB3C9C0F /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; @@ -142,6 +145,7 @@ 4E945AD6862C403F74E57755 /* RoomTimelineItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105B2A8426404EF66F00CFDB /* RoomTimelineItemFactory.swift */; }; 4FC1EFE4968A259CBBACFAFB /* RoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */; }; 4FF90E2242DBD596E1ED2E27 /* AppCoordinatorStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */; }; + 501304F26B52DF7024011B6C /* EmojiMartJSONLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF9E3E6A23180EC05F06460 /* EmojiMartJSONLoaderTests.swift */; }; 50C90117FE25390BFBD40173 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; }; 51DB67C5B5BC68B0A6FF54D4 /* MockRoomProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */; }; @@ -157,11 +161,18 @@ 5B8B51CEC4717AF487794685 /* NotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; }; + 5D04B17929378AB300FD5B00 /* apple_emojis_data.json in Resources */ = {isa = PBXBuildFile; fileRef = 5D04B17829378AB300FD5B00 /* apple_emojis_data.json */; }; + 5D04B17B29378D3600FD5B00 /* EmojiMartEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D04B17A29378D3600FD5B00 /* EmojiMartEmoji.swift */; }; + 5D04B17D2937ADE300FD5B00 /* EmojiItemSkin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D04B17C2937ADE300FD5B00 /* EmojiItemSkin.swift */; }; + 5D04B17F293A333600FD5B00 /* EmojiPickerSearchFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D04B17E293A333600FD5B00 /* EmojiPickerSearchFieldView.swift */; }; + 5D04B181293A337400FD5B00 /* EmojiPickerHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D04B180293A337400FD5B00 /* EmojiPickerHeaderView.swift */; }; 5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */; }; 5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; }; 5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; 5D7960B32C350FA93F48D02B /* OnboardingModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */; }; 5D9F0695DC6C0057F85C12B6 /* UserNotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */; }; + 5DE2282C293F29FC001790FD /* EmojiProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE2282B293F29FC001790FD /* EmojiProvider.swift */; }; + 5DE2282E293F2CF6001790FD /* EmojiLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE2282D293F2CF6001790FD /* EmojiLoaderProtocol.swift */; }; 5E0F2E612718BB4397A6D40A /* TextRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */; }; 5E25568E1CDAD983517E58B5 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179423E34EE846E048E49CBF /* MediaSourceProxy.swift */; }; 5E540CAEF764D7FBD8D80776 /* VideoPlayerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A3FC45B7643298BF361CEB1 /* VideoPlayerModels.swift */; }; @@ -183,6 +194,7 @@ 67D6E0700A9C1E676F6231F8 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; }; 67E391A2E00709FB41903B36 /* MockMediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */; }; 6832733838C57A7D3FE8FEB5 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 36B7FC232711031AA2B0D188 /* DTCoreText */; }; + 68998AE39FDE863ED69711F8 /* TimelineItemReactionsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA4807E660393F385D673DA2 /* TimelineItemReactionsMenuView.swift */; }; 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; }; 690ED5315B401238A3249DCB /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3FDFF4C1153D263BAB93C1F3 /* README.md */; }; 69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; @@ -203,9 +215,11 @@ 7096FA3AC218D914E88BFB70 /* AggregratedReaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */; }; 719E7AAD1F8E68F68F30FECD /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40C19719687984FD9478FBE /* Task.swift */; }; 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; + 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 744C029EB6C43429926A0499 /* AnalyticsPromptViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A86C95340248A8B7BA9A43 /* AnalyticsPromptViewModelProtocol.swift */; }; 74604ACFDBE7F54260E7B617 /* ApplicationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */; }; + 748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */; }; 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; 758BF44CA565AB0AB84F2185 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 75EA4ABBFAA810AFF289D6F4 /* TemplateViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB6E40BAD4504D899FAAC9A /* TemplateViewModel.swift */; }; @@ -231,6 +245,7 @@ 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; 8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */; }; + 8292BBE82AD31A393183CF28 /* EmojiPickerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E016C12585FC5A6EA6941B5 /* EmojiPickerScreen.swift */; }; 834DD9E41FC42A509BAD52E3 /* NavigationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */; }; 83E5054739949181CA981193 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD667C4BB98CF4F3FE2CE3B0 /* LoginCoordinator.swift */; }; 841172E1576A863F4450132D /* WeakKeyDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0ADFDC712027931F2216668 /* WeakKeyDictionary.swift */; }; @@ -255,6 +270,7 @@ 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; 91DFCB641FBA03EE2DA0189E /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FB27E1BE894F9F9F0134372 /* FilePreviewScreen.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; + 92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; 93BA4A81B6D893271101F9F0 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; 9462C62798F47E39DCC182D2 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA89A2DD51B6BBE1DA55E263 /* Application.swift */; }; @@ -339,11 +355,13 @@ BCEC41FB1F2BB663183863E4 /* LoginServerInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D379E13DD9D987470A3C70C /* LoginServerInfoSection.swift */; }; BD782053BE4C3D2F0BDE5699 /* ServiceLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */; }; BFB534E338A3D949944FB2F5 /* NotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */; }; + C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; }; C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */; }; C35CF4DAB1467FE1BBDC204B /* MessageTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAF1C75771D9DC75877F4B4 /* MessageTimelineItem.swift */; }; C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; + C5FDDC40ABD907B7C47F89AB /* EmojiMartJSONLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3553C7E1218AB3EF08FFEEA4 /* EmojiMartJSONLoader.swift */; }; C74EE50257ED925C2B8EFCE6 /* MockSoftLogoutScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B869438A1B52836F912A702 /* MockSoftLogoutScreenState.swift */; }; C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; C7B251DC896C0867C51B616D /* AnalyticsPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541542F5AC323709D8563458 /* AnalyticsPrompt.swift */; }; @@ -369,6 +387,7 @@ D0619D2E6B9C511190FBEB95 /* RoomMessageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607974D08BD2AF83725D817A /* RoomMessageProtocol.swift */; }; D2D70B5DB1A5E4AF0CD88330 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 033DB41C51865A2E83174E87 /* target.yml */; }; D3E603A5E9D529CF293E1BF9 /* VideoPlayerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1651A532305027D3F605E2B /* VideoPlayerCoordinator.swift */; }; + D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; }; D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A45283CF1DB96E583BECA6 /* ImageRoomTimelineView.swift */; }; D6417E5A799C3C7F14F9EC0A /* SessionVerificationViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3069ADED46D063202FE7698 /* SessionVerificationViewModelProtocol.swift */; }; D79F0F852C6A4255D5E616D2 /* UserNotificationControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED2D2F6A137A95EA50413BE /* UserNotificationControllerProtocol.swift */; }; @@ -403,6 +422,7 @@ EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; }; EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; }; EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; }; + EE6933C935080B4E0348A58B /* EmojiMartCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5C3AACCAA82392D08924496 /* EmojiMartCategory.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; @@ -503,6 +523,7 @@ 0DB634B42CFE667112369D57 /* VideoPlayerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerScreen.swift; sourceTree = ""; }; 0DD16CE9A66C9040B066AD60 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = ""; }; 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerTests.swift; sourceTree = ""; }; + 0E016C12585FC5A6EA6941B5 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = ""; }; 0E7062F88E9D5F79C8A80524 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = ""; }; 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = ""; }; 0EE9EAF0309A2A1D67D8FAF5 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -514,6 +535,7 @@ 109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControllerTests.swift; sourceTree = ""; }; 10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderProtocol.swift; sourceTree = ""; }; 1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationController.swift; sourceTree = ""; }; + 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModelProtocol.swift; sourceTree = ""; }; 111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; 113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; 1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -586,15 +608,18 @@ 33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = ""; }; 3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = ""; }; 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = ""; }; + 3553C7E1218AB3EF08FFEEA4 /* EmojiMartJSONLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartJSONLoader.swift; sourceTree = ""; }; 3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; 3782C506F4FF1AADF61B6212 /* tlh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tlh; path = tlh.lproj/Localizable.strings; sourceTree = ""; }; + 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = ""; }; 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = ""; }; 399427358A80BA2848E698A2 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/Localizable.strings"; sourceTree = ""; }; 39EBB6903EFD4236B8D11A42 /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = ""; }; 3B5B535DA49C54523FF7A412 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/Localizable.strings; sourceTree = ""; }; + 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = ""; }; 3CDF9E55650D6035D6536538 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = ""; }; 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = ""; }; 3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = ""; }; @@ -662,8 +687,15 @@ 5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; 57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = ""; }; 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = ""; }; + 5D04B17829378AB300FD5B00 /* apple_emojis_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = ""; }; + 5D04B17A29378D3600FD5B00 /* EmojiMartEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartEmoji.swift; sourceTree = ""; }; + 5D04B17C2937ADE300FD5B00 /* EmojiItemSkin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemSkin.swift; sourceTree = ""; }; + 5D04B17E293A333600FD5B00 /* EmojiPickerSearchFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSearchFieldView.swift; sourceTree = ""; }; + 5D04B180293A337400FD5B00 /* EmojiPickerHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerHeaderView.swift; sourceTree = ""; }; 5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = ""; }; 5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = ""; }; + 5DE2282B293F29FC001790FD /* EmojiProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiProvider.swift; sourceTree = ""; }; + 5DE2282D293F2CF6001790FD /* EmojiLoaderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiLoaderProtocol.swift; sourceTree = ""; }; 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = ""; }; @@ -762,6 +794,7 @@ 9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = ""; }; 9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationItemProxy+NSE.swift"; sourceTree = ""; }; 9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = ""; }; + 9BF9E3E6A23180EC05F06460 /* EmojiMartJSONLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartJSONLoaderTests.swift; sourceTree = ""; }; 9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurementParser.swift; sourceTree = ""; }; 9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = ""; }; 9C7F7DE62D33C6A26CBFCD72 /* IntegrationTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -834,6 +867,7 @@ BB33A751BFDA223BDD106EC0 /* OnboardingModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModels.swift; sourceTree = ""; }; BC9B05D6B293A039EB963CA7 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = ""; }; BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = ""; }; + BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = ""; }; BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = ""; }; BEE6BF9BA63FF42F8AF6EEEA /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = ""; }; C00A7110B937C6AE2EF5D7D6 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = ""; }; @@ -842,6 +876,7 @@ C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; }; C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; + C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartStore.swift; sourceTree = ""; }; C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelProtocol.swift; sourceTree = ""; }; C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; @@ -906,6 +941,7 @@ E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = ""; }; E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; + E31F530ED515C10A5915D1B9 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = ""; }; E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = ""; }; E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = ""; }; E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = ""; }; @@ -914,6 +950,7 @@ E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = ""; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = ""; }; + E5C3AACCAA82392D08924496 /* EmojiMartCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartCategory.swift; sourceTree = ""; }; E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -938,11 +975,13 @@ F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetectorTests.swift; sourceTree = ""; }; F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = ""; }; F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; + F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = ""; }; F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = ""; }; F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationToastView.swift; sourceTree = ""; }; F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = ""; }; F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCacheTests.swift; sourceTree = ""; }; + F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModel.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetector.swift; sourceTree = ""; }; F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = ""; }; @@ -951,6 +990,7 @@ F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = ""; }; FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = ""; }; + FA4807E660393F385D673DA2 /* TimelineItemReactionsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReactionsMenuView.swift; sourceTree = ""; }; FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = ""; }; FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = ""; }; FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProxyProtocol.swift; sourceTree = ""; }; @@ -1059,6 +1099,7 @@ 0ED3F5C21537519389C07644 /* BugReport */, 2D6DC9871FD7173E51D67C73 /* Cache */, 8039515BAA53B7C3275AC64A /* Client */, + 39557ADF21345E18F3865B9E /* Emojis */, CA555F7C7CA382ACACF0D82B /* Keychain */, 79E560F5113ED25D172E550C /* Media */, 6DE13A7AE6587B079F4049D7 /* Notification */, @@ -1183,6 +1224,16 @@ path = UITests; sourceTree = ""; }; + 323160803A296713F839540B /* View */ = { + isa = PBXGroup; + children = ( + 0E016C12585FC5A6EA6941B5 /* EmojiPickerScreen.swift */, + 5D04B17E293A333600FD5B00 /* EmojiPickerSearchFieldView.swift */, + 5D04B180293A337400FD5B00 /* EmojiPickerHeaderView.swift */, + ); + path = View; + sourceTree = ""; + }; 328DD5DA1281F758B72006C7 /* Views */ = { isa = PBXGroup; children = ( @@ -1215,6 +1266,19 @@ path = ServerSelection; sourceTree = ""; }; + 39557ADF21345E18F3865B9E /* Emojis */ = { + isa = PBXGroup; + children = ( + 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */, + 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */, + 5DE2282D293F2CF6001790FD /* EmojiLoaderProtocol.swift */, + 5DE2282B293F29FC001790FD /* EmojiProvider.swift */, + 5BACB442D02C878293C04837 /* EmojiMart */, + 5D04B17C2937ADE300FD5B00 /* EmojiItemSkin.swift */, + ); + path = Emojis; + sourceTree = ""; + }; 3A304097A59704AC9B869EC6 /* Helpers */ = { isa = PBXGroup; children = ( @@ -1438,6 +1502,18 @@ path = TimeLineItemContent; sourceTree = ""; }; + 5BACB442D02C878293C04837 /* EmojiMart */ = { + isa = PBXGroup; + children = ( + 3553C7E1218AB3EF08FFEEA4 /* EmojiMartJSONLoader.swift */, + 5D04B17829378AB300FD5B00 /* apple_emojis_data.json */, + E5C3AACCAA82392D08924496 /* EmojiMartCategory.swift */, + C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */, + 5D04B17A29378D3600FD5B00 /* EmojiMartEmoji.swift */, + ); + path = EmojiMart; + sourceTree = ""; + }; 5E01022071DDDC48EF453374 /* View */ = { isa = PBXGroup; children = ( @@ -1548,6 +1624,8 @@ EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */, 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */, DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */, + 9BF9E3E6A23180EC05F06460 /* EmojiMartJSONLoaderTests.swift */, + E31F530ED515C10A5915D1B9 /* EmojiProviderTests.swift */, F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */, DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */, 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */, @@ -2019,6 +2097,7 @@ isa = PBXGroup; children = ( D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */, + FA4807E660393F385D673DA2 /* TimelineItemReactionsMenuView.swift */, 351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */, ); path = Supplementary; @@ -2112,7 +2191,9 @@ E0EEBB2F7AA1BB36FC08F606 /* AnalyticsPrompt */, E74CD7681375AD2EAA34D66B /* Authentication */, 4009BE2E791C16AC6EE39A7E /* BugReport */, + F5A65D1D3B83593598DC278D /* EmojiPickerScreen */, B442FCF47E0A6F28D7D50A4D /* FilePreview */, + FC26FB522EDED4965C5325F0 /* Folder */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */, A448A3A8F764174C60CD0CA1 /* Other */, @@ -2171,6 +2252,25 @@ path = Background; sourceTree = ""; }; + F5A65D1D3B83593598DC278D /* EmojiPickerScreen */ = { + isa = PBXGroup; + children = ( + BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */, + F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */, + F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */, + 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */, + 323160803A296713F839540B /* View */, + ); + path = EmojiPickerScreen; + sourceTree = ""; + }; + FC26FB522EDED4965C5325F0 /* Folder */ = { + isa = PBXGroup; + children = ( + ); + path = Folder; + sourceTree = ""; + }; FCDF06BDB123505F0334B4F9 /* Timeline */ = { isa = PBXGroup; children = ( @@ -2475,6 +2575,7 @@ buildActionMask = 2147483647; files = ( B80C4FABB5529DF12436FFDA /* AppIcon.pdf in Resources */, + 5D04B17929378AB300FD5B00 /* apple_emojis_data.json in Resources */, 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */, B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */, AB34401E4E1CAD5D2EC3072B /* LaunchScreen.storyboard in Resources */, @@ -2641,6 +2742,8 @@ 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */, 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */, + 501304F26B52DF7024011B6C /* EmojiMartJSONLoaderTests.swift in Sources */, + 0B4F67EF7AD6264047613E0B /* EmojiProviderTests.swift in Sources */, 7E7DF1867F98B0D10A6C0A63 /* FileCacheTests.swift in Sources */, CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, @@ -2707,6 +2810,7 @@ 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */, CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, + 5DE2282E293F2CF6001790FD /* EmojiLoaderProtocol.swift in Sources */, B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */, A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */, 00F3059B1E0CFCA019710C3E /* BugReportModels.swift in Sources */, @@ -2726,9 +2830,21 @@ 1CF18DE71D5D23C61BD88852 /* DebugScreen.swift in Sources */, EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */, FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */, + 5D04B181293A337400FD5B00 /* EmojiPickerHeaderView.swift in Sources */, 9738F894DB1BD383BE05767A /* ElementSettings.swift in Sources */, D8CFF02C2730EE5BC4F17ABF /* ElementToggleStyle.swift in Sources */, 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, + 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */, + D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */, + EE6933C935080B4E0348A58B /* EmojiMartCategory.swift in Sources */, + C5FDDC40ABD907B7C47F89AB /* EmojiMartJSONLoader.swift in Sources */, + 92B95779840CD749117B3615 /* EmojiMartStore.swift in Sources */, + 5D04B17B29378D3600FD5B00 /* EmojiMartEmoji.swift in Sources */, + C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */, + 748F482FEF4E04D61C39AAD7 /* EmojiPickerScreenModels.swift in Sources */, + 8292BBE82AD31A393183CF28 /* EmojiPickerScreen.swift in Sources */, + 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */, + 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */, 6647430A45B4A8E692909A8F /* EmoteRoomTimelineItem.swift in Sources */, 68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */, F6E860FF7B18B81DF43B30B8 /* EncryptedRoomTimelineItem.swift in Sources */, @@ -2811,6 +2927,7 @@ 14132418A748C988B85B025E /* OnboardingPageIndicator.swift in Sources */, F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */, 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */, + 5D04B17F293A333600FD5B00 /* EmojiPickerSearchFieldView.swift in Sources */, CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */, 992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */, CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, @@ -2856,6 +2973,7 @@ 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */, 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */, BB01CC19C3D3322308D1B2CF /* ServerSelectionViewModel.swift in Sources */, + 5DE2282C293F29FC001790FD /* EmojiProvider.swift in Sources */, 19839F3526CE8C35AAF241AD /* ServerSelectionViewModelProtocol.swift in Sources */, BD782053BE4C3D2F0BDE5699 /* ServiceLocator.swift in Sources */, 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */, @@ -2883,6 +3001,7 @@ A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */, 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */, 44AE0752E001D1D10605CD88 /* Swipe.swift in Sources */, + 5D04B17D2937ADE300FD5B00 /* EmojiItemSkin.swift in Sources */, E290C78E7F09F47FD2662986 /* Task.swift in Sources */, 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */, 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */, @@ -2895,6 +3014,7 @@ 5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */, 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */, 01CB8ACFA5E143E89C168CA8 /* TimelineItemContextMenu.swift in Sources */, + 68998AE39FDE863ED69711F8 /* TimelineItemReactionsMenuView.swift in Sources */, F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */, 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */, 9B582B3EEFEA615D4A6FBF1A /* TimelineReactionsView.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index c4f6d02417..cfc6292cfe 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -2347,3 +2347,11 @@ "onboarding_new_app_layout_feedback_title" = "Give Feedback"; "onboarding_new_app_layout_feedback_message" = "Tap top right to see the option to feedback."; "onboarding_new_app_layout_button_try" = "Try it out"; +"emoji_picker_people_category" = "Smileys & People"; +"emoji_picker_nature_category" = "Animals & Nature"; +"emoji_picker_foods_category" = "Food & Drink"; +"emoji_picker_activity_category" = "Activities"; +"emoji_picker_places_category" = "Travel & Places"; +"emoji_picker_objects_category" = "Objects"; +"emoji_picker_symbols_category" = "Symbols"; +"emoji_picker_flags_category" = "Flags"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 2afcaab7cb..2384fde9ca 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -1239,6 +1239,22 @@ public enum ElementL10n { public static var editPollTitle: String { return ElementL10n.tr("Localizable", "edit_poll_title") } /// (edited) public static var editedSuffix: String { return ElementL10n.tr("Localizable", "edited_suffix") } + /// Activities + public static var emojiPickerActivityCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_activity_category") } + /// Flags + public static var emojiPickerFlagsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_flags_category") } + /// Food & Drink + public static var emojiPickerFoodsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_foods_category") } + /// Animals & Nature + public static var emojiPickerNatureCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_nature_category") } + /// Objects + public static var emojiPickerObjectsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_objects_category") } + /// Smileys & People + public static var emojiPickerPeopleCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_people_category") } + /// Travel & Places + public static var emojiPickerPlacesCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_places_category") } + /// Symbols + public static var emojiPickerSymbolsCategory: String { return ElementL10n.tr("Localizable", "emoji_picker_symbols_category") } /// Your contact book is empty public static var emptyContactBook: String { return ElementL10n.tr("Localizable", "empty_contact_book") } /// Encrypted message diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift new file mode 100644 index 0000000000..194f057407 --- /dev/null +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenCoordinator.swift @@ -0,0 +1,55 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct EmojiPickerScreenCoordinatorParameters { + let emojiProvider: EmojiProviderProtocol + let itemId: String +} + +enum EmojiPickerScreenCoordinatorAction { + case selectEmoji(emojiId: String, itemId: String) +} + +final class EmojiPickerScreenCoordinator: CoordinatorProtocol { + private let parameters: EmojiPickerScreenCoordinatorParameters + private var viewModel: EmojiPickerScreenViewModelProtocol + + var callback: ((EmojiPickerScreenCoordinatorAction) -> Void)? + + init(parameters: EmojiPickerScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = EmojiPickerScreenViewModel(emojiProvider: parameters.emojiProvider) + } + + func start() { + viewModel.callback = { [weak self] action in + guard let self else { return } + MXLog.debug("EmojiPickerScreenViewModel did complete with result: \(action).") + switch action { + case let .selectEmoji(emojiId: emojiId): + self.callback?(.selectEmoji(emojiId: emojiId, itemId: self.parameters.itemId)) + } + } + } + + func toPresentable() -> AnyView { + AnyView(EmojiPickerScreen(context: viewModel.context) + .presentationDetents([.medium, .large])) + } +} diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift new file mode 100644 index 0000000000..b2f75111d5 --- /dev/null +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift @@ -0,0 +1,64 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum EmojiPickerScreenViewModelAction { + case selectEmoji(emojiId: String) +} + +struct EmojiPickerScreenViewState: BindableState { + var categories: [EmojiPickerEmojiCategoryViewData] +} + +enum EmojiPickerScreenViewAction { + case search(searchString: String) + case emojiSelected(emoji: EmojiPickerEmojiViewData) +} + +struct EmojiPickerEmojiCategoryViewData: Identifiable { + let id: String + let emojis: [EmojiPickerEmojiViewData] + + var name: String { + switch id { + case "people": + return ElementL10n.emojiPickerPeopleCategory + case "nature": + return ElementL10n.emojiPickerNatureCategory + case "foods": + return ElementL10n.emojiPickerFoodsCategory + case "activity": + return ElementL10n.emojiPickerActivityCategory + case "places": + return ElementL10n.emojiPickerPlacesCategory + case "objects": + return ElementL10n.emojiPickerObjectsCategory + case "symbols": + return ElementL10n.emojiPickerSymbolsCategory + case "flags": + return ElementL10n.emojiPickerFlagsCategory + default: + MXLog.failure("Missing translation for emoji category with id \(id)") + return "" + } + } +} + +struct EmojiPickerEmojiViewData: Identifiable { + var id: String + let value: String +} diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift new file mode 100644 index 0000000000..3ca576521b --- /dev/null +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -0,0 +1,68 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +typealias EmojiPickerScreenViewModelType = StateStoreViewModel + +class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScreenViewModelProtocol { + var callback: ((EmojiPickerScreenViewModelAction) -> Void)? + + private let emojiProvider: EmojiProviderProtocol + + init(emojiProvider: EmojiProviderProtocol) { + let initialViewState = EmojiPickerScreenViewState(categories: []) + self.emojiProvider = emojiProvider + super.init(initialViewState: initialViewState) + loadEmojis() + } + + // MARK: - Public + + override func process(viewAction: EmojiPickerScreenViewAction) async { + switch viewAction { + case let .search(searchString: searchString): + let categories = await emojiProvider.getCategories(searchString: searchString) + state.categories = convert(emojiCategories: categories) + case let .emojiSelected(emoji: emoji): + callback?(.selectEmoji(emojiId: emoji.id)) + } + } + + // MARK: - Private + + private func loadEmojis() { + Task(priority: .userInitiated) { [weak self] in + let categories = await emojiProvider.getCategories(searchString: nil) + self?.state.categories = convert(emojiCategories: categories) + } + } + + private func convert(emojiCategories: [EmojiCategory]) -> [EmojiPickerEmojiCategoryViewData] { + emojiCategories.compactMap { emojiCategory in + + let emojisViewData: [EmojiPickerEmojiViewData] = emojiCategory.emojis.compactMap { emojiItem in + + guard let firstSkin = emojiItem.skins.first else { + return nil + } + return EmojiPickerEmojiViewData(id: emojiItem.id, value: firstSkin.value) + } + + return EmojiPickerEmojiCategoryViewData(id: emojiCategory.id, emojis: emojisViewData) + } + } +} diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModelProtocol.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModelProtocol.swift new file mode 100644 index 0000000000..1084995e38 --- /dev/null +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +@MainActor +protocol EmojiPickerScreenViewModelProtocol { + var callback: ((EmojiPickerScreenViewModelAction) -> Void)? { get set } + var context: EmojiPickerScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerHeaderView.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerHeaderView.swift new file mode 100644 index 0000000000..86c876898e --- /dev/null +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerHeaderView.swift @@ -0,0 +1,36 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct EmojiPickerHeaderView: View { + let title: String + + var body: some View { + HStack { + Text(title) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +struct EmojiPickerHeaderView_Previews: PreviewProvider { + static var previews: some View { + Group { + EmojiPickerHeaderView(title: "") + } + } +} diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift new file mode 100644 index 0000000000..ec2cd9a7a4 --- /dev/null +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift @@ -0,0 +1,59 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct EmojiPickerScreen: View { + @ObservedObject var context: EmojiPickerScreenViewModel.Context + @State var searchString = "" + + var body: some View { + VStack { + Text(ElementL10n.reactions) + .padding(.top, 20) + EmojiPickerSearchFieldView(searchString: $searchString) + .padding(.horizontal, 10) + ScrollView { + LazyVGrid(columns: [GridItem(.adaptive(minimum: 45))], spacing: 3) { + ForEach(context.viewState.categories) { category in + Section(header: EmojiPickerHeaderView(title: category.name) + .padding(.horizontal, 13) + .padding(.top, 10)) { + ForEach(category.emojis) { emoji in + Text(emoji.value) + .frame(width: 45, height: 45) + .onTapGesture { + context.send(viewAction: .emojiSelected(emoji: emoji)) + } + } + } + } + } + } + } + .onChange(of: searchString) { _ in + context.send(viewAction: .search(searchString: searchString)) + } + } +} + +// MARK: - Previews + +struct EmojiPickerScreen_Previews: PreviewProvider { + static var previews: some View { + EmojiPickerScreen(context: EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()).context) + } +} diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerSearchFieldView.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerSearchFieldView.swift new file mode 100644 index 0000000000..6cb7edc4a9 --- /dev/null +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerSearchFieldView.swift @@ -0,0 +1,45 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct EmojiPickerSearchFieldView: View { + @Binding var searchString: String + @FocusState private var isSearchFocused: Bool + + var body: some View { + HStack { + Image(systemName: "magnifyingglass") + TextField(ElementL10n.search, text: $searchString) + .focused($isSearchFocused) + if isSearchFocused { + Spacer() + Button { + searchString = "" + isSearchFocused = false + } label: { + Text(ElementL10n.actionCancel) + } + } + } + } +} + +struct EmojiPickerSearchFieldView_Previews: PreviewProvider { + static var previews: some View { + EmojiPickerSearchFieldView(searchString: .constant("")) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 831281155f..645117624f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -22,6 +22,7 @@ struct RoomScreenCoordinatorParameters { let mediaProvider: MediaProviderProtocol let roomName: String? let roomAvatarUrl: String? + let emojiProvide: EmojiProviderProtocol } final class RoomScreenCoordinator: CoordinatorProtocol { @@ -57,6 +58,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { self.displayVideo(for: videoURL) case .displayFile(let fileURL, let title): self.displayFile(for: fileURL, with: title) + case .displayEmojiPicker(let itemId): + self.displayEmojiPickerScreen(for: itemId) } } } @@ -108,4 +111,22 @@ final class RoomScreenCoordinator: CoordinatorProtocol { navigationController.push(coordinator) } + + private func displayEmojiPickerScreen(for itemId: String) { + guard let emojiProvider = parameters?.emojiProvide else { + fatalError() + } + let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider, + itemId: itemId) + let coordinator = EmojiPickerScreenCoordinator(parameters: params) + coordinator.callback = { [weak self] action in + switch action { + case let .selectEmoji(emojiId: emojiId, itemId: itemId): + self?.navigationController.dismissSheet() + MXLog.debug("Save \(emojiId) for \(itemId)") + } + } + + navigationController.presentSheet(coordinator) + } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 27510235c5..cd3868adb6 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -20,6 +20,7 @@ import UIKit enum RoomScreenViewModelAction { case displayVideo(videoURL: URL) case displayFile(fileURL: URL, title: String?) + case displayEmojiPicker(itemId: String) } enum RoomScreenComposerMode: Equatable { @@ -29,6 +30,7 @@ enum RoomScreenComposerMode: Equatable { } enum RoomScreenViewAction { + case displayEmojiPicker(itemId: String) case paginateBackwards case itemAppeared(id: String) case itemDisappeared(id: String) @@ -38,6 +40,7 @@ enum RoomScreenViewAction { case sendReaction(key: String, eventID: String) case cancelReply case cancelEdit + case displayReactionsMenuForItemId(itemId: String) } struct RoomScreenViewState: BindableState { @@ -48,6 +51,7 @@ struct RoomScreenViewState: BindableState { var isBackPaginating = false var showLoading = false var bindings: RoomScreenViewStateBindings + var displayReactionsMenuForItemId = "" var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)? diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index b66af880e1..aa5379fe9d 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -91,6 +91,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol var callback: ((RoomScreenViewModelAction) -> Void)? + // swiftlint:disable:next cyclomatic_complexity override func process(viewAction: RoomScreenViewAction) async { switch viewAction { case .paginateBackwards: @@ -108,6 +109,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case .sendReaction(let key, _): #warning("Reaction implementation awaiting SDK support.") MXLog.warning("React with \(key) failed. Not implemented.") + case .displayEmojiPicker(let itemId): + callback?(.displayEmojiPicker(itemId: itemId)) + case .displayReactionsMenuForItemId(let itemId): + state.displayReactionsMenuForItemId = itemId case .cancelReply: state.composerMode = .default case .cancelEdit: diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 9aa13bf945..fc4fe05b99 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -19,6 +19,7 @@ import SwiftUI struct RoomScreen: View { @ObservedObject private var settings = ElementSettings.shared @ObservedObject var context: RoomScreenViewModel.Context + @State private var showReactionsMenuForItemId = "" var body: some View { timeline diff --git a/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemReactionsMenuView.swift b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemReactionsMenuView.swift new file mode 100644 index 0000000000..f9e9b0db81 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/Supplementary/TimelineItemReactionsMenuView.swift @@ -0,0 +1,51 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct TimelineItemReactionsMenuView: View { + private let emojis = ["๐Ÿ‘๐Ÿผ", "๐Ÿ‘Ž๐Ÿผ", "๐Ÿ˜„", "๐Ÿ™๐Ÿผ", "๐Ÿ˜‡"] + + var onDisplayEmojiPicker: (() -> Void)? + + var body: some View { + HStack { + HStack(spacing: 10) { + ForEach(emojis, id: \.self) { emoji in + Text(emoji) + } + } + .padding(10) + .background(.gray) + .cornerRadius(15) + HStack(spacing: 10) { + Text("โž•") + } + .padding(10) + .background(.gray) + .cornerRadius(15) + .onTapGesture { + onDisplayEmojiPicker?() + } + } + } +} + +struct TimelineItemReactionsMenuView_Previews: PreviewProvider { + static var previews: some View { + TimelineItemReactionsMenuView(onDisplayEmojiPicker: nil) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableView.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableView.swift index edb35a8b89..15efaf0503 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineTableView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineTableView.swift @@ -102,6 +102,12 @@ struct TimelineTableView: UIViewRepresentable { } } + var displayReactionsMenuForItemId = "" { + didSet { + tableView?.reloadData() + } + } + /// The table's diffable data source. private var dataSource: UITableViewDiffableDataSource? private var cancellables: Set = [] @@ -175,25 +181,35 @@ struct TimelineTableView: UIViewRepresentable { cell.item = timelineItem cell.contentConfiguration = UIHostingConfiguration { - timelineItem - .frame(maxWidth: .infinity, alignment: .leading) - .opacity(viewModelContext.viewState.opacity(for: timelineItem)) - .contextMenu { - viewModelContext.viewState.contextMenuBuilder?(timelineItem.id) - } - .onAppear { - viewModelContext.send(viewAction: .itemAppeared(id: timelineItem.id)) - } - .onDisappear { - viewModelContext.send(viewAction: .itemDisappeared(id: timelineItem.id)) - } - .environment(\.openURL, OpenURLAction { url in - viewModelContext.send(viewAction: .linkClicked(url: url)) - return .systemAction - }) - .onTapGesture { - viewModelContext.send(viewAction: .itemTapped(id: timelineItem.id)) + VStack { + if viewModelContext.viewState.displayReactionsMenuForItemId == timelineItem.id { + TimelineItemReactionsMenuView { + viewModelContext.send(viewAction: .displayEmojiPicker(itemId: timelineItem.id)) + } } + timelineItem + .frame(maxWidth: .infinity, alignment: .leading) + .opacity(viewModelContext.viewState.opacity(for: timelineItem)) + .contextMenu { + viewModelContext.viewState.contextMenuBuilder?(timelineItem.id) + } + .onAppear { + viewModelContext.send(viewAction: .itemAppeared(id: timelineItem.id)) + } + .onDisappear { + viewModelContext.send(viewAction: .itemDisappeared(id: timelineItem.id)) + } + .environment(\.openURL, OpenURLAction { url in + viewModelContext.send(viewAction: .linkClicked(url: url)) + return .systemAction + }) + .onTapGesture(count: 2) { + viewModelContext.send(viewAction: .displayReactionsMenuForItemId(itemId: timelineItem.id)) + } + .onTapGesture { + viewModelContext.send(viewAction: .itemTapped(id: timelineItem.id)) + } + } } .margins(.all, self.timelineStyle.rowInsets) .minSize(height: 1) @@ -240,6 +256,9 @@ struct TimelineTableView: UIViewRepresentable { if composerMode != viewModelContext.viewState.composerMode { composerMode = viewModelContext.viewState.composerMode } + if displayReactionsMenuForItemId != viewModelContext.viewState.displayReactionsMenuForItemId { + displayReactionsMenuForItemId = viewModelContext.viewState.displayReactionsMenuForItemId + } } /// Updates the table view with the latest items from the ``timelineItems`` array. After diff --git a/ElementX/Sources/Services/Emojis/EmojiCategory.swift b/ElementX/Sources/Services/Emojis/EmojiCategory.swift new file mode 100644 index 0000000000..fe74184ae6 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiCategory.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +struct EmojiCategory: Equatable, Identifiable { + let id: String + let emojis: [EmojiItem] +} diff --git a/ElementX/Sources/Services/Emojis/EmojiItem.swift b/ElementX/Sources/Services/Emojis/EmojiItem.swift new file mode 100644 index 0000000000..6b8abee133 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiItem.swift @@ -0,0 +1,35 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct EmojiItem: Equatable, Identifiable { + var id: String + let name: String + let keywords: [String] + let skins: [EmojiItemSkin] +} + +extension EmojiItem { + init?(from emojiMart: EmojiMartEmoji) { + id = emojiMart.id + name = emojiMart.name + keywords = emojiMart.keywords + skins = emojiMart.skins.compactMap { emojiMartEmojiSkin in + EmojiItemSkin(from: emojiMartEmojiSkin) + } + } +} diff --git a/ElementX/Sources/Services/Emojis/EmojiItemSkin.swift b/ElementX/Sources/Services/Emojis/EmojiItemSkin.swift new file mode 100644 index 0000000000..2aedd83c32 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiItemSkin.swift @@ -0,0 +1,36 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct EmojiItemSkin: Equatable { + let value: String + + init?(from emojiMartEmojiSkin: EmojiMartEmojiSkin) { + let unicodeStringComponents = emojiMartEmojiSkin.unified.components(separatedBy: "-") + + var emoji = "" + + for unicodeStringComponent in unicodeStringComponents { + guard let unicodeCodePoint = Int(unicodeStringComponent, radix: 16), + let emojiUnicodeScalar = UnicodeScalar(unicodeCodePoint) else { + return nil + } + emoji.append(String(emojiUnicodeScalar)) + } + value = emoji + } +} diff --git a/ElementX/Sources/Services/Emojis/EmojiLoaderProtocol.swift b/ElementX/Sources/Services/Emojis/EmojiLoaderProtocol.swift new file mode 100644 index 0000000000..9df9c3eb91 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiLoaderProtocol.swift @@ -0,0 +1,21 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol EmojiLoaderProtocol { + func load() async -> [EmojiCategory] +} diff --git a/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartCategory.swift b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartCategory.swift new file mode 100644 index 0000000000..87dbb767b0 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartCategory.swift @@ -0,0 +1,22 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct EmojiMartCategory: Decodable { + let id: String + let emojis: [String] +} diff --git a/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartEmoji.swift b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartEmoji.swift new file mode 100644 index 0000000000..a08e5d2281 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartEmoji.swift @@ -0,0 +1,29 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct EmojiMartEmoji: Decodable { + let id: String + let name: String + let keywords: [String] + let skins: [EmojiMartEmojiSkin] +} + +struct EmojiMartEmojiSkin: Decodable { + let unified: String + let native: String +} diff --git a/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartJSONLoader.swift b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartJSONLoader.swift new file mode 100644 index 0000000000..15eb1bfa8e --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartJSONLoader.swift @@ -0,0 +1,60 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +private enum EmojiMartJSONLoaderError: Error { + case fileNotFound +} + +class EmojiMartJSONLoader: EmojiLoaderProtocol { + /// Emoji data coming from https://github.com/missive/emoji-mart/blob/main/packages/emoji-mart-data/sets/14/apple.json + private let jsonFilename = "apple_emojis_data" + + func load() async -> [EmojiCategory] { + do { + let data = try await loadJSONData() + let store = try await decodeJSONData(data: data) + return emojiCategories(from: store) + } catch { + MXLog.error("Couldn't parse emoji json") + return [] + } + } + + private func loadJSONData() async throws -> Data { + guard let jsonDataURL = Bundle.main.url(forResource: jsonFilename, withExtension: "json") else { + throw EmojiMartJSONLoaderError.fileNotFound + } + return try Data(contentsOf: jsonDataURL) + } + + private func decodeJSONData(data: Data) async throws -> EmojiMartStore { + try JSONDecoder().decode(EmojiMartStore.self, from: data) + } + + private func emojiCategories(from emojiMartStore: EmojiMartStore) -> [EmojiCategory] { + emojiMartStore.categories.map { emojiMartCategory -> EmojiCategory in + let emojiItems = emojiMartCategory.emojis.compactMap { emoji -> EmojiItem? in + guard let emojiMartEmoji = emojiMartStore.emojis.first(where: { $0.id == emoji }) else { + return nil + } + return EmojiItem(from: emojiMartEmoji) + } + return EmojiCategory(id: emojiMartCategory.id, emojis: emojiItems) + } + } +} diff --git a/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartStore.swift b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartStore.swift new file mode 100644 index 0000000000..39639ee82e --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiMart/EmojiMartStore.swift @@ -0,0 +1,35 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct EmojiMartStore: Decodable { + let categories: [EmojiMartCategory] + let emojis: [EmojiMartEmoji] + + enum CodingKeys: CodingKey { + case categories + case emojis + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + categories = try container.decode([EmojiMartCategory].self, forKey: .categories) + + let emojisDictionary = try container.decode([String: EmojiMartEmoji].self, forKey: .emojis) + emojis = emojisDictionary.map(\.value) + } +} diff --git a/ElementX/Sources/Services/Emojis/EmojiMart/apple_emojis_data.json b/ElementX/Sources/Services/Emojis/EmojiMart/apple_emojis_data.json new file mode 100644 index 0000000000..403fbc9a74 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiMart/apple_emojis_data.json @@ -0,0 +1,29850 @@ +{ + "categories": [{ + "id": "people", + "emojis": ["grinning", "smiley", "smile", "grin", "laughing", "sweat_smile", "rolling_on_the_floor_laughing", "joy", "slightly_smiling_face", "upside_down_face", "melting_face", "wink", "blush", "innocent", "smiling_face_with_3_hearts", "heart_eyes", "star-struck", "kissing_heart", "kissing", "relaxed", "kissing_closed_eyes", "kissing_smiling_eyes", "smiling_face_with_tear", "yum", "stuck_out_tongue", "stuck_out_tongue_winking_eye", "zany_face", "stuck_out_tongue_closed_eyes", "money_mouth_face", "hugging_face", "face_with_hand_over_mouth", "face_with_open_eyes_and_hand_over_mouth", "face_with_peeking_eye", "shushing_face", "thinking_face", "saluting_face", "zipper_mouth_face", "face_with_raised_eyebrow", "neutral_face", "expressionless", "no_mouth", "dotted_line_face", "face_in_clouds", "smirk", "unamused", "face_with_rolling_eyes", "grimacing", "face_exhaling", "lying_face", "relieved", "pensive", "sleepy", "drooling_face", "sleeping", "mask", "face_with_thermometer", "face_with_head_bandage", "nauseated_face", "face_vomiting", "sneezing_face", "hot_face", "cold_face", "woozy_face", "dizzy_face", "face_with_spiral_eyes", "exploding_head", "face_with_cowboy_hat", "partying_face", "disguised_face", "sunglasses", "nerd_face", "face_with_monocle", "confused", "face_with_diagonal_mouth", "worried", "slightly_frowning_face", "white_frowning_face", "open_mouth", "hushed", "astonished", "flushed", "pleading_face", "face_holding_back_tears", "frowning", "anguished", "fearful", "cold_sweat", "disappointed_relieved", "cry", "sob", "scream", "confounded", "persevere", "disappointed", "sweat", "weary", "tired_face", "yawning_face", "triumph", "rage", "angry", "face_with_symbols_on_mouth", "smiling_imp", "imp", "skull", "skull_and_crossbones", "hankey", "clown_face", "japanese_ogre", "japanese_goblin", "ghost", "alien", "space_invader", "robot_face", "wave", "raised_back_of_hand", "raised_hand_with_fingers_splayed", "hand", "spock-hand", "rightwards_hand", "leftwards_hand", "palm_down_hand", "palm_up_hand", "ok_hand", "pinched_fingers", "pinching_hand", "v", "crossed_fingers", "hand_with_index_finger_and_thumb_crossed", "i_love_you_hand_sign", "the_horns", "call_me_hand", "point_left", "point_right", "point_up_2", "middle_finger", "point_down", "point_up", "index_pointing_at_the_viewer", "+1", "-1", "fist", "facepunch", "left-facing_fist", "right-facing_fist", "clap", "raised_hands", "heart_hands", "open_hands", "palms_up_together", "handshake", "pray", "writing_hand", "nail_care", "selfie", "muscle", "mechanical_arm", "mechanical_leg", "leg", "foot", "ear", "ear_with_hearing_aid", "nose", "brain", "anatomical_heart", "lungs", "tooth", "bone", "eyes", "eye", "tongue", "lips", "biting_lip", "baby", "child", "boy", "girl", "adult", "person_with_blond_hair", "man", "bearded_person", "man_with_beard", "woman_with_beard", "red_haired_man", "curly_haired_man", "white_haired_man", "bald_man", "woman", "red_haired_woman", "red_haired_person", "curly_haired_woman", "curly_haired_person", "white_haired_woman", "white_haired_person", "bald_woman", "bald_person", "blond-haired-woman", "blond-haired-man", "older_adult", "older_man", "older_woman", "person_frowning", "man-frowning", "woman-frowning", "person_with_pouting_face", "man-pouting", "woman-pouting", "no_good", "man-gesturing-no", "woman-gesturing-no", "ok_woman", "man-gesturing-ok", "woman-gesturing-ok", "information_desk_person", "man-tipping-hand", "woman-tipping-hand", "raising_hand", "man-raising-hand", "woman-raising-hand", "deaf_person", "deaf_man", "deaf_woman", "bow", "man-bowing", "woman-bowing", "face_palm", "man-facepalming", "woman-facepalming", "shrug", "man-shrugging", "woman-shrugging", "health_worker", "male-doctor", "female-doctor", "student", "male-student", "female-student", "teacher", "male-teacher", "female-teacher", "judge", "male-judge", "female-judge", "farmer", "male-farmer", "female-farmer", "cook", "male-cook", "female-cook", "mechanic", "male-mechanic", "female-mechanic", "factory_worker", "male-factory-worker", "female-factory-worker", "office_worker", "male-office-worker", "female-office-worker", "scientist", "male-scientist", "female-scientist", "technologist", "male-technologist", "female-technologist", "singer", "male-singer", "female-singer", "artist", "male-artist", "female-artist", "pilot", "male-pilot", "female-pilot", "astronaut", "male-astronaut", "female-astronaut", "firefighter", "male-firefighter", "female-firefighter", "cop", "male-police-officer", "female-police-officer", "sleuth_or_spy", "male-detective", "female-detective", "guardsman", "male-guard", "female-guard", "ninja", "construction_worker", "male-construction-worker", "female-construction-worker", "person_with_crown", "prince", "princess", "man_with_turban", "man-wearing-turban", "woman-wearing-turban", "man_with_gua_pi_mao", "person_with_headscarf", "person_in_tuxedo", "man_in_tuxedo", "woman_in_tuxedo", "bride_with_veil", "man_with_veil", "woman_with_veil", "pregnant_woman", "pregnant_man", "pregnant_person", "breast-feeding", "woman_feeding_baby", "man_feeding_baby", "person_feeding_baby", "angel", "santa", "mrs_claus", "mx_claus", "superhero", "male_superhero", "female_superhero", "supervillain", "male_supervillain", "female_supervillain", "mage", "male_mage", "female_mage", "fairy", "male_fairy", "female_fairy", "vampire", "male_vampire", "female_vampire", "merperson", "merman", "mermaid", "elf", "male_elf", "female_elf", "genie", "male_genie", "female_genie", "zombie", "male_zombie", "female_zombie", "troll", "massage", "man-getting-massage", "woman-getting-massage", "haircut", "man-getting-haircut", "woman-getting-haircut", "walking", "man-walking", "woman-walking", "standing_person", "man_standing", "woman_standing", "kneeling_person", "man_kneeling", "woman_kneeling", "person_with_probing_cane", "man_with_probing_cane", "woman_with_probing_cane", "person_in_motorized_wheelchair", "man_in_motorized_wheelchair", "woman_in_motorized_wheelchair", "person_in_manual_wheelchair", "man_in_manual_wheelchair", "woman_in_manual_wheelchair", "runner", "man-running", "woman-running", "dancer", "man_dancing", "man_in_business_suit_levitating", "dancers", "men-with-bunny-ears-partying", "women-with-bunny-ears-partying", "person_in_steamy_room", "man_in_steamy_room", "woman_in_steamy_room", "person_climbing", "man_climbing", "woman_climbing", "fencer", "horse_racing", "skier", "snowboarder", "golfer", "man-golfing", "woman-golfing", "surfer", "man-surfing", "woman-surfing", "rowboat", "man-rowing-boat", "woman-rowing-boat", "swimmer", "man-swimming", "woman-swimming", "person_with_ball", "man-bouncing-ball", "woman-bouncing-ball", "weight_lifter", "man-lifting-weights", "woman-lifting-weights", "bicyclist", "man-biking", "woman-biking", "mountain_bicyclist", "man-mountain-biking", "woman-mountain-biking", "person_doing_cartwheel", "man-cartwheeling", "woman-cartwheeling", "wrestlers", "man-wrestling", "woman-wrestling", "water_polo", "man-playing-water-polo", "woman-playing-water-polo", "handball", "man-playing-handball", "woman-playing-handball", "juggling", "man-juggling", "woman-juggling", "person_in_lotus_position", "man_in_lotus_position", "woman_in_lotus_position", "bath", "sleeping_accommodation", "people_holding_hands", "two_women_holding_hands", "man_and_woman_holding_hands", "two_men_holding_hands", "couplekiss", "woman-kiss-man", "man-kiss-man", "woman-kiss-woman", "couple_with_heart", "woman-heart-man", "man-heart-man", "woman-heart-woman", "family", "man-woman-boy", "man-woman-girl", "man-woman-girl-boy", "man-woman-boy-boy", "man-woman-girl-girl", "man-man-boy", "man-man-girl", "man-man-girl-boy", "man-man-boy-boy", "man-man-girl-girl", "woman-woman-boy", "woman-woman-girl", "woman-woman-girl-boy", "woman-woman-boy-boy", "woman-woman-girl-girl", "man-boy", "man-boy-boy", "man-girl", "man-girl-boy", "man-girl-girl", "woman-boy", "woman-boy-boy", "woman-girl", "woman-girl-boy", "woman-girl-girl", "speaking_head_in_silhouette", "bust_in_silhouette", "busts_in_silhouette", "people_hugging", "footprints", "smiley_cat", "smile_cat", "joy_cat", "heart_eyes_cat", "smirk_cat", "kissing_cat", "scream_cat", "crying_cat_face", "pouting_cat", "see_no_evil", "hear_no_evil", "speak_no_evil", "kiss", "love_letter", "cupid", "gift_heart", "sparkling_heart", "heartpulse", "heartbeat", "revolving_hearts", "two_hearts", "heart_decoration", "heavy_heart_exclamation_mark_ornament", "broken_heart", "heart_on_fire", "mending_heart", "heart", "orange_heart", "yellow_heart", "green_heart", "blue_heart", "purple_heart", "brown_heart", "black_heart", "white_heart", "100", "anger", "boom", "dizzy", "sweat_drops", "dash", "hole", "bomb", "speech_balloon", "eye-in-speech-bubble", "left_speech_bubble", "right_anger_bubble", "thought_balloon", "zzz"] + }, { + "id": "nature", + "emojis": ["monkey_face", "monkey", "gorilla", "orangutan", "dog", "dog2", "guide_dog", "service_dog", "poodle", "wolf", "fox_face", "raccoon", "cat", "cat2", "black_cat", "lion_face", "tiger", "tiger2", "leopard", "horse", "racehorse", "unicorn_face", "zebra_face", "deer", "bison", "cow", "ox", "water_buffalo", "cow2", "pig", "pig2", "boar", "pig_nose", "ram", "sheep", "goat", "dromedary_camel", "camel", "llama", "giraffe_face", "elephant", "mammoth", "rhinoceros", "hippopotamus", "mouse", "mouse2", "rat", "hamster", "rabbit", "rabbit2", "chipmunk", "beaver", "hedgehog", "bat", "bear", "polar_bear", "koala", "panda_face", "sloth", "otter", "skunk", "kangaroo", "badger", "feet", "turkey", "chicken", "rooster", "hatching_chick", "baby_chick", "hatched_chick", "bird", "penguin", "dove_of_peace", "eagle", "duck", "swan", "owl", "dodo", "feather", "flamingo", "peacock", "parrot", "frog", "crocodile", "turtle", "lizard", "snake", "dragon_face", "dragon", "sauropod", "t-rex", "whale", "whale2", "dolphin", "seal", "fish", "tropical_fish", "blowfish", "shark", "octopus", "shell", "coral", "snail", "butterfly", "bug", "ant", "bee", "beetle", "ladybug", "cricket", "cockroach", "spider", "spider_web", "scorpion", "mosquito", "fly", "worm", "microbe", "bouquet", "cherry_blossom", "white_flower", "lotus", "rosette", "rose", "wilted_flower", "hibiscus", "sunflower", "blossom", "tulip", "seedling", "potted_plant", "evergreen_tree", "deciduous_tree", "palm_tree", "cactus", "ear_of_rice", "herb", "shamrock", "four_leaf_clover", "maple_leaf", "fallen_leaf", "leaves", "empty_nest", "nest_with_eggs"] + }, { + "id": "foods", + "emojis": ["grapes", "melon", "watermelon", "tangerine", "lemon", "banana", "pineapple", "mango", "apple", "green_apple", "pear", "peach", "cherries", "strawberry", "blueberries", "kiwifruit", "tomato", "olive", "coconut", "avocado", "eggplant", "potato", "carrot", "corn", "hot_pepper", "bell_pepper", "cucumber", "leafy_green", "broccoli", "garlic", "onion", "mushroom", "peanuts", "beans", "chestnut", "bread", "croissant", "baguette_bread", "flatbread", "pretzel", "bagel", "pancakes", "waffle", "cheese_wedge", "meat_on_bone", "poultry_leg", "cut_of_meat", "bacon", "hamburger", "fries", "pizza", "hotdog", "sandwich", "taco", "burrito", "tamale", "stuffed_flatbread", "falafel", "egg", "fried_egg", "shallow_pan_of_food", "stew", "fondue", "bowl_with_spoon", "green_salad", "popcorn", "butter", "salt", "canned_food", "bento", "rice_cracker", "rice_ball", "rice", "curry", "ramen", "spaghetti", "sweet_potato", "oden", "sushi", "fried_shrimp", "fish_cake", "moon_cake", "dango", "dumpling", "fortune_cookie", "takeout_box", "crab", "lobster", "shrimp", "squid", "oyster", "icecream", "shaved_ice", "ice_cream", "doughnut", "cookie", "birthday", "cake", "cupcake", "pie", "chocolate_bar", "candy", "lollipop", "custard", "honey_pot", "baby_bottle", "glass_of_milk", "coffee", "teapot", "tea", "sake", "champagne", "wine_glass", "cocktail", "tropical_drink", "beer", "beers", "clinking_glasses", "tumbler_glass", "pouring_liquid", "cup_with_straw", "bubble_tea", "beverage_box", "mate_drink", "ice_cube", "chopsticks", "knife_fork_plate", "fork_and_knife", "spoon", "hocho", "jar", "amphora"] + }, { + "id": "activity", + "emojis": ["jack_o_lantern", "christmas_tree", "fireworks", "sparkler", "firecracker", "sparkles", "balloon", "tada", "confetti_ball", "tanabata_tree", "bamboo", "dolls", "flags", "wind_chime", "rice_scene", "red_envelope", "ribbon", "gift", "reminder_ribbon", "admission_tickets", "ticket", "medal", "trophy", "sports_medal", "first_place_medal", "second_place_medal", "third_place_medal", "soccer", "baseball", "softball", "basketball", "volleyball", "football", "rugby_football", "tennis", "flying_disc", "bowling", "cricket_bat_and_ball", "field_hockey_stick_and_ball", "ice_hockey_stick_and_puck", "lacrosse", "table_tennis_paddle_and_ball", "badminton_racquet_and_shuttlecock", "boxing_glove", "martial_arts_uniform", "goal_net", "golf", "ice_skate", "fishing_pole_and_fish", "diving_mask", "running_shirt_with_sash", "ski", "sled", "curling_stone", "dart", "yo-yo", "kite", "8ball", "crystal_ball", "magic_wand", "nazar_amulet", "hamsa", "video_game", "joystick", "slot_machine", "game_die", "jigsaw", "teddy_bear", "pinata", "mirror_ball", "nesting_dolls", "spades", "hearts", "diamonds", "clubs", "chess_pawn", "black_joker", "mahjong", "flower_playing_cards", "performing_arts", "frame_with_picture", "art", "thread", "sewing_needle", "yarn", "knot"] + }, { + "id": "places", + "emojis": ["earth_africa", "earth_americas", "earth_asia", "globe_with_meridians", "world_map", "japan", "compass", "snow_capped_mountain", "mountain", "volcano", "mount_fuji", "camping", "beach_with_umbrella", "desert", "desert_island", "national_park", "stadium", "classical_building", "building_construction", "bricks", "rock", "wood", "hut", "house_buildings", "derelict_house_building", "house", "house_with_garden", "office", "post_office", "european_post_office", "hospital", "bank", "hotel", "love_hotel", "convenience_store", "school", "department_store", "factory", "japanese_castle", "european_castle", "wedding", "tokyo_tower", "statue_of_liberty", "church", "mosque", "hindu_temple", "synagogue", "shinto_shrine", "kaaba", "fountain", "tent", "foggy", "night_with_stars", "cityscape", "sunrise_over_mountains", "sunrise", "city_sunset", "city_sunrise", "bridge_at_night", "hotsprings", "carousel_horse", "playground_slide", "ferris_wheel", "roller_coaster", "barber", "circus_tent", "steam_locomotive", "railway_car", "bullettrain_side", "bullettrain_front", "train2", "metro", "light_rail", "station", "tram", "monorail", "mountain_railway", "train", "bus", "oncoming_bus", "trolleybus", "minibus", "ambulance", "fire_engine", "police_car", "oncoming_police_car", "taxi", "oncoming_taxi", "car", "oncoming_automobile", "blue_car", "pickup_truck", "truck", "articulated_lorry", "tractor", "racing_car", "racing_motorcycle", "motor_scooter", "manual_wheelchair", "motorized_wheelchair", "auto_rickshaw", "bike", "scooter", "skateboard", "roller_skate", "busstop", "motorway", "railway_track", "oil_drum", "fuelpump", "wheel", "rotating_light", "traffic_light", "vertical_traffic_light", "octagonal_sign", "construction", "anchor", "ring_buoy", "boat", "canoe", "speedboat", "passenger_ship", "ferry", "motor_boat", "ship", "airplane", "small_airplane", "airplane_departure", "airplane_arriving", "parachute", "seat", "helicopter", "suspension_railway", "mountain_cableway", "aerial_tramway", "satellite", "rocket", "flying_saucer", "bellhop_bell", "luggage", "hourglass", "hourglass_flowing_sand", "watch", "alarm_clock", "stopwatch", "timer_clock", "mantelpiece_clock", "clock12", "clock1230", "clock1", "clock130", "clock2", "clock230", "clock3", "clock330", "clock4", "clock430", "clock5", "clock530", "clock6", "clock630", "clock7", "clock730", "clock8", "clock830", "clock9", "clock930", "clock10", "clock1030", "clock11", "clock1130", "new_moon", "waxing_crescent_moon", "first_quarter_moon", "moon", "full_moon", "waning_gibbous_moon", "last_quarter_moon", "waning_crescent_moon", "crescent_moon", "new_moon_with_face", "first_quarter_moon_with_face", "last_quarter_moon_with_face", "thermometer", "sunny", "full_moon_with_face", "sun_with_face", "ringed_planet", "star", "star2", "stars", "milky_way", "cloud", "partly_sunny", "thunder_cloud_and_rain", "mostly_sunny", "barely_sunny", "partly_sunny_rain", "rain_cloud", "snow_cloud", "lightning", "tornado", "fog", "wind_blowing_face", "cyclone", "rainbow", "closed_umbrella", "umbrella", "umbrella_with_rain_drops", "umbrella_on_ground", "zap", "snowflake", "snowman", "snowman_without_snow", "comet", "fire", "droplet", "ocean"] + }, { + "id": "objects", + "emojis": ["eyeglasses", "dark_sunglasses", "goggles", "lab_coat", "safety_vest", "necktie", "shirt", "jeans", "scarf", "gloves", "coat", "socks", "dress", "kimono", "sari", "one-piece_swimsuit", "briefs", "shorts", "bikini", "womans_clothes", "purse", "handbag", "pouch", "shopping_bags", "school_satchel", "thong_sandal", "mans_shoe", "athletic_shoe", "hiking_boot", "womans_flat_shoe", "high_heel", "sandal", "ballet_shoes", "boot", "crown", "womans_hat", "tophat", "mortar_board", "billed_cap", "military_helmet", "helmet_with_white_cross", "prayer_beads", "lipstick", "ring", "gem", "mute", "speaker", "sound", "loud_sound", "loudspeaker", "mega", "postal_horn", "bell", "no_bell", "musical_score", "musical_note", "notes", "studio_microphone", "level_slider", "control_knobs", "microphone", "headphones", "radio", "saxophone", "accordion", "guitar", "musical_keyboard", "trumpet", "violin", "banjo", "drum_with_drumsticks", "long_drum", "iphone", "calling", "phone", "telephone_receiver", "pager", "fax", "battery", "low_battery", "electric_plug", "computer", "desktop_computer", "printer", "keyboard", "three_button_mouse", "trackball", "minidisc", "floppy_disk", "cd", "dvd", "abacus", "movie_camera", "film_frames", "film_projector", "clapper", "tv", "camera", "camera_with_flash", "video_camera", "vhs", "mag", "mag_right", "candle", "bulb", "flashlight", "izakaya_lantern", "diya_lamp", "notebook_with_decorative_cover", "closed_book", "book", "green_book", "blue_book", "orange_book", "books", "notebook", "ledger", "page_with_curl", "scroll", "page_facing_up", "newspaper", "rolled_up_newspaper", "bookmark_tabs", "bookmark", "label", "moneybag", "coin", "yen", "dollar", "euro", "pound", "money_with_wings", "credit_card", "receipt", "chart", "email", "e-mail", "incoming_envelope", "envelope_with_arrow", "outbox_tray", "inbox_tray", "package", "mailbox", "mailbox_closed", "mailbox_with_mail", "mailbox_with_no_mail", "postbox", "ballot_box_with_ballot", "pencil2", "black_nib", "lower_left_fountain_pen", "lower_left_ballpoint_pen", "lower_left_paintbrush", "lower_left_crayon", "memo", "briefcase", "file_folder", "open_file_folder", "card_index_dividers", "date", "calendar", "spiral_note_pad", "spiral_calendar_pad", "card_index", "chart_with_upwards_trend", "chart_with_downwards_trend", "bar_chart", "clipboard", "pushpin", "round_pushpin", "paperclip", "linked_paperclips", "straight_ruler", "triangular_ruler", "scissors", "card_file_box", "file_cabinet", "wastebasket", "lock", "unlock", "lock_with_ink_pen", "closed_lock_with_key", "key", "old_key", "hammer", "axe", "pick", "hammer_and_pick", "hammer_and_wrench", "dagger_knife", "crossed_swords", "gun", "boomerang", "bow_and_arrow", "shield", "carpentry_saw", "wrench", "screwdriver", "nut_and_bolt", "gear", "compression", "scales", "probing_cane", "link", "chains", "hook", "toolbox", "magnet", "ladder", "alembic", "test_tube", "petri_dish", "dna", "microscope", "telescope", "satellite_antenna", "syringe", "drop_of_blood", "pill", "adhesive_bandage", "crutch", "stethoscope", "x-ray", "door", "elevator", "mirror", "window", "bed", "couch_and_lamp", "chair", "toilet", "plunger", "shower", "bathtub", "mouse_trap", "razor", "lotion_bottle", "safety_pin", "broom", "basket", "roll_of_paper", "bucket", "soap", "bubbles", "toothbrush", "sponge", "fire_extinguisher", "shopping_trolley", "smoking", "coffin", "headstone", "funeral_urn", "moyai", "placard", "identification_card"] + }, { + "id": "symbols", + "emojis": ["atm", "put_litter_in_its_place", "potable_water", "wheelchair", "mens", "womens", "restroom", "baby_symbol", "wc", "passport_control", "customs", "baggage_claim", "left_luggage", "warning", "children_crossing", "no_entry", "no_entry_sign", "no_bicycles", "no_smoking", "do_not_litter", "non-potable_water", "no_pedestrians", "no_mobile_phones", "underage", "radioactive_sign", "biohazard_sign", "arrow_up", "arrow_upper_right", "arrow_right", "arrow_lower_right", "arrow_down", "arrow_lower_left", "arrow_left", "arrow_upper_left", "arrow_up_down", "left_right_arrow", "leftwards_arrow_with_hook", "arrow_right_hook", "arrow_heading_up", "arrow_heading_down", "arrows_clockwise", "arrows_counterclockwise", "back", "end", "on", "soon", "top", "place_of_worship", "atom_symbol", "om_symbol", "star_of_david", "wheel_of_dharma", "yin_yang", "latin_cross", "orthodox_cross", "star_and_crescent", "peace_symbol", "menorah_with_nine_branches", "six_pointed_star", "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpius", "sagittarius", "capricorn", "aquarius", "pisces", "ophiuchus", "twisted_rightwards_arrows", "repeat", "repeat_one", "arrow_forward", "fast_forward", "black_right_pointing_double_triangle_with_vertical_bar", "black_right_pointing_triangle_with_double_vertical_bar", "arrow_backward", "rewind", "black_left_pointing_double_triangle_with_vertical_bar", "arrow_up_small", "arrow_double_up", "arrow_down_small", "arrow_double_down", "double_vertical_bar", "black_square_for_stop", "black_circle_for_record", "eject", "cinema", "low_brightness", "high_brightness", "signal_strength", "vibration_mode", "mobile_phone_off", "transgender_symbol", "heavy_multiplication_x", "heavy_plus_sign", "heavy_minus_sign", "heavy_division_sign", "heavy_equals_sign", "infinity", "bangbang", "interrobang", "question", "grey_question", "grey_exclamation", "exclamation", "wavy_dash", "currency_exchange", "heavy_dollar_sign", "recycle", "fleur_de_lis", "trident", "name_badge", "beginner", "o", "white_check_mark", "ballot_box_with_check", "heavy_check_mark", "x", "negative_squared_cross_mark", "curly_loop", "loop", "part_alternation_mark", "eight_spoked_asterisk", "eight_pointed_black_star", "sparkle", "copyright", "registered", "tm", "hash", "keycap_star", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "keycap_ten", "capital_abcd", "abcd", "1234", "symbols", "abc", "a", "ab", "b", "cl", "cool", "free", "information_source", "id", "m", "new", "ng", "o2", "ok", "parking", "sos", "up", "vs", "koko", "sa", "u6708", "u6709", "u6307", "ideograph_advantage", "u5272", "u7121", "u7981", "accept", "u7533", "u5408", "u7a7a", "congratulations", "secret", "u55b6", "u6e80", "red_circle", "large_orange_circle", "large_yellow_circle", "large_green_circle", "large_blue_circle", "large_purple_circle", "large_brown_circle", "black_circle", "white_circle", "large_red_square", "large_orange_square", "large_yellow_square", "large_green_square", "large_blue_square", "large_purple_square", "large_brown_square", "black_large_square", "white_large_square", "black_medium_square", "white_medium_square", "black_medium_small_square", "white_medium_small_square", "black_small_square", "white_small_square", "large_orange_diamond", "large_blue_diamond", "small_orange_diamond", "small_blue_diamond", "small_red_triangle", "small_red_triangle_down", "diamond_shape_with_a_dot_inside", "radio_button", "white_square_button", "black_square_button"] + }, { + "id": "flags", + "emojis": ["checkered_flag", "cn", "crossed_flags", "de", "es", "flag-ac", "flag-ad", "flag-ae", "flag-af", "flag-ag", "flag-ai", "flag-al", "flag-am", "flag-ao", "flag-aq", "flag-ar", "flag-as", "flag-at", "flag-au", "flag-aw", "flag-ax", "flag-az", "flag-ba", "flag-bb", "flag-bd", "flag-be", "flag-bf", "flag-bg", "flag-bh", "flag-bi", "flag-bj", "flag-bl", "flag-bm", "flag-bn", "flag-bo", "flag-bq", "flag-br", "flag-bs", "flag-bt", "flag-bv", "flag-bw", "flag-by", "flag-bz", "flag-ca", "flag-cc", "flag-cd", "flag-cf", "flag-cg", "flag-ch", "flag-ci", "flag-ck", "flag-cl", "flag-cm", "flag-co", "flag-cp", "flag-cr", "flag-cu", "flag-cv", "flag-cw", "flag-cx", "flag-cy", "flag-cz", "flag-dg", "flag-dj", "flag-dk", "flag-dm", "flag-do", "flag-dz", "flag-ea", "flag-ec", "flag-ee", "flag-eg", "flag-eh", "flag-england", "flag-er", "flag-et", "flag-eu", "flag-fi", "flag-fj", "flag-fk", "flag-fm", "flag-fo", "flag-ga", "flag-gd", "flag-ge", "flag-gf", "flag-gg", "flag-gh", "flag-gi", "flag-gl", "flag-gm", "flag-gn", "flag-gp", "flag-gq", "flag-gr", "flag-gs", "flag-gt", "flag-gu", "flag-gw", "flag-gy", "flag-hk", "flag-hm", "flag-hn", "flag-hr", "flag-ht", "flag-hu", "flag-ic", "flag-id", "flag-ie", "flag-il", "flag-im", "flag-in", "flag-io", "flag-iq", "flag-ir", "flag-is", "flag-je", "flag-jm", "flag-jo", "flag-ke", "flag-kg", "flag-kh", "flag-ki", "flag-km", "flag-kn", "flag-kp", "flag-kw", "flag-ky", "flag-kz", "flag-la", "flag-lb", "flag-lc", "flag-li", "flag-lk", "flag-lr", "flag-ls", "flag-lt", "flag-lu", "flag-lv", "flag-ly", "flag-ma", "flag-mc", "flag-md", "flag-me", "flag-mf", "flag-mg", "flag-mh", "flag-mk", "flag-ml", "flag-mm", "flag-mn", "flag-mo", "flag-mp", "flag-mq", "flag-mr", "flag-ms", "flag-mt", "flag-mu", "flag-mv", "flag-mw", "flag-mx", "flag-my", "flag-mz", "flag-na", "flag-nc", "flag-ne", "flag-nf", "flag-ng", "flag-ni", "flag-nl", "flag-no", "flag-np", "flag-nr", "flag-nu", "flag-nz", "flag-om", "flag-pa", "flag-pe", "flag-pf", "flag-pg", "flag-ph", "flag-pk", "flag-pl", "flag-pm", "flag-pn", "flag-pr", "flag-ps", "flag-pt", "flag-pw", "flag-py", "flag-qa", "flag-re", "flag-ro", "flag-rs", "flag-rw", "flag-sa", "flag-sb", "flag-sc", "flag-scotland", "flag-sd", "flag-se", "flag-sg", "flag-sh", "flag-si", "flag-sj", "flag-sk", "flag-sl", "flag-sm", "flag-sn", "flag-so", "flag-sr", "flag-ss", "flag-st", "flag-sv", "flag-sx", "flag-sy", "flag-sz", "flag-ta", "flag-tc", "flag-td", "flag-tf", "flag-tg", "flag-th", "flag-tj", "flag-tk", "flag-tl", "flag-tm", "flag-tn", "flag-to", "flag-tr", "flag-tt", "flag-tv", "flag-tw", "flag-tz", "flag-ua", "flag-ug", "flag-um", "flag-un", "flag-uy", "flag-uz", "flag-va", "flag-vc", "flag-ve", "flag-vg", "flag-vi", "flag-vn", "flag-vu", "flag-wales", "flag-wf", "flag-ws", "flag-xk", "flag-ye", "flag-yt", "flag-za", "flag-zm", "flag-zw", "fr", "gb", "it", "jp", "kr", "pirate_flag", "rainbow-flag", "ru", "transgender_flag", "triangular_flag_on_post", "us", "waving_black_flag", "waving_white_flag"] + }], + "emojis": { + "100": { + "id": "100", + "name": "Hundred Points", + "keywords": ["100", "score", "perfect", "numbers", "century", "exam", "quiz", "test", "pass"], + "skins": [{ + "unified": "1f4af", + "native": "๐Ÿ’ฏ", + "x": 28, + "y": 6 + }], + "version": 1 + }, + "1234": { + "id": "1234", + "name": "Input Numbers", + "keywords": ["1234", "blue", "square"], + "skins": [{ + "unified": "1f522", + "native": "๐Ÿ”ข", + "x": 29, + "y": 59 + }], + "version": 1 + }, + "grinning": { + "id": "grinning", + "name": "Grinning Face", + "emoticons": [":D"], + "keywords": ["smile", "happy", "joy", ":D", "grin"], + "skins": [{ + "unified": "1f600", + "native": "๐Ÿ˜€", + "x": 32, + "y": 20 + }], + "version": 1 + }, + "smiley": { + "id": "smiley", + "name": "Grinning Face with Big Eyes", + "emoticons": [":)", "=)", "=-)"], + "keywords": ["smiley", "happy", "joy", "haha", ":D", ":)", "smile", "funny"], + "skins": [{ + "unified": "1f603", + "native": "๐Ÿ˜ƒ", + "x": 32, + "y": 23 + }], + "version": 1 + }, + "smile": { + "id": "smile", + "name": "Grinning Face with Smiling Eyes", + "emoticons": [":)", "C:", "c:", ":D", ":-D"], + "keywords": ["smile", "happy", "joy", "funny", "haha", "laugh", "like", ":D", ":)"], + "skins": [{ + "unified": "1f604", + "native": "๐Ÿ˜„", + "x": 32, + "y": 24 + }], + "version": 1 + }, + "grin": { + "id": "grin", + "name": "Beaming Face with Smiling Eyes", + "keywords": ["grin", "happy", "smile", "joy", "kawaii"], + "skins": [{ + "unified": "1f601", + "native": "๐Ÿ˜", + "x": 32, + "y": 21 + }], + "version": 1 + }, + "laughing": { + "id": "laughing", + "name": "Grinning Squinting Face", + "emoticons": [":>", ":->"], + "keywords": ["laughing", "satisfied", "happy", "joy", "lol", "haha", "glad", "XD", "laugh"], + "skins": [{ + "unified": "1f606", + "native": "๐Ÿ˜†", + "x": 32, + "y": 26 + }], + "version": 1 + }, + "sweat_smile": { + "id": "sweat_smile", + "name": "Grinning Face with Sweat", + "keywords": ["smile", "hot", "happy", "laugh", "relief"], + "skins": [{ + "unified": "1f605", + "native": "๐Ÿ˜…", + "x": 32, + "y": 25 + }], + "version": 1 + }, + "rolling_on_the_floor_laughing": { + "id": "rolling_on_the_floor_laughing", + "name": "Rolling on the Floor Laughing", + "keywords": ["face", "lol", "haha", "rofl"], + "skins": [{ + "unified": "1f923", + "native": "๐Ÿคฃ", + "x": 40, + "y": 15 + }], + "version": 3 + }, + "joy": { + "id": "joy", + "name": "Face with Tears of Joy", + "keywords": ["cry", "weep", "happy", "happytears", "haha"], + "skins": [{ + "unified": "1f602", + "native": "๐Ÿ˜‚", + "x": 32, + "y": 22 + }], + "version": 1 + }, + "slightly_smiling_face": { + "id": "slightly_smiling_face", + "name": "Slightly Smiling Face", + "emoticons": [":)", "(:", ":-)"], + "keywords": ["smile"], + "skins": [{ + "unified": "1f642", + "native": "๐Ÿ™‚", + "x": 33, + "y": 28 + }], + "version": 1 + }, + "upside_down_face": { + "id": "upside_down_face", + "name": "Upside-Down Face", + "keywords": ["upside", "down", "flipped", "silly", "smile"], + "skins": [{ + "unified": "1f643", + "native": "๐Ÿ™ƒ", + "x": 33, + "y": 29 + }], + "version": 1 + }, + "melting_face": { + "id": "melting_face", + "name": "Melting Face", + "keywords": ["hot", "heat"], + "skins": [{ + "unified": "1fae0", + "native": "๐Ÿซ ", + "x": 55, + "y": 12 + }], + "version": 14 + }, + "wink": { + "id": "wink", + "name": "Winking Face", + "emoticons": [";)", ";-)"], + "keywords": ["wink", "happy", "mischievous", "secret", ";)", "smile", "eye"], + "skins": [{ + "unified": "1f609", + "native": "๐Ÿ˜‰", + "x": 32, + "y": 29 + }], + "version": 1 + }, + "blush": { + "id": "blush", + "name": "Smiling Face with Smiling Eyes", + "emoticons": [":)"], + "keywords": ["blush", "smile", "happy", "flushed", "crush", "embarrassed", "shy", "joy"], + "skins": [{ + "unified": "1f60a", + "native": "๐Ÿ˜Š", + "x": 32, + "y": 30 + }], + "version": 1 + }, + "innocent": { + "id": "innocent", + "name": "Smiling Face with Halo", + "keywords": ["innocent", "angel", "heaven"], + "skins": [{ + "unified": "1f607", + "native": "๐Ÿ˜‡", + "x": 32, + "y": 27 + }], + "version": 1 + }, + "smiling_face_with_3_hearts": { + "id": "smiling_face_with_3_hearts", + "name": "Smiling Face with Hearts", + "keywords": ["3", "love", "like", "affection", "valentines", "infatuation", "crush", "adore"], + "skins": [{ + "unified": "1f970", + "native": "๐Ÿฅฐ", + "x": 43, + "y": 58 + }], + "version": 11 + }, + "heart_eyes": { + "id": "heart_eyes", + "name": "Smiling Face with Heart-Eyes", + "keywords": ["heart", "eyes", "love", "like", "affection", "valentines", "infatuation", "crush"], + "skins": [{ + "unified": "1f60d", + "native": "๐Ÿ˜", + "x": 32, + "y": 33 + }], + "version": 1 + }, + "star-struck": { + "id": "star-struck", + "name": "Star-Struck", + "keywords": ["star", "struck", "grinning", "face", "with", "eyes", "smile", "starry"], + "skins": [{ + "unified": "1f929", + "native": "๐Ÿคฉ", + "x": 40, + "y": 38 + }], + "version": 5 + }, + "kissing_heart": { + "id": "kissing_heart", + "name": "Face Blowing a Kiss", + "emoticons": [":*", ":-*"], + "keywords": ["kissing", "heart", "love", "like", "affection", "valentines", "infatuation"], + "skins": [{ + "unified": "1f618", + "native": "๐Ÿ˜˜", + "x": 32, + "y": 44 + }], + "version": 1 + }, + "kissing": { + "id": "kissing", + "name": "Kissing Face", + "keywords": ["love", "like", "3", "valentines", "infatuation", "kiss"], + "skins": [{ + "unified": "1f617", + "native": "๐Ÿ˜—", + "x": 32, + "y": 43 + }], + "version": 1 + }, + "relaxed": { + "id": "relaxed", + "name": "Smiling Face", + "keywords": ["relaxed", "blush", "massage", "happiness"], + "skins": [{ + "unified": "263a-fe0f", + "native": "โ˜บ๏ธ", + "x": 57, + "y": 4 + }], + "version": 1 + }, + "kissing_closed_eyes": { + "id": "kissing_closed_eyes", + "name": "Kissing Face with Closed Eyes", + "keywords": ["love", "like", "affection", "valentines", "infatuation", "kiss"], + "skins": [{ + "unified": "1f61a", + "native": "๐Ÿ˜š", + "x": 32, + "y": 46 + }], + "version": 1 + }, + "kissing_smiling_eyes": { + "id": "kissing_smiling_eyes", + "name": "Kissing Face with Smiling Eyes", + "keywords": ["affection", "valentines", "infatuation", "kiss"], + "skins": [{ + "unified": "1f619", + "native": "๐Ÿ˜™", + "x": 32, + "y": 45 + }], + "version": 1 + }, + "smiling_face_with_tear": { + "id": "smiling_face_with_tear", + "name": "Smiling Face with Tear", + "keywords": ["sad", "cry", "pretend"], + "skins": [{ + "unified": "1f972", + "native": "๐Ÿฅฒ", + "x": 43, + "y": 60 + }], + "version": 13 + }, + "yum": { + "id": "yum", + "name": "Face Savoring Food", + "keywords": ["yum", "happy", "joy", "tongue", "smile", "silly", "yummy", "nom", "delicious", "savouring"], + "skins": [{ + "unified": "1f60b", + "native": "๐Ÿ˜‹", + "x": 32, + "y": 31 + }], + "version": 1 + }, + "stuck_out_tongue": { + "id": "stuck_out_tongue", + "name": "Face with Tongue", + "emoticons": [":p", ":-p", ":P", ":-P", ":b", ":-b"], + "keywords": ["stuck", "out", "prank", "childish", "playful", "mischievous", "smile"], + "skins": [{ + "unified": "1f61b", + "native": "๐Ÿ˜›", + "x": 32, + "y": 47 + }], + "version": 1 + }, + "stuck_out_tongue_winking_eye": { + "id": "stuck_out_tongue_winking_eye", + "name": "Winking Face with Tongue", + "emoticons": [";p", ";-p", ";b", ";-b", ";P", ";-P"], + "keywords": ["stuck", "out", "eye", "prank", "childish", "playful", "mischievous", "smile", "wink"], + "skins": [{ + "unified": "1f61c", + "native": "๐Ÿ˜œ", + "x": 32, + "y": 48 + }], + "version": 1 + }, + "zany_face": { + "id": "zany_face", + "name": "Zany Face", + "keywords": ["grinning", "with", "one", "large", "and", "small", "eye", "goofy", "crazy"], + "skins": [{ + "unified": "1f92a", + "native": "๐Ÿคช", + "x": 40, + "y": 39 + }], + "version": 5 + }, + "stuck_out_tongue_closed_eyes": { + "id": "stuck_out_tongue_closed_eyes", + "name": "Squinting Face with Tongue", + "keywords": ["stuck", "out", "closed", "eyes", "prank", "playful", "mischievous", "smile"], + "skins": [{ + "unified": "1f61d", + "native": "๐Ÿ˜", + "x": 32, + "y": 49 + }], + "version": 1 + }, + "money_mouth_face": { + "id": "money_mouth_face", + "name": "Money-Mouth Face", + "keywords": ["money", "mouth", "rich", "dollar"], + "skins": [{ + "unified": "1f911", + "native": "๐Ÿค‘", + "x": 38, + "y": 59 + }], + "version": 1 + }, + "hugging_face": { + "id": "hugging_face", + "name": "Hugging Face", + "keywords": ["smile", "hug"], + "skins": [{ + "unified": "1f917", + "native": "๐Ÿค—", + "x": 39, + "y": 4 + }], + "version": 1 + }, + "face_with_hand_over_mouth": { + "id": "face_with_hand_over_mouth", + "name": "Face with Hand over Mouth", + "keywords": ["smiling", "eyes", "and", "covering", "whoops", "shock", "surprise"], + "skins": [{ + "unified": "1f92d", + "native": "๐Ÿคญ", + "x": 40, + "y": 42 + }], + "version": 5 + }, + "face_with_open_eyes_and_hand_over_mouth": { + "id": "face_with_open_eyes_and_hand_over_mouth", + "name": "Face with Open Eyes and Hand over Mouth", + "keywords": ["silence", "secret", "shock", "surprise"], + "skins": [{ + "unified": "1fae2", + "native": "๐Ÿซข", + "x": 55, + "y": 14 + }], + "version": 14 + }, + "face_with_peeking_eye": { + "id": "face_with_peeking_eye", + "name": "Face with Peeking Eye", + "keywords": ["scared", "frightening", "embarrassing"], + "skins": [{ + "unified": "1fae3", + "native": "๐Ÿซฃ", + "x": 55, + "y": 15 + }], + "version": 14 + }, + "shushing_face": { + "id": "shushing_face", + "name": "Shushing Face", + "keywords": ["with", "finger", "covering", "closed", "lips", "quiet", "shhh"], + "skins": [{ + "unified": "1f92b", + "native": "๐Ÿคซ", + "x": 40, + "y": 40 + }], + "version": 5 + }, + "thinking_face": { + "id": "thinking_face", + "name": "Thinking Face", + "keywords": ["hmmm", "think", "consider"], + "skins": [{ + "unified": "1f914", + "native": "๐Ÿค”", + "x": 39, + "y": 1 + }], + "version": 1 + }, + "saluting_face": { + "id": "saluting_face", + "name": "Saluting Face", + "keywords": ["respect", "salute"], + "skins": [{ + "unified": "1fae1", + "native": "๐Ÿซก", + "x": 55, + "y": 13 + }], + "version": 14 + }, + "zipper_mouth_face": { + "id": "zipper_mouth_face", + "name": "Zipper-Mouth Face", + "keywords": ["zipper", "mouth", "sealed", "secret"], + "skins": [{ + "unified": "1f910", + "native": "๐Ÿค", + "x": 38, + "y": 58 + }], + "version": 1 + }, + "face_with_raised_eyebrow": { + "id": "face_with_raised_eyebrow", + "name": "Face with Raised Eyebrow", + "keywords": ["one", "distrust", "scepticism", "disapproval", "disbelief", "surprise"], + "skins": [{ + "unified": "1f928", + "native": "๐Ÿคจ", + "x": 40, + "y": 37 + }], + "version": 5 + }, + "neutral_face": { + "id": "neutral_face", + "name": "Neutral Face", + "emoticons": [":|", ":-|"], + "keywords": ["indifference", "meh", ":", ""], + "skins": [{ + "unified": "1f610", + "native": "๐Ÿ˜", + "x": 32, + "y": 36 + }], + "version": 1 + }, + "expressionless": { + "id": "expressionless", + "name": "Expressionless Face", + "emoticons": ["-_-"], + "keywords": ["indifferent", "-", "", "meh", "deadpan"], + "skins": [{ + "unified": "1f611", + "native": "๐Ÿ˜‘", + "x": 32, + "y": 37 + }], + "version": 1 + }, + "no_mouth": { + "id": "no_mouth", + "name": "Face Without Mouth", + "keywords": ["no", "hellokitty"], + "skins": [{ + "unified": "1f636", + "native": "๐Ÿ˜ถ", + "x": 33, + "y": 16 + }], + "version": 1 + }, + "dotted_line_face": { + "id": "dotted_line_face", + "name": "Dotted Line Face", + "keywords": ["invisible", "lonely", "isolation", "depression"], + "skins": [{ + "unified": "1fae5", + "native": "๐Ÿซฅ", + "x": 55, + "y": 17 + }], + "version": 14 + }, + "face_in_clouds": { + "id": "face_in_clouds", + "name": "Face in Clouds", + "keywords": ["shower", "steam", "dream"], + "skins": [{ + "unified": "1f636-200d-1f32b-fe0f", + "native": "๐Ÿ˜ถโ€๐ŸŒซ๏ธ", + "x": 33, + "y": 15 + }], + "version": 13.1 + }, + "smirk": { + "id": "smirk", + "name": "Smirking Face", + "keywords": ["smirk", "smile", "mean", "prank", "smug", "sarcasm"], + "skins": [{ + "unified": "1f60f", + "native": "๐Ÿ˜", + "x": 32, + "y": 35 + }], + "version": 1 + }, + "unamused": { + "id": "unamused", + "name": "Unamused Face", + "emoticons": [":("], + "keywords": ["indifference", "bored", "straight", "serious", "sarcasm", "unimpressed", "skeptical", "dubious", "side", "eye"], + "skins": [{ + "unified": "1f612", + "native": "๐Ÿ˜’", + "x": 32, + "y": 38 + }], + "version": 1 + }, + "face_with_rolling_eyes": { + "id": "face_with_rolling_eyes", + "name": "Face with Rolling Eyes", + "keywords": ["eyeroll", "frustrated"], + "skins": [{ + "unified": "1f644", + "native": "๐Ÿ™„", + "x": 33, + "y": 30 + }], + "version": 1 + }, + "grimacing": { + "id": "grimacing", + "name": "Grimacing Face", + "keywords": ["grimace", "teeth"], + "skins": [{ + "unified": "1f62c", + "native": "๐Ÿ˜ฌ", + "x": 33, + "y": 3 + }], + "version": 1 + }, + "face_exhaling": { + "id": "face_exhaling", + "name": "Face Exhaling", + "keywords": ["relieve", "relief", "tired", "sigh"], + "skins": [{ + "unified": "1f62e-200d-1f4a8", + "native": "๐Ÿ˜ฎโ€๐Ÿ’จ", + "x": 33, + "y": 5 + }], + "version": 13.1 + }, + "lying_face": { + "id": "lying_face", + "name": "Lying Face", + "keywords": ["lie", "pinocchio"], + "skins": [{ + "unified": "1f925", + "native": "๐Ÿคฅ", + "x": 40, + "y": 17 + }], + "version": 3 + }, + "relieved": { + "id": "relieved", + "name": "Relieved Face", + "keywords": ["relaxed", "phew", "massage", "happiness"], + "skins": [{ + "unified": "1f60c", + "native": "๐Ÿ˜Œ", + "x": 32, + "y": 32 + }], + "version": 1 + }, + "pensive": { + "id": "pensive", + "name": "Pensive Face", + "keywords": ["sad", "depressed", "upset"], + "skins": [{ + "unified": "1f614", + "native": "๐Ÿ˜”", + "x": 32, + "y": 40 + }], + "version": 1 + }, + "sleepy": { + "id": "sleepy", + "name": "Sleepy Face", + "keywords": ["tired", "rest", "nap"], + "skins": [{ + "unified": "1f62a", + "native": "๐Ÿ˜ช", + "x": 33, + "y": 1 + }], + "version": 1 + }, + "drooling_face": { + "id": "drooling_face", + "name": "Drooling Face", + "keywords": [], + "skins": [{ + "unified": "1f924", + "native": "๐Ÿคค", + "x": 40, + "y": 16 + }], + "version": 3 + }, + "sleeping": { + "id": "sleeping", + "name": "Sleeping Face", + "keywords": ["tired", "sleepy", "night", "zzz"], + "skins": [{ + "unified": "1f634", + "native": "๐Ÿ˜ด", + "x": 33, + "y": 12 + }], + "version": 1 + }, + "mask": { + "id": "mask", + "name": "Face with Medical Mask", + "keywords": ["sick", "ill", "disease"], + "skins": [{ + "unified": "1f637", + "native": "๐Ÿ˜ท", + "x": 33, + "y": 17 + }], + "version": 1 + }, + "face_with_thermometer": { + "id": "face_with_thermometer", + "name": "Face with Thermometer", + "keywords": ["sick", "temperature", "cold", "fever"], + "skins": [{ + "unified": "1f912", + "native": "๐Ÿค’", + "x": 38, + "y": 60 + }], + "version": 1 + }, + "face_with_head_bandage": { + "id": "face_with_head_bandage", + "name": "Face with Head-Bandage", + "keywords": ["head", "bandage", "injured", "clumsy", "hurt"], + "skins": [{ + "unified": "1f915", + "native": "๐Ÿค•", + "x": 39, + "y": 2 + }], + "version": 1 + }, + "nauseated_face": { + "id": "nauseated_face", + "name": "Nauseated Face", + "keywords": ["vomit", "gross", "green", "sick", "throw", "up", "ill"], + "skins": [{ + "unified": "1f922", + "native": "๐Ÿคข", + "x": 40, + "y": 14 + }], + "version": 3 + }, + "face_vomiting": { + "id": "face_vomiting", + "name": "Face Vomiting", + "keywords": ["with", "open", "mouth", "sick"], + "skins": [{ + "unified": "1f92e", + "native": "๐Ÿคฎ", + "x": 40, + "y": 43 + }], + "version": 5 + }, + "sneezing_face": { + "id": "sneezing_face", + "name": "Sneezing Face", + "keywords": ["gesundheit", "sneeze", "sick", "allergy"], + "skins": [{ + "unified": "1f927", + "native": "๐Ÿคง", + "x": 40, + "y": 36 + }], + "version": 3 + }, + "hot_face": { + "id": "hot_face", + "name": "Hot Face", + "keywords": ["feverish", "heat", "red", "sweating"], + "skins": [{ + "unified": "1f975", + "native": "๐Ÿฅต", + "x": 44, + "y": 2 + }], + "version": 11 + }, + "cold_face": { + "id": "cold_face", + "name": "Cold Face", + "keywords": ["blue", "freezing", "frozen", "frostbite", "icicles"], + "skins": [{ + "unified": "1f976", + "native": "๐Ÿฅถ", + "x": 44, + "y": 3 + }], + "version": 11 + }, + "woozy_face": { + "id": "woozy_face", + "name": "Woozy Face", + "keywords": ["dizzy", "intoxicated", "tipsy", "wavy"], + "skins": [{ + "unified": "1f974", + "native": "๐Ÿฅด", + "x": 44, + "y": 1 + }], + "version": 11 + }, + "dizzy_face": { + "id": "dizzy_face", + "name": "Dizzy Face", + "keywords": ["spent", "unconscious", "xox"], + "skins": [{ + "unified": "1f635", + "native": "๐Ÿ˜ต", + "x": 33, + "y": 14 + }], + "version": 1 + }, + "face_with_spiral_eyes": { + "id": "face_with_spiral_eyes", + "name": "Face with Spiral Eyes", + "keywords": ["sick", "ill", "confused", "nauseous", "nausea"], + "skins": [{ + "unified": "1f635-200d-1f4ab", + "native": "๐Ÿ˜ตโ€๐Ÿ’ซ", + "x": 33, + "y": 13 + }], + "version": 13.1 + }, + "exploding_head": { + "id": "exploding_head", + "name": "Exploding Head", + "keywords": ["shocked", "face", "with", "mind", "blown"], + "skins": [{ + "unified": "1f92f", + "native": "๐Ÿคฏ", + "x": 40, + "y": 44 + }], + "version": 5 + }, + "face_with_cowboy_hat": { + "id": "face_with_cowboy_hat", + "name": "Cowboy Hat Face", + "keywords": ["with", "cowgirl"], + "skins": [{ + "unified": "1f920", + "native": "๐Ÿค ", + "x": 40, + "y": 12 + }], + "version": 3 + }, + "partying_face": { + "id": "partying_face", + "name": "Partying Face", + "keywords": ["celebration", "woohoo"], + "skins": [{ + "unified": "1f973", + "native": "๐Ÿฅณ", + "x": 44, + "y": 0 + }], + "version": 11 + }, + "disguised_face": { + "id": "disguised_face", + "name": "Disguised Face", + "keywords": ["pretent", "brows", "glasses", "moustache"], + "skins": [{ + "unified": "1f978", + "native": "๐Ÿฅธ", + "x": 44, + "y": 10 + }], + "version": 13 + }, + "sunglasses": { + "id": "sunglasses", + "name": "Smiling Face with Sunglasses", + "emoticons": ["8)"], + "keywords": ["cool", "smile", "summer", "beach", "sunglass"], + "skins": [{ + "unified": "1f60e", + "native": "๐Ÿ˜Ž", + "x": 32, + "y": 34 + }], + "version": 1 + }, + "nerd_face": { + "id": "nerd_face", + "name": "Nerd Face", + "keywords": ["nerdy", "geek", "dork"], + "skins": [{ + "unified": "1f913", + "native": "๐Ÿค“", + "x": 39, + "y": 0 + }], + "version": 1 + }, + "face_with_monocle": { + "id": "face_with_monocle", + "name": "Face with Monocle", + "keywords": ["stuffy", "wealthy"], + "skins": [{ + "unified": "1f9d0", + "native": "๐Ÿง", + "x": 47, + "y": 11 + }], + "version": 5 + }, + "confused": { + "id": "confused", + "name": "Confused Face", + "emoticons": [":\\", ":-\\", ":/", ":-/"], + "keywords": ["indifference", "huh", "weird", "hmmm", ":/"], + "skins": [{ + "unified": "1f615", + "native": "๐Ÿ˜•", + "x": 32, + "y": 41 + }], + "version": 1 + }, + "face_with_diagonal_mouth": { + "id": "face_with_diagonal_mouth", + "name": "Face with Diagonal Mouth", + "keywords": ["skeptic", "confuse", "frustrated", "indifferent"], + "skins": [{ + "unified": "1fae4", + "native": "๐Ÿซค", + "x": 55, + "y": 16 + }], + "version": 14 + }, + "worried": { + "id": "worried", + "name": "Worried Face", + "keywords": ["concern", "nervous", ":("], + "skins": [{ + "unified": "1f61f", + "native": "๐Ÿ˜Ÿ", + "x": 32, + "y": 51 + }], + "version": 1 + }, + "slightly_frowning_face": { + "id": "slightly_frowning_face", + "name": "Slightly Frowning Face", + "keywords": ["disappointed", "sad", "upset"], + "skins": [{ + "unified": "1f641", + "native": "๐Ÿ™", + "x": 33, + "y": 27 + }], + "version": 1 + }, + "white_frowning_face": { + "id": "white_frowning_face", + "name": "Frowning Face", + "keywords": ["white", "sad", "upset", "frown"], + "skins": [{ + "unified": "2639-fe0f", + "native": "โ˜น๏ธ", + "x": 57, + "y": 3 + }], + "version": 1 + }, + "open_mouth": { + "id": "open_mouth", + "name": "Face with Open Mouth", + "emoticons": [":o", ":-o", ":O", ":-O"], + "keywords": ["surprise", "impressed", "wow", "whoa", ":O"], + "skins": [{ + "unified": "1f62e", + "native": "๐Ÿ˜ฎ", + "x": 33, + "y": 6 + }], + "version": 1 + }, + "hushed": { + "id": "hushed", + "name": "Hushed Face", + "keywords": ["woo", "shh"], + "skins": [{ + "unified": "1f62f", + "native": "๐Ÿ˜ฏ", + "x": 33, + "y": 7 + }], + "version": 1 + }, + "astonished": { + "id": "astonished", + "name": "Astonished Face", + "keywords": ["xox", "surprised", "poisoned"], + "skins": [{ + "unified": "1f632", + "native": "๐Ÿ˜ฒ", + "x": 33, + "y": 10 + }], + "version": 1 + }, + "flushed": { + "id": "flushed", + "name": "Flushed Face", + "keywords": ["blush", "shy", "flattered"], + "skins": [{ + "unified": "1f633", + "native": "๐Ÿ˜ณ", + "x": 33, + "y": 11 + }], + "version": 1 + }, + "pleading_face": { + "id": "pleading_face", + "name": "Pleading Face", + "keywords": ["begging", "mercy"], + "skins": [{ + "unified": "1f97a", + "native": "๐Ÿฅบ", + "x": 44, + "y": 12 + }], + "version": 11 + }, + "face_holding_back_tears": { + "id": "face_holding_back_tears", + "name": "Face Holding Back Tears", + "keywords": ["touched", "gratitude"], + "skins": [{ + "unified": "1f979", + "native": "๐Ÿฅน", + "x": 44, + "y": 11 + }], + "version": 14 + }, + "frowning": { + "id": "frowning", + "name": "Frowning Face with Open Mouth", + "keywords": ["aw", "what"], + "skins": [{ + "unified": "1f626", + "native": "๐Ÿ˜ฆ", + "x": 32, + "y": 58 + }], + "version": 1 + }, + "anguished": { + "id": "anguished", + "name": "Anguished Face", + "emoticons": ["D:"], + "keywords": ["stunned", "nervous"], + "skins": [{ + "unified": "1f627", + "native": "๐Ÿ˜ง", + "x": 32, + "y": 59 + }], + "version": 1 + }, + "fearful": { + "id": "fearful", + "name": "Fearful Face", + "keywords": ["scared", "terrified", "nervous", "oops", "huh"], + "skins": [{ + "unified": "1f628", + "native": "๐Ÿ˜จ", + "x": 32, + "y": 60 + }], + "version": 1 + }, + "cold_sweat": { + "id": "cold_sweat", + "name": "Anxious Face with Sweat", + "keywords": ["cold", "nervous"], + "skins": [{ + "unified": "1f630", + "native": "๐Ÿ˜ฐ", + "x": 33, + "y": 8 + }], + "version": 1 + }, + "disappointed_relieved": { + "id": "disappointed_relieved", + "name": "Sad but Relieved Face", + "keywords": ["disappointed", "phew", "sweat", "nervous"], + "skins": [{ + "unified": "1f625", + "native": "๐Ÿ˜ฅ", + "x": 32, + "y": 57 + }], + "version": 1 + }, + "cry": { + "id": "cry", + "name": "Crying Face", + "emoticons": [":'("], + "keywords": ["cry", "tears", "sad", "depressed", "upset", ":'("], + "skins": [{ + "unified": "1f622", + "native": "๐Ÿ˜ข", + "x": 32, + "y": 54 + }], + "version": 1 + }, + "sob": { + "id": "sob", + "name": "Loudly Crying Face", + "emoticons": [":'("], + "keywords": ["sob", "cry", "tears", "sad", "upset", "depressed"], + "skins": [{ + "unified": "1f62d", + "native": "๐Ÿ˜ญ", + "x": 33, + "y": 4 + }], + "version": 1 + }, + "scream": { + "id": "scream", + "name": "Face Screaming in Fear", + "keywords": ["scream", "munch", "scared", "omg"], + "skins": [{ + "unified": "1f631", + "native": "๐Ÿ˜ฑ", + "x": 33, + "y": 9 + }], + "version": 1 + }, + "confounded": { + "id": "confounded", + "name": "Confounded Face", + "keywords": ["confused", "sick", "unwell", "oops", ":S"], + "skins": [{ + "unified": "1f616", + "native": "๐Ÿ˜–", + "x": 32, + "y": 42 + }], + "version": 1 + }, + "persevere": { + "id": "persevere", + "name": "Persevering Face", + "keywords": ["persevere", "sick", "no", "upset", "oops"], + "skins": [{ + "unified": "1f623", + "native": "๐Ÿ˜ฃ", + "x": 32, + "y": 55 + }], + "version": 1 + }, + "disappointed": { + "id": "disappointed", + "name": "Disappointed Face", + "emoticons": ["):", ":(", ":-("], + "keywords": ["sad", "upset", "depressed", ":("], + "skins": [{ + "unified": "1f61e", + "native": "๐Ÿ˜ž", + "x": 32, + "y": 50 + }], + "version": 1 + }, + "sweat": { + "id": "sweat", + "name": "Face with Cold Sweat", + "keywords": ["downcast", "hot", "sad", "tired", "exercise"], + "skins": [{ + "unified": "1f613", + "native": "๐Ÿ˜“", + "x": 32, + "y": 39 + }], + "version": 1 + }, + "weary": { + "id": "weary", + "name": "Weary Face", + "keywords": ["tired", "sleepy", "sad", "frustrated", "upset"], + "skins": [{ + "unified": "1f629", + "native": "๐Ÿ˜ฉ", + "x": 33, + "y": 0 + }], + "version": 1 + }, + "tired_face": { + "id": "tired_face", + "name": "Tired Face", + "keywords": ["sick", "whine", "upset", "frustrated"], + "skins": [{ + "unified": "1f62b", + "native": "๐Ÿ˜ซ", + "x": 33, + "y": 2 + }], + "version": 1 + }, + "yawning_face": { + "id": "yawning_face", + "name": "Yawning Face", + "keywords": ["tired", "sleepy"], + "skins": [{ + "unified": "1f971", + "native": "๐Ÿฅฑ", + "x": 43, + "y": 59 + }], + "version": 12 + }, + "triumph": { + "id": "triumph", + "name": "Face with Look of Triumph", + "keywords": ["steam", "from", "nose", "gas", "phew", "proud", "pride"], + "skins": [{ + "unified": "1f624", + "native": "๐Ÿ˜ค", + "x": 32, + "y": 56 + }], + "version": 1 + }, + "rage": { + "id": "rage", + "name": "Pouting Face", + "keywords": ["rage", "angry", "mad", "hate", "despise"], + "skins": [{ + "unified": "1f621", + "native": "๐Ÿ˜ก", + "x": 32, + "y": 53 + }], + "version": 1 + }, + "angry": { + "id": "angry", + "name": "Angry Face", + "emoticons": [">:(", ">:-("], + "keywords": ["mad", "annoyed", "frustrated"], + "skins": [{ + "unified": "1f620", + "native": "๐Ÿ˜ ", + "x": 32, + "y": 52 + }], + "version": 1 + }, + "face_with_symbols_on_mouth": { + "id": "face_with_symbols_on_mouth", + "name": "Face with Symbols on Mouth", + "keywords": ["serious", "covering", "swearing", "cursing", "cussing", "profanity", "expletive"], + "skins": [{ + "unified": "1f92c", + "native": "๐Ÿคฌ", + "x": 40, + "y": 41 + }], + "version": 5 + }, + "smiling_imp": { + "id": "smiling_imp", + "name": "Smiling Face with Horns", + "keywords": ["imp", "devil"], + "skins": [{ + "unified": "1f608", + "native": "๐Ÿ˜ˆ", + "x": 32, + "y": 28 + }], + "version": 1 + }, + "imp": { + "id": "imp", + "name": "Imp", + "keywords": ["angry", "face", "with", "horns", "devil"], + "skins": [{ + "unified": "1f47f", + "native": "๐Ÿ‘ฟ", + "x": 25, + "y": 8 + }], + "version": 1 + }, + "skull": { + "id": "skull", + "name": "Skull", + "keywords": ["dead", "skeleton", "creepy", "death"], + "skins": [{ + "unified": "1f480", + "native": "๐Ÿ’€", + "x": 25, + "y": 9 + }], + "version": 1 + }, + "skull_and_crossbones": { + "id": "skull_and_crossbones", + "name": "Skull and Crossbones", + "keywords": ["poison", "danger", "deadly", "scary", "death", "pirate", "evil"], + "skins": [{ + "unified": "2620-fe0f", + "native": "โ˜ ๏ธ", + "x": 56, + "y": 56 + }], + "version": 1 + }, + "hankey": { + "id": "hankey", + "name": "Pile of Poo", + "keywords": ["hankey", "poop", "shit", "shitface", "fail", "turd"], + "skins": [{ + "unified": "1f4a9", + "native": "๐Ÿ’ฉ", + "x": 27, + "y": 56 + }], + "version": 1 + }, + "clown_face": { + "id": "clown_face", + "name": "Clown Face", + "keywords": [], + "skins": [{ + "unified": "1f921", + "native": "๐Ÿคก", + "x": 40, + "y": 13 + }], + "version": 3 + }, + "japanese_ogre": { + "id": "japanese_ogre", + "name": "Ogre", + "keywords": ["japanese", "monster", "red", "mask", "halloween", "scary", "creepy", "devil", "demon"], + "skins": [{ + "unified": "1f479", + "native": "๐Ÿ‘น", + "x": 24, + "y": 58 + }], + "version": 1 + }, + "japanese_goblin": { + "id": "japanese_goblin", + "name": "Goblin", + "keywords": ["japanese", "red", "evil", "mask", "monster", "scary", "creepy"], + "skins": [{ + "unified": "1f47a", + "native": "๐Ÿ‘บ", + "x": 24, + "y": 59 + }], + "version": 1 + }, + "ghost": { + "id": "ghost", + "name": "Ghost", + "keywords": ["halloween", "spooky", "scary"], + "skins": [{ + "unified": "1f47b", + "native": "๐Ÿ‘ป", + "x": 24, + "y": 60 + }], + "version": 1 + }, + "alien": { + "id": "alien", + "name": "Alien", + "keywords": ["UFO", "paul", "weird", "outer", "space"], + "skins": [{ + "unified": "1f47d", + "native": "๐Ÿ‘ฝ", + "x": 25, + "y": 6 + }], + "version": 1 + }, + "space_invader": { + "id": "space_invader", + "name": "Alien Monster", + "keywords": ["space", "invader", "game", "arcade", "play"], + "skins": [{ + "unified": "1f47e", + "native": "๐Ÿ‘พ", + "x": 25, + "y": 7 + }], + "version": 1 + }, + "robot_face": { + "id": "robot_face", + "name": "Robot", + "keywords": ["face", "computer", "machine", "bot"], + "skins": [{ + "unified": "1f916", + "native": "๐Ÿค–", + "x": 39, + "y": 3 + }], + "version": 1 + }, + "smiley_cat": { + "id": "smiley_cat", + "name": "Grinning Cat", + "keywords": ["smiley", "animal", "cats", "happy", "smile"], + "skins": [{ + "unified": "1f63a", + "native": "๐Ÿ˜บ", + "x": 33, + "y": 20 + }], + "version": 1 + }, + "smile_cat": { + "id": "smile_cat", + "name": "Grinning Cat with Smiling Eyes", + "keywords": ["smile", "animal", "cats"], + "skins": [{ + "unified": "1f638", + "native": "๐Ÿ˜ธ", + "x": 33, + "y": 18 + }], + "version": 1 + }, + "joy_cat": { + "id": "joy_cat", + "name": "Cat with Tears of Joy", + "keywords": ["animal", "cats", "haha", "happy"], + "skins": [{ + "unified": "1f639", + "native": "๐Ÿ˜น", + "x": 33, + "y": 19 + }], + "version": 1 + }, + "heart_eyes_cat": { + "id": "heart_eyes_cat", + "name": "Smiling Cat with Heart-Eyes", + "keywords": ["heart", "eyes", "animal", "love", "like", "affection", "cats", "valentines"], + "skins": [{ + "unified": "1f63b", + "native": "๐Ÿ˜ป", + "x": 33, + "y": 21 + }], + "version": 1 + }, + "smirk_cat": { + "id": "smirk_cat", + "name": "Cat with Wry Smile", + "keywords": ["smirk", "animal", "cats"], + "skins": [{ + "unified": "1f63c", + "native": "๐Ÿ˜ผ", + "x": 33, + "y": 22 + }], + "version": 1 + }, + "kissing_cat": { + "id": "kissing_cat", + "name": "Kissing Cat", + "keywords": ["animal", "cats", "kiss"], + "skins": [{ + "unified": "1f63d", + "native": "๐Ÿ˜ฝ", + "x": 33, + "y": 23 + }], + "version": 1 + }, + "scream_cat": { + "id": "scream_cat", + "name": "Weary Cat", + "keywords": ["scream", "animal", "cats", "munch", "scared"], + "skins": [{ + "unified": "1f640", + "native": "๐Ÿ™€", + "x": 33, + "y": 26 + }], + "version": 1 + }, + "crying_cat_face": { + "id": "crying_cat_face", + "name": "Crying Cat", + "keywords": ["face", "animal", "tears", "weep", "sad", "cats", "upset", "cry"], + "skins": [{ + "unified": "1f63f", + "native": "๐Ÿ˜ฟ", + "x": 33, + "y": 25 + }], + "version": 1 + }, + "pouting_cat": { + "id": "pouting_cat", + "name": "Pouting Cat", + "keywords": ["animal", "cats"], + "skins": [{ + "unified": "1f63e", + "native": "๐Ÿ˜พ", + "x": 33, + "y": 24 + }], + "version": 1 + }, + "see_no_evil": { + "id": "see_no_evil", + "name": "See-No-Evil Monkey", + "keywords": ["see", "no", "evil", "animal", "nature", "haha"], + "skins": [{ + "unified": "1f648", + "native": "๐Ÿ™ˆ", + "x": 34, + "y": 24 + }], + "version": 1 + }, + "hear_no_evil": { + "id": "hear_no_evil", + "name": "Hear-No-Evil Monkey", + "keywords": ["hear", "no", "evil", "animal", "nature"], + "skins": [{ + "unified": "1f649", + "native": "๐Ÿ™‰", + "x": 34, + "y": 25 + }], + "version": 1 + }, + "speak_no_evil": { + "id": "speak_no_evil", + "name": "Speak-No-Evil Monkey", + "keywords": ["speak", "no", "evil", "animal", "nature", "omg"], + "skins": [{ + "unified": "1f64a", + "native": "๐Ÿ™Š", + "x": 34, + "y": 26 + }], + "version": 1 + }, + "kiss": { + "id": "kiss", + "name": "Kiss Mark", + "keywords": ["face", "lips", "love", "like", "affection", "valentines"], + "skins": [{ + "unified": "1f48b", + "native": "๐Ÿ’‹", + "x": 26, + "y": 37 + }], + "version": 1 + }, + "love_letter": { + "id": "love_letter", + "name": "Love Letter", + "keywords": ["email", "like", "affection", "envelope", "valentines"], + "skins": [{ + "unified": "1f48c", + "native": "๐Ÿ’Œ", + "x": 26, + "y": 38 + }], + "version": 1 + }, + "cupid": { + "id": "cupid", + "name": "Heart with Arrow", + "keywords": ["cupid", "love", "like", "affection", "valentines"], + "skins": [{ + "unified": "1f498", + "native": "๐Ÿ’˜", + "x": 27, + "y": 39 + }], + "version": 1 + }, + "gift_heart": { + "id": "gift_heart", + "name": "Heart with Ribbon", + "keywords": ["gift", "love", "valentines"], + "skins": [{ + "unified": "1f49d", + "native": "๐Ÿ’", + "x": 27, + "y": 44 + }], + "version": 1 + }, + "sparkling_heart": { + "id": "sparkling_heart", + "name": "Sparkling Heart", + "keywords": ["love", "like", "affection", "valentines"], + "skins": [{ + "unified": "1f496", + "native": "๐Ÿ’–", + "x": 27, + "y": 37 + }], + "version": 1 + }, + "heartpulse": { + "id": "heartpulse", + "name": "Growing Heart", + "keywords": ["heartpulse", "like", "love", "affection", "valentines", "pink"], + "skins": [{ + "unified": "1f497", + "native": "๐Ÿ’—", + "x": 27, + "y": 38 + }], + "version": 1 + }, + "heartbeat": { + "id": "heartbeat", + "name": "Beating Heart", + "keywords": ["heartbeat", "love", "like", "affection", "valentines", "pink"], + "skins": [{ + "unified": "1f493", + "native": "๐Ÿ’“", + "x": 27, + "y": 34 + }], + "version": 1 + }, + "revolving_hearts": { + "id": "revolving_hearts", + "name": "Revolving Hearts", + "keywords": ["love", "like", "affection", "valentines"], + "skins": [{ + "unified": "1f49e", + "native": "๐Ÿ’ž", + "x": 27, + "y": 45 + }], + "version": 1 + }, + "two_hearts": { + "id": "two_hearts", + "name": "Two Hearts", + "keywords": ["love", "like", "affection", "valentines", "heart"], + "skins": [{ + "unified": "1f495", + "native": "๐Ÿ’•", + "x": 27, + "y": 36 + }], + "version": 1 + }, + "heart_decoration": { + "id": "heart_decoration", + "name": "Heart Decoration", + "keywords": ["purple", "square", "love", "like"], + "skins": [{ + "unified": "1f49f", + "native": "๐Ÿ’Ÿ", + "x": 27, + "y": 46 + }], + "version": 1 + }, + "heavy_heart_exclamation_mark_ornament": { + "id": "heavy_heart_exclamation_mark_ornament", + "name": "Heart Exclamation", + "keywords": ["heavy", "mark", "ornament", "decoration", "love"], + "skins": [{ + "unified": "2763-fe0f", + "native": "โฃ๏ธ", + "x": 59, + "y": 7 + }], + "version": 1 + }, + "broken_heart": { + "id": "broken_heart", + "name": "Broken Heart", + "emoticons": [" [EmojiCategory] +} + +private enum EmojiProviderState { + case notLoaded + case inProgress(Task<[EmojiCategory], Never>) + case loaded([EmojiCategory]) +} + +class EmojiProvider: EmojiProviderProtocol { + private let loader: EmojiLoaderProtocol + private var state: EmojiProviderState = .notLoaded + + init(loader: EmojiLoaderProtocol = EmojiMartJSONLoader()) { + self.loader = loader + Task { + await loadIfNeeded() + } + } + + func getCategories(searchString: String? = nil) async -> [EmojiCategory] { + let emojiCategories = await loadIfNeeded() + if let searchString = searchString, searchString.isEmpty == false { + return search(searchString: searchString, emojiCategories: emojiCategories) + } else { + return emojiCategories + } + } + + private func search(searchString: String, emojiCategories: [EmojiCategory]) -> [EmojiCategory] { + emojiCategories.compactMap { category in + let emojis = category.emojis.filter { emoji in + let searchArray = [emoji.id, emoji.name] + emoji.keywords + return searchArray.description.range(of: searchString, options: .caseInsensitive) != nil + } + return emojis.isEmpty ? nil : EmojiCategory(id: category.id, emojis: emojis) + } + } + + private func loadIfNeeded() async -> [EmojiCategory] { + switch state { + case .notLoaded: + let task = Task { + await loader.load() + } + state = .inProgress(task) + let categories = await task.value + state = .loaded(categories) + return categories + case .loaded(let categories): + return categories + case .inProgress(let task): + return await task.value + } + } +} diff --git a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift index c2751cce95..05e4da6bc8 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionFlowCoordinator.swift @@ -26,6 +26,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { private let userSession: UserSessionProtocol private let navigationController: NavigationController private let bugReportService: BugReportServiceProtocol + private let emojiProvider: EmojiProviderProtocol = EmojiProvider() var callback: ((UserSessionFlowCoordinatorAction) -> Void)? @@ -148,7 +149,8 @@ class UserSessionFlowCoordinator: CoordinatorProtocol { timelineController: timelineController, mediaProvider: userSession.mediaProvider, roomName: roomProxy.displayName ?? roomProxy.name, - roomAvatarUrl: roomProxy.avatarURL) + roomAvatarUrl: roomProxy.avatarURL, + emojiProvide: emojiProvider) let coordinator = RoomScreenCoordinator(parameters: parameters) navigationController.push(coordinator) { [weak self] in diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index e10bce5e60..7b23f2fae9 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -121,14 +121,16 @@ class MockScreen: Identifiable { timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), roomName: "Some room name", - roomAvatarUrl: nil) + roomAvatarUrl: nil, + emojiProvide: EmojiProvider()) return RoomScreenCoordinator(parameters: parameters) case .roomEncryptedWithAvatar: let parameters = RoomScreenCoordinatorParameters(navigationController: navigationController, timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), roomName: "Some room name", - roomAvatarUrl: "mock_url") + roomAvatarUrl: "mock_url", + emojiProvide: EmojiProvider()) return RoomScreenCoordinator(parameters: parameters) case .sessionVerification: let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: MockSessionVerificationControllerProxy()) diff --git a/UnitTests/Sources/EmojiMartJSONLoaderTests.swift b/UnitTests/Sources/EmojiMartJSONLoaderTests.swift new file mode 100644 index 0000000000..112000e5ef --- /dev/null +++ b/UnitTests/Sources/EmojiMartJSONLoaderTests.swift @@ -0,0 +1,32 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +final class EmojiMartJSONLoaderTests: XCTestCase { + var sut: EmojiMartJSONLoader! + + override func setUp() { + sut = EmojiMartJSONLoader() + } + + func test_whenEmojisLoaded_CorrectCategoryCountReturned() async { + let categories = await sut.load() + XCTAssertEqual(categories.count, 8) + } +} diff --git a/UnitTests/Sources/EmojiProviderTests.swift b/UnitTests/Sources/EmojiProviderTests.swift new file mode 100644 index 0000000000..1c9b50a7d0 --- /dev/null +++ b/UnitTests/Sources/EmojiProviderTests.swift @@ -0,0 +1,104 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +@testable import ElementX + +final class EmojiProviderTests: XCTestCase { + var sut: EmojiProvider! + private var emojiLoaderMock: EmojiLoaderMock! + + @MainActor override func setUp() { + emojiLoaderMock = EmojiLoaderMock() + sut = EmojiProvider(loader: emojiLoaderMock) + } + + func test_whenEmojisLoaded_categoriesAreLoadedFromLoader() async throws { + let item = EmojiItem(id: "test", name: "test", keywords: ["1", "2"], skins: [try slightlySmilingFaceEmoji()]) + let category = EmojiCategory(id: "test", emojis: [item]) + emojiLoaderMock.categories = [category] + let categories = await sut.getCategories() + XCTAssertEqual(emojiLoaderMock.categories, categories) + } + + func test_whenEmojisLoadedAndSearchStringEmpty_allCategoriesReturned() async throws { + let item = EmojiItem(id: "test", name: "test", keywords: ["1", "2"], skins: [try slightlySmilingFaceEmoji()]) + let category = EmojiCategory(id: "test", emojis: [item]) + emojiLoaderMock.categories = [category] + let categories = await sut.getCategories(searchString: "") + XCTAssertEqual(emojiLoaderMock.categories, categories) + } + + func test_whenEmojisLoadedSecondTime_cachedValuesAreUsed() async throws { + let categoriesForFirstLoad = [EmojiCategory(id: "test", + emojis: [EmojiItem(id: "test", name: "test", keywords: ["1", "2"], skins: [try slightlySmilingFaceEmoji()])])] + let categoriesForSecondLoad = [EmojiCategory(id: "test2", + emojis: [EmojiItem(id: "test2", name: "test2", keywords: ["3", "4"], skins: [try meltingFaceEmoji()])])] + emojiLoaderMock.categories = categoriesForFirstLoad + _ = await sut.getCategories() + emojiLoaderMock.categories = categoriesForSecondLoad + let categories = await sut.getCategories() + XCTAssertEqual(categories, categoriesForFirstLoad) + } + + func test_whenEmojisSearched_correctNumberOfCategoriesReturned() async throws { + let searchString = "smile" + var categories = [EmojiCategory]() + categories.append(EmojiCategory(id: "test", + emojis: [EmojiItem(id: "\(searchString)_123", + name: "emoji0", + keywords: ["key1", "key1"], + skins: [try slightlySmilingFaceEmoji()]), + EmojiItem(id: "emoji_1", + name: searchString, + keywords: ["key1", "key1"], + skins: [try slightlySmilingFaceEmoji()]), + EmojiItem(id: "emoji_2", + name: "emoji2", + keywords: ["key1", "\(searchString)_123"], + skins: [try slightlySmilingFaceEmoji()]), + EmojiItem(id: "emoji_3", + name: "emoji_3", + keywords: ["key1", "key1"], + skins: [try slightlySmilingFaceEmoji()])])) + categories.append(EmojiCategory(id: "test", + emojis: [EmojiItem(id: "\(searchString)_123", + name: "emoji0", + keywords: ["key1", "key1"], + skins: [try slightlySmilingFaceEmoji()])])) + emojiLoaderMock.categories = categories + _ = await sut.getCategories() + let result = await sut.getCategories(searchString: searchString) + XCTAssertEqual(result.count, 2) + XCTAssertEqual(result.first?.emojis.count, 3) + } + + private func slightlySmilingFaceEmoji() throws -> EmojiItemSkin { + try XCTUnwrap(EmojiItemSkin(from: EmojiMartEmojiSkin(unified: "1f642", native: "๐Ÿ™‚"))) + } + + private func meltingFaceEmoji() throws -> EmojiItemSkin { + try XCTUnwrap(EmojiItemSkin(from: EmojiMartEmojiSkin(unified: "1fae0", native: "๐Ÿซ "))) + } +} + +private class EmojiLoaderMock: EmojiLoaderProtocol { + var categories = [ElementX.EmojiCategory]() + func load() async -> [ElementX.EmojiCategory] { + categories + } +}