Timeline Reactions: Emoji picker (#350)

* Screens template

* Loading and parsing

* Gridview

* New json

* Search method

* Unit tests

* Added emoji provider state

* Removed containsIgnoringCase

* Added plus icon, added double tap gesture, dismiss sheet after selecting emoji

* Renamed EmojisProvider to EmojiProvider

* Renamed EmojiPickerScreenScreen

* Extra padding

* Renamed EmojisLoaderProtocol

* Category names

* Moved method into public section of the class

* Return all categories when search string is empty

* Added cancel button and tittle

* Removed empty lines

* Removed swiftlint warning
This commit is contained in:
Aleksandrs Proskurins 2022-12-06 16:04:48 +02:00 committed by GitHub
parent dcedb3fb98
commit 7f91852291
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 30941 additions and 21 deletions

View File

@ -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 = "<group>"; };
0DD16CE9A66C9040B066AD60 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationControllerTests.swift; sourceTree = "<group>"; };
0E016C12585FC5A6EA6941B5 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = "<group>"; };
0E7062F88E9D5F79C8A80524 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStore.swift; sourceTree = "<group>"; };
0EE9EAF0309A2A1D67D8FAF5 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sv; path = sv.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -514,6 +535,7 @@
109361C96BFFBE2FD89BF15C /* NavigationControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControllerTests.swift; sourceTree = "<group>"; };
10CC626F97AD70FF0420C115 /* RoomSummaryProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProviderProtocol.swift; sourceTree = "<group>"; };
1113CA0A67B4AA227AAFB63B /* UserNotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationController.swift; sourceTree = "<group>"; };
11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModelProtocol.swift; sourceTree = "<group>"; };
111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = "<group>"; };
113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -586,15 +608,18 @@
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
3429142FE11930422E7CC1A0 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = "<group>"; };
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = "<group>"; };
3553C7E1218AB3EF08FFEEA4 /* EmojiMartJSONLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartJSONLoader.swift; sourceTree = "<group>"; };
3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = "<group>"; };
35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3782C506F4FF1AADF61B6212 /* tlh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tlh; path = tlh.lproj/Localizable.strings; sourceTree = "<group>"; };
37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = "<group>"; };
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = "<group>"; };
399427358A80BA2848E698A2 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-MX"; path = "es-MX.lproj/Localizable.strings"; sourceTree = "<group>"; };
39EBB6903EFD4236B8D11A42 /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3ACBDC1D28EFB7789EB467E0 /* MockRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomProxy.swift; sourceTree = "<group>"; };
3B5B535DA49C54523FF7A412 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nn; path = nn.lproj/Localizable.strings; sourceTree = "<group>"; };
3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = "<group>"; };
3CDF9E55650D6035D6536538 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "nb-NO"; path = "nb-NO.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelTests.swift; sourceTree = "<group>"; };
3D4DD336905C72F95EAF34B7 /* ElementX-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ElementX-Bridging-Header.h"; sourceTree = "<group>"; };
@ -662,8 +687,15 @@
5773C86AF04AEF26515AD00C /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = "<group>"; };
57F95CADD0A5DBD76B990FCB /* ServiceLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocator.swift; sourceTree = "<group>"; };
5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModelProtocol.swift; sourceTree = "<group>"; };
5D04B17829378AB300FD5B00 /* apple_emojis_data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apple_emojis_data.json; sourceTree = "<group>"; };
5D04B17A29378D3600FD5B00 /* EmojiMartEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartEmoji.swift; sourceTree = "<group>"; };
5D04B17C2937ADE300FD5B00 /* EmojiItemSkin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemSkin.swift; sourceTree = "<group>"; };
5D04B17E293A333600FD5B00 /* EmojiPickerSearchFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerSearchFieldView.swift; sourceTree = "<group>"; };
5D04B180293A337400FD5B00 /* EmojiPickerHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerHeaderView.swift; sourceTree = "<group>"; };
5D26A086A8278D39B5756D6F /* project.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = project.yml; sourceTree = "<group>"; };
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinatorUITests.swift; sourceTree = "<group>"; };
5DE2282B293F29FC001790FD /* EmojiProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiProvider.swift; sourceTree = "<group>"; };
5DE2282D293F2CF6001790FD /* EmojiLoaderProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiLoaderProtocol.swift; sourceTree = "<group>"; };
5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = "<group>"; };
5FF214969B25BFCBF87B908B /* bn-BD */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "bn-BD"; path = "bn-BD.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@ -762,6 +794,7 @@
9A68BCE6438873D2661D93D0 /* BugReportServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportServiceProtocol.swift; sourceTree = "<group>"; };
9B1FBF8CA40199B8058B1F08 /* NotificationItemProxy+NSE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationItemProxy+NSE.swift"; sourceTree = "<group>"; };
9B577F829C693B8DFB7014FD /* RedactedRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedactedRoomTimelineItem.swift; sourceTree = "<group>"; };
9BF9E3E6A23180EC05F06460 /* EmojiMartJSONLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartJSONLoaderTests.swift; sourceTree = "<group>"; };
9C4048041C1A6B20CB97FD18 /* TestMeasurementParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMeasurementParser.swift; sourceTree = "<group>"; };
9C5E81214D27A6B898FC397D /* ElementX.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ElementX.entitlements; sourceTree = "<group>"; };
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 = "<group>"; };
BC9B05D6B293A039EB963CA7 /* az */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = az; path = az.lproj/Localizable.strings; sourceTree = "<group>"; };
BE6C10032A77AE7DC5AA4C50 /* MessageComposerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposerTextField.swift; sourceTree = "<group>"; };
BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = "<group>"; };
BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = "<group>"; };
BEE6BF9BA63FF42F8AF6EEEA /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = sr; path = sr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
C00A7110B937C6AE2EF5D7D6 /* FileRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineItem.swift; sourceTree = "<group>"; };
@ -842,6 +876,7 @@
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
C1198B925F4A88DA74083662 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = "<group>"; };
C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = "<group>"; };
C38AE3617D7619EF30CDD229 /* EmojiMartStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartStore.swift; sourceTree = "<group>"; };
C3F652E88106B855A2A55ADE /* FilePreviewViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewViewModelProtocol.swift; sourceTree = "<group>"; };
C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = "<group>"; };
C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
@ -906,6 +941,7 @@
E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = "<group>"; };
E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarSize.swift; sourceTree = "<group>"; };
E26747B3154A5DBC3A7E24A5 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = "<group>"; };
E31F530ED515C10A5915D1B9 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; };
E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = "<group>"; };
@ -914,6 +950,7 @@
E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = "<group>"; };
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
E579A0DA01F488C97B771EF6 /* lv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = lv; path = lv.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
E5C3AACCAA82392D08924496 /* EmojiMartCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartCategory.swift; sourceTree = "<group>"; };
E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreen.swift; sourceTree = "<group>"; };
E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = "<group>"; };
E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@ -938,11 +975,13 @@
F03C9D319676F3C0DC6B0203 /* ScreenshotDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotDetectorTests.swift; sourceTree = "<group>"; };
F0E7BF8F7BB1021F889C6483 /* MockBugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBugReportService.swift; sourceTree = "<group>"; };
F15BE37BE2FB86E00C8D150A /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = "<group>"; };
F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenModels.swift; sourceTree = "<group>"; };
F23BA6D4842D53C5AC9B7584 /* nn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nn; path = nn.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
F2D58333B377888012740101 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = "<group>"; };
F31A4E5941ACBA4BB9FEF94C /* UserNotificationToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationToastView.swift; sourceTree = "<group>"; };
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegexTests.swift; sourceTree = "<group>"; };
F3648F2FADEF2672D6A0D489 /* FileCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileCacheTests.swift; sourceTree = "<group>"; };
F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModel.swift; sourceTree = "<group>"; };
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 = "<group>"; };
F6A8C632CEF4600107792899 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
@ -951,6 +990,7 @@
F77C060C2ACC4CB7336A29E7 /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = "<group>"; };
F9E785D5137510481733A3E8 /* TextRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineView.swift; sourceTree = "<group>"; };
FA154570F693D93513E584C1 /* RoomMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMessageFactory.swift; sourceTree = "<group>"; };
FA4807E660393F385D673DA2 /* TimelineItemReactionsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReactionsMenuView.swift; sourceTree = "<group>"; };
FAB10E673916D2B8D21FD197 /* TemplateModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateModels.swift; sourceTree = "<group>"; };
FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorProtocol.swift; sourceTree = "<group>"; };
FC3D31C2DA6910AA0079678A /* MediaProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProxyProtocol.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
323160803A296713F839540B /* View */ = {
isa = PBXGroup;
children = (
0E016C12585FC5A6EA6941B5 /* EmojiPickerScreen.swift */,
5D04B17E293A333600FD5B00 /* EmojiPickerSearchFieldView.swift */,
5D04B180293A337400FD5B00 /* EmojiPickerHeaderView.swift */,
);
path = View;
sourceTree = "<group>";
};
328DD5DA1281F758B72006C7 /* Views */ = {
isa = PBXGroup;
children = (
@ -1215,6 +1266,19 @@
path = ServerSelection;
sourceTree = "<group>";
};
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 = "<group>";
};
3A304097A59704AC9B869EC6 /* Helpers */ = {
isa = PBXGroup;
children = (
@ -1438,6 +1502,18 @@
path = TimeLineItemContent;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
F5A65D1D3B83593598DC278D /* EmojiPickerScreen */ = {
isa = PBXGroup;
children = (
BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */,
F174A5627CDB3CAF280D1880 /* EmojiPickerScreenModels.swift */,
F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */,
11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */,
323160803A296713F839540B /* View */,
);
path = EmojiPickerScreen;
sourceTree = "<group>";
};
FC26FB522EDED4965C5325F0 /* Folder */ = {
isa = PBXGroup;
children = (
);
path = Folder;
sourceTree = "<group>";
};
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 */,

View File

@ -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";

View File

@ -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

View File

@ -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]))
}
}

View File

@ -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
}

View File

@ -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<EmojiPickerScreenViewState, EmojiPickerScreenViewAction>
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)
}
}
}

View File

@ -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 }
}

View File

@ -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: "")
}
}
}

View File

@ -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)
}
}

View File

@ -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(""))
}
}

View File

@ -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)
}
}

View File

@ -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)?

View File

@ -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:

View File

@ -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

View File

@ -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)
}
}

View File

@ -102,6 +102,12 @@ struct TimelineTableView: UIViewRepresentable {
}
}
var displayReactionsMenuForItemId = "" {
didSet {
tableView?.reloadData()
}
}
/// The table's diffable data source.
private var dataSource: UITableViewDiffableDataSource<TimelineSection, RoomTimelineViewProvider>?
private var cancellables: Set<AnyCancellable> = []
@ -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

View File

@ -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]
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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]
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,76 @@
//
// 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 EmojiProviderProtocol {
func getCategories(searchString: String?) async -> [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
}
}
}

View File

@ -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

View File

@ -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())

View File

@ -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)
}
}

View File

@ -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
}
}