Fix #1934 - Hook reaction pickers into the system's recently used keyboard emojis (#3453)

This commit is contained in:
Stefan Ceriu 2024-10-25 19:58:56 +03:00 committed by GitHub
parent 35cbc84a99
commit 7a47e37d38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 274 additions and 133 deletions

View File

@ -511,7 +511,6 @@
71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; };
733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; }; 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; };
7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; };
7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; };
73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; }; 73F33E9776B7A50B65A031D2 /* AppLockSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0BA67B3E4EF9D29D14A78CE /* AppLockSettingsScreenViewModelTests.swift */; };
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; }; 73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */; };
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; }; 7405B4824D45BA7C3D943E76 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D0CBC76C80E04345E11F2DB /* Application.swift */; };
@ -764,6 +763,7 @@
A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; };
A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */; }; A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.swift */; };
A52090A4FE0DB826578DFC03 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0724EBDFE8BB4C9E5547C57D /* Client.swift */; }; A52090A4FE0DB826578DFC03 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0724EBDFE8BB4C9E5547C57D /* Client.swift */; };
A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */; };
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; };
A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; };
A64B52D9F73F9A6B95AF24FE /* UserDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */; }; A64B52D9F73F9A6B95AF24FE /* UserDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CD503F5E0938FE53C7C6E7 /* UserDetailsEditScreenCoordinator.swift */; };
@ -969,7 +969,6 @@
D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; }; D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; };
D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.swift */; }; D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.swift */; };
D5B1531A72387D432939D4E0 /* RoomDirectorySearchProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */; }; D5B1531A72387D432939D4E0 /* RoomDirectorySearchProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */; };
D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */; };
D5E771132BB36240DE38102F /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; D5E771132BB36240DE38102F /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; };
D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */; }; D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */; };
D6152E21036B88C44ECB22E7 /* EncryptionResetPasswordScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */; }; D6152E21036B88C44ECB22E7 /* EncryptionResetPasswordScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */; };
@ -1455,7 +1454,6 @@
36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; }; 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = "<group>"; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = "<group>"; };
376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerURLBuildersTests.swift; sourceTree = "<group>"; }; 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerURLBuildersTests.swift; sourceTree = "<group>"; };
37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = "<group>"; };
37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = "<group>"; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = "<group>"; };
37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = "<group>"; }; 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = "<group>"; };
37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreen.swift; sourceTree = "<group>"; }; 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreen.swift; sourceTree = "<group>"; };
@ -1474,7 +1472,6 @@
3BAC027034248429A438886B /* AppMediatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorMock.swift; sourceTree = "<group>"; }; 3BAC027034248429A438886B /* AppMediatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorMock.swift; sourceTree = "<group>"; };
3BC1B7CB061C9865B2B91B56 /* QRCodeLoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModel.swift; sourceTree = "<group>"; }; 3BC1B7CB061C9865B2B91B56 /* QRCodeLoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModel.swift; sourceTree = "<group>"; };
3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.swift; sourceTree = "<group>"; }; 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.swift; sourceTree = "<group>"; };
3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = "<group>"; };
3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = "<group>"; }; 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = "<group>"; };
3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = "<group>"; }; 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = "<group>"; };
3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = "<group>"; }; 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = "<group>"; };
@ -1815,6 +1812,7 @@
8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = "<group>"; }; 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = "<group>"; };
8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = "<group>"; }; 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = "<group>"; };
8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = "<group>"; }; 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = "<group>"; };
8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderProtocol.swift; sourceTree = "<group>"; };
8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = "<group>"; }; 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = "<group>"; };
8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = "<group>"; }; 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = "<group>"; };
8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = "<group>"; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = "<group>"; };
@ -3080,10 +3078,9 @@
39557ADF21345E18F3865B9E /* Emojis */ = { 39557ADF21345E18F3865B9E /* Emojis */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */,
37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */,
201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */, 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */,
6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */, 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */,
8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */,
); );
path = Emojis; path = Emojis;
sourceTree = "<group>"; sourceTree = "<group>";
@ -6448,9 +6445,7 @@
370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */, 370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */,
3F997171C3C79A45E92BF9EF /* ElementWellKnown.swift in Sources */, 3F997171C3C79A45E92BF9EF /* ElementWellKnown.swift in Sources */,
7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */,
7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */,
E45C9FA22BC13B477FD3B4AC /* EmojiDetection.swift in Sources */, E45C9FA22BC13B477FD3B4AC /* EmojiDetection.swift in Sources */,
D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */,
3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */, 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */,
340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */, 340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */,
C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */, C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */,
@ -6459,6 +6454,7 @@
2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */, 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */,
1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */, 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */,
FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */, FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */,
A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */,
5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */, 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */,
8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */, 8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */,
661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */, 661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */,

View File

@ -139,6 +139,7 @@
"common_edited_suffix" = "(edited)"; "common_edited_suffix" = "(edited)";
"common_editing" = "Editing"; "common_editing" = "Editing";
"common_emote" = "* %1$@ %2$@"; "common_emote" = "* %1$@ %2$@";
"common_encryption" = "Encryption";
"common_encryption_enabled" = "Encryption enabled"; "common_encryption_enabled" = "Encryption enabled";
"common_enter_your_pin" = "Enter your PIN"; "common_enter_your_pin" = "Enter your PIN";
"common_error" = "Error"; "common_error" = "Error";
@ -149,6 +150,7 @@
"common_favourited" = "Favourited"; "common_favourited" = "Favourited";
"common_file" = "File"; "common_file" = "File";
"common_forward_message" = "Forward message"; "common_forward_message" = "Forward message";
"common_frequently_used" = "Frequently used";
"common_gif" = "GIF"; "common_gif" = "GIF";
"common_image" = "Image"; "common_image" = "Image";
"common_in_reply_to" = "In reply to %1$@"; "common_in_reply_to" = "In reply to %1$@";
@ -342,6 +344,9 @@
"screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL"; "screen_advanced_settings_element_call_base_url" = "Custom Element Call base URL";
"screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call."; "screen_advanced_settings_element_call_base_url_description" = "Set a custom base URL for Element Call.";
"screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address."; "screen_advanced_settings_element_call_base_url_validation_error" = "Invalid URL, please make sure you include the protocol (http/https) and the correct address.";
"screen_create_room_room_address_section_footer" = "In order for this room to be visible in the public room directory, you will need a room address.";
"screen_create_room_room_address_section_title" = "Room address";
"screen_create_room_room_visibility_section_title" = "Room visibility";
"screen_create_room_access_section_anyone_option_description" = "Anyone can join this room"; "screen_create_room_access_section_anyone_option_description" = "Anyone can join this room";
"screen_create_room_access_section_anyone_option_title" = "Anyone"; "screen_create_room_access_section_anyone_option_title" = "Anyone";
"screen_create_room_access_section_header" = "Room Access"; "screen_create_room_access_section_header" = "Room Access";
@ -449,9 +454,12 @@
"screen_change_server_title" = "Select your server"; "screen_change_server_title" = "Select your server";
"screen_chat_backup_key_backup_action_disable" = "Turn off backup"; "screen_chat_backup_key_backup_action_disable" = "Turn off backup";
"screen_chat_backup_key_backup_action_enable" = "Turn on backup"; "screen_chat_backup_key_backup_action_enable" = "Turn on backup";
"screen_chat_backup_key_backup_description" = "Backup ensures that you don't lose your message history. %1$@."; "screen_chat_backup_key_backup_description" = "Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@.";
"screen_chat_backup_key_backup_title" = "Backup"; "screen_chat_backup_key_backup_title" = "Key storage";
"screen_chat_backup_key_storage_toggle_description" = "Upload keys from this device";
"screen_chat_backup_key_storage_toggle_title" = "Allow key storage";
"screen_chat_backup_recovery_action_change" = "Change recovery key"; "screen_chat_backup_recovery_action_change" = "Change recovery key";
"screen_chat_backup_recovery_action_change_description" = "Recover your cryptographic identity and message history with a recovery key if youve lost all your existing devices.";
"screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync."; "screen_chat_backup_recovery_action_confirm_description" = "Your chat backup is currently out of sync.";
"screen_chat_backup_recovery_action_setup" = "Set up recovery"; "screen_chat_backup_recovery_action_setup" = "Set up recovery";
"screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere."; "screen_chat_backup_recovery_action_setup_description" = "Get access to your encrypted messages if you lose all your devices or are signed out of %1$@ everywhere.";
@ -473,10 +481,10 @@
"screen_create_poll_title" = "Create Poll"; "screen_create_poll_title" = "Create Poll";
"screen_create_room_action_create_room" = "New room"; "screen_create_room_action_create_room" = "New room";
"screen_create_room_error_creating_room" = "An error occurred when creating the room"; "screen_create_room_error_creating_room" = "An error occurred when creating the room";
"screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption cant be disabled afterwards."; "screen_create_room_private_option_description" = "Only people invited can access this room. All messages are end-to-end encrypted.";
"screen_create_room_private_option_title" = "Private room (invite only)"; "screen_create_room_private_option_title" = "Private room";
"screen_create_room_public_option_description" = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date."; "screen_create_room_public_option_description" = "Anyone can find this room.\nYou can change this anytime in room settings.";
"screen_create_room_public_option_title" = "Public room (anyone)"; "screen_create_room_public_option_title" = "Public room";
"screen_create_room_topic_label" = "Topic (optional)"; "screen_create_room_topic_label" = "Topic (optional)";
"screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; "screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone.";
"screen_deactivate_account_delete_all_messages" = "Delete all my messages"; "screen_deactivate_account_delete_all_messages" = "Delete all my messages";
@ -624,7 +632,6 @@
"screen_qr_code_login_verify_code_title" = "Your verification code"; "screen_qr_code_login_verify_code_title" = "Your verification code";
"screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work."; "screen_recovery_key_change_description" = "Get a new recovery key if you've lost your existing one. After changing your recovery key, your old one will no longer work.";
"screen_recovery_key_change_generate_key" = "Generate a new recovery key"; "screen_recovery_key_change_generate_key" = "Generate a new recovery key";
"screen_recovery_key_change_generate_key_description" = "Make sure you can store your recovery key somewhere safe";
"screen_recovery_key_change_success" = "Recovery key changed"; "screen_recovery_key_change_success" = "Recovery key changed";
"screen_recovery_key_change_title" = "Change recovery key?"; "screen_recovery_key_change_title" = "Change recovery key?";
"screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key"; "screen_recovery_key_confirm_create_new_recovery_key" = "Create new recovery key";
@ -638,14 +645,14 @@
"screen_recovery_key_copied_to_clipboard" = "Copied recovery key"; "screen_recovery_key_copied_to_clipboard" = "Copied recovery key";
"screen_recovery_key_generating_key" = "Generating…"; "screen_recovery_key_generating_key" = "Generating…";
"screen_recovery_key_save_action" = "Save recovery key"; "screen_recovery_key_save_action" = "Save recovery key";
"screen_recovery_key_save_description" = "Write down your recovery key somewhere safe or save it in a password manager."; "screen_recovery_key_save_description" = "Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe.";
"screen_recovery_key_save_key_description" = "Tap to copy recovery key"; "screen_recovery_key_save_key_description" = "Tap to copy recovery key";
"screen_recovery_key_save_title" = "Save your recovery key"; "screen_recovery_key_save_title" = "Save your recovery key somewhere safe";
"screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step."; "screen_recovery_key_setup_confirmation_description" = "You will not be able to access your new recovery key after this step.";
"screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?"; "screen_recovery_key_setup_confirmation_title" = "Have you saved your recovery key?";
"screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting Change recovery key."; "screen_recovery_key_setup_description" = "Your chat backup is protected by a recovery key. If you need a new recovery key after setup you can recreate by selecting Change recovery key.";
"screen_recovery_key_setup_generate_key" = "Generate your recovery key"; "screen_recovery_key_setup_generate_key" = "Generate your recovery key";
"screen_recovery_key_setup_generate_key_description" = "Make sure you can store your recovery key somewhere safe"; "screen_recovery_key_setup_generate_key_description" = "Do not share this with anyone!";
"screen_recovery_key_setup_success" = "Recovery setup successful"; "screen_recovery_key_setup_success" = "Recovery setup successful";
"screen_recovery_key_setup_title" = "Set up recovery"; "screen_recovery_key_setup_title" = "Set up recovery";
"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; "screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user";
@ -1019,6 +1026,7 @@
"screen_login_subtitle" = "Matrix is an open network for secure, decentralised communication."; "screen_login_subtitle" = "Matrix is an open network for secure, decentralised communication.";
"screen_notification_settings_mentions_section_title" = "Mentions"; "screen_notification_settings_mentions_section_title" = "Mentions";
"screen_qr_code_login_invalid_scan_state_retry_button" = "Try again"; "screen_qr_code_login_invalid_scan_state_retry_button" = "Try again";
"screen_recovery_key_change_generate_key_description" = "Do not share this with anyone!";
"screen_recovery_key_confirm_title" = "Enter your recovery key"; "screen_recovery_key_confirm_title" = "Enter your recovery key";
"screen_report_content_block_user" = "Block user"; "screen_report_content_block_user" = "Block user";
"screen_reset_encryption_password_placeholder" = "Enter…"; "screen_reset_encryption_password_placeholder" = "Enter…";

View File

@ -48,6 +48,7 @@ final class AppSettings {
case enableOnlySignedDeviceIsolationMode case enableOnlySignedDeviceIsolationMode
case identityPinningViolationNotificationsEnabled case identityPinningViolationNotificationsEnabled
case knockingEnabled case knockingEnabled
case frequentEmojisEnabled
} }
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
@ -290,6 +291,9 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store)) @UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store))
var knockingEnabled var knockingEnabled
@UserPreference(key: UserDefaultsKeys.frequentEmojisEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store))
var frequentEmojisEnabled
#endif #endif
// MARK: - Shared // MARK: - Shared

View File

@ -22,6 +22,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
private let roomProxy: JoinedRoomProxyProtocol private let roomProxy: JoinedRoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol private let userIndicatorController: UserIndicatorControllerProtocol
private let appMediator: AppMediatorProtocol private let appMediator: AppMediatorProtocol
private let emojiProvider: EmojiProviderProtocol
private let actionsSubject: PassthroughSubject<PinnedEventsTimelineFlowCoordinatorAction, Never> = .init() private let actionsSubject: PassthroughSubject<PinnedEventsTimelineFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<PinnedEventsTimelineFlowCoordinatorAction, Never> { var actionsPublisher: AnyPublisher<PinnedEventsTimelineFlowCoordinatorAction, Never> {
@ -35,13 +36,15 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol, roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
roomProxy: JoinedRoomProxyProtocol, roomProxy: JoinedRoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol,
appMediator: AppMediatorProtocol) { appMediator: AppMediatorProtocol,
emojiProvider: EmojiProviderProtocol) {
self.navigationStackCoordinator = navigationStackCoordinator self.navigationStackCoordinator = navigationStackCoordinator
self.userSession = userSession self.userSession = userSession
self.roomTimelineControllerFactory = roomTimelineControllerFactory self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.appMediator = appMediator self.appMediator = appMediator
self.emojiProvider = emojiProvider
} }
func start() { func start() {
@ -71,7 +74,8 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
mediaProvider: userSession.mediaProvider, mediaProvider: userSession.mediaProvider,
mediaPlayerProvider: MediaPlayerProvider(), mediaPlayerProvider: MediaPlayerProvider(),
voiceMessageMediaManager: userSession.voiceMessageMediaManager, voiceMessageMediaManager: userSession.voiceMessageMediaManager,
appMediator: appMediator)) appMediator: appMediator,
emojiProvider: emojiProvider))
coordinator.actions coordinator.actions
.sink { [weak self] action in .sink { [weak self] action in

View File

@ -1340,7 +1340,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: roomTimelineControllerFactory, roomTimelineControllerFactory: roomTimelineControllerFactory,
roomProxy: roomProxy, roomProxy: roomProxy,
userIndicatorController: userIndicatorController, userIndicatorController: userIndicatorController,
appMediator: appMediator) appMediator: appMediator,
emojiProvider: emojiProvider)
coordinator.actionsPublisher.sink { [weak self] action in coordinator.actionsPublisher.sink { [weak self] action in
guard let self else { guard let self else {

View File

@ -492,7 +492,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
isChildFlow: false, isChildFlow: false,
roomTimelineControllerFactory: roomTimelineControllerFactory, roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: detailNavigationStackCoordinator, navigationStackCoordinator: detailNavigationStackCoordinator,
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: appSettings),
ongoingCallRoomIDPublisher: elementCallService.ongoingCallRoomIDPublisher, ongoingCallRoomIDPublisher: elementCallService.ongoingCallRoomIDPublisher,
appMediator: appMediator, appMediator: appMediator,
appSettings: appSettings, appSettings: appSettings,

View File

@ -312,6 +312,8 @@ internal enum L10n {
internal static func commonEmote(_ p1: Any, _ p2: Any) -> String { internal static func commonEmote(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "common_emote", String(describing: p1), String(describing: p2)) return L10n.tr("Localizable", "common_emote", String(describing: p1), String(describing: p2))
} }
/// Encryption
internal static var commonEncryption: String { return L10n.tr("Localizable", "common_encryption") }
/// Encryption enabled /// Encryption enabled
internal static var commonEncryptionEnabled: String { return L10n.tr("Localizable", "common_encryption_enabled") } internal static var commonEncryptionEnabled: String { return L10n.tr("Localizable", "common_encryption_enabled") }
/// Enter your PIN /// Enter your PIN
@ -332,6 +334,8 @@ internal enum L10n {
internal static var commonFile: String { return L10n.tr("Localizable", "common_file") } internal static var commonFile: String { return L10n.tr("Localizable", "common_file") }
/// Forward message /// Forward message
internal static var commonForwardMessage: String { return L10n.tr("Localizable", "common_forward_message") } internal static var commonForwardMessage: String { return L10n.tr("Localizable", "common_forward_message") }
/// Frequently used
internal static var commonFrequentlyUsed: String { return L10n.tr("Localizable", "common_frequently_used") }
/// GIF /// GIF
internal static var commonGif: String { return L10n.tr("Localizable", "common_gif") } internal static var commonGif: String { return L10n.tr("Localizable", "common_gif") }
/// Image /// Image
@ -1005,14 +1009,20 @@ internal enum L10n {
internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") } internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") }
/// Turn on backup /// Turn on backup
internal static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") } internal static var screenChatBackupKeyBackupActionEnable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_enable") }
/// Backup ensures that you don't lose your message history. %1$@. /// Store your cryptographic identity and message keys securely on the server. This will allow you to view your message history on any new devices. %1$@.
internal static func screenChatBackupKeyBackupDescription(_ p1: Any) -> String { internal static func screenChatBackupKeyBackupDescription(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_chat_backup_key_backup_description", String(describing: p1)) return L10n.tr("Localizable", "screen_chat_backup_key_backup_description", String(describing: p1))
} }
/// Backup /// Key storage
internal static var screenChatBackupKeyBackupTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_title") } internal static var screenChatBackupKeyBackupTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_title") }
/// Upload keys from this device
internal static var screenChatBackupKeyStorageToggleDescription: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_toggle_description") }
/// Allow key storage
internal static var screenChatBackupKeyStorageToggleTitle: String { return L10n.tr("Localizable", "screen_chat_backup_key_storage_toggle_title") }
/// Change recovery key /// Change recovery key
internal static var screenChatBackupRecoveryActionChange: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change") } internal static var screenChatBackupRecoveryActionChange: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change") }
/// Recover your cryptographic identity and message history with a recovery key if youve lost all your existing devices.
internal static var screenChatBackupRecoveryActionChangeDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change_description") }
/// Enter recovery key /// Enter recovery key
internal static var screenChatBackupRecoveryActionConfirm: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm") } internal static var screenChatBackupRecoveryActionConfirm: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm") }
/// Your chat backup is currently out of sync. /// Your chat backup is currently out of sync.
@ -1079,16 +1089,23 @@ internal enum L10n {
internal static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") } internal static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") }
/// An error occurred when creating the room /// An error occurred when creating the room
internal static var screenCreateRoomErrorCreatingRoom: String { return L10n.tr("Localizable", "screen_create_room_error_creating_room") } internal static var screenCreateRoomErrorCreatingRoom: String { return L10n.tr("Localizable", "screen_create_room_error_creating_room") }
/// Messages in this room are encrypted. Encryption cant be disabled afterwards. /// Only people invited can access this room. All messages are end-to-end encrypted.
internal static var screenCreateRoomPrivateOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_private_option_description") } internal static var screenCreateRoomPrivateOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_private_option_description") }
/// Private room (invite only) /// Private room
internal static var screenCreateRoomPrivateOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_private_option_title") } internal static var screenCreateRoomPrivateOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_private_option_title") }
/// Messages are not encrypted and anyone can read them. You can enable encryption at a later date. /// Anyone can find this room.
/// You can change this anytime in room settings.
internal static var screenCreateRoomPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_public_option_description") } internal static var screenCreateRoomPublicOptionDescription: String { return L10n.tr("Localizable", "screen_create_room_public_option_description") }
/// Public room (anyone) /// Public room
internal static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") } internal static var screenCreateRoomPublicOptionTitle: String { return L10n.tr("Localizable", "screen_create_room_public_option_title") }
/// In order for this room to be visible in the public room directory, you will need a room address.
internal static var screenCreateRoomRoomAddressSectionFooter: String { return L10n.tr("Localizable", "screen_create_room_room_address_section_footer") }
/// Room address
internal static var screenCreateRoomRoomAddressSectionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_address_section_title") }
/// Room name /// Room name
internal static var screenCreateRoomRoomNameLabel: String { return L10n.tr("Localizable", "screen_create_room_room_name_label") } internal static var screenCreateRoomRoomNameLabel: String { return L10n.tr("Localizable", "screen_create_room_room_name_label") }
/// Room visibility
internal static var screenCreateRoomRoomVisibilitySectionTitle: String { return L10n.tr("Localizable", "screen_create_room_room_visibility_section_title") }
/// Create a room /// Create a room
internal static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") } internal static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") }
/// Topic (optional) /// Topic (optional)
@ -1475,7 +1492,7 @@ internal enum L10n {
internal static var screenRecoveryKeyChangeDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_description") } internal static var screenRecoveryKeyChangeDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_description") }
/// Generate a new recovery key /// Generate a new recovery key
internal static var screenRecoveryKeyChangeGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key") } internal static var screenRecoveryKeyChangeGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key") }
/// Make sure you can store your recovery key somewhere safe /// Do not share this with anyone!
internal static var screenRecoveryKeyChangeGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key_description") } internal static var screenRecoveryKeyChangeGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_generate_key_description") }
/// Recovery key changed /// Recovery key changed
internal static var screenRecoveryKeyChangeSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_change_success") } internal static var screenRecoveryKeyChangeSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_change_success") }
@ -1505,11 +1522,11 @@ internal enum L10n {
internal static var screenRecoveryKeyGeneratingKey: String { return L10n.tr("Localizable", "screen_recovery_key_generating_key") } internal static var screenRecoveryKeyGeneratingKey: String { return L10n.tr("Localizable", "screen_recovery_key_generating_key") }
/// Save recovery key /// Save recovery key
internal static var screenRecoveryKeySaveAction: String { return L10n.tr("Localizable", "screen_recovery_key_save_action") } internal static var screenRecoveryKeySaveAction: String { return L10n.tr("Localizable", "screen_recovery_key_save_action") }
/// Write down your recovery key somewhere safe or save it in a password manager. /// Write down this recovery key somewhere safe, like a password manager, encrypted note, or a physical safe.
internal static var screenRecoveryKeySaveDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_description") } internal static var screenRecoveryKeySaveDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_description") }
/// Tap to copy recovery key /// Tap to copy recovery key
internal static var screenRecoveryKeySaveKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_key_description") } internal static var screenRecoveryKeySaveKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_save_key_description") }
/// Save your recovery key /// Save your recovery key somewhere safe
internal static var screenRecoveryKeySaveTitle: String { return L10n.tr("Localizable", "screen_recovery_key_save_title") } internal static var screenRecoveryKeySaveTitle: String { return L10n.tr("Localizable", "screen_recovery_key_save_title") }
/// You will not be able to access your new recovery key after this step. /// You will not be able to access your new recovery key after this step.
internal static var screenRecoveryKeySetupConfirmationDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_description") } internal static var screenRecoveryKeySetupConfirmationDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_confirmation_description") }
@ -1519,7 +1536,7 @@ internal enum L10n {
internal static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") } internal static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") }
/// Generate your recovery key /// Generate your recovery key
internal static var screenRecoveryKeySetupGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key") } internal static var screenRecoveryKeySetupGenerateKey: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key") }
/// Make sure you can store your recovery key somewhere safe /// Do not share this with anyone!
internal static var screenRecoveryKeySetupGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key_description") } internal static var screenRecoveryKeySetupGenerateKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_generate_key_description") }
/// Recovery setup successful /// Recovery setup successful
internal static var screenRecoveryKeySetupSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_setup_success") } internal static var screenRecoveryKeySetupSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_setup_success") }

View File

@ -44,6 +44,8 @@ struct EmojiPickerEmojiCategoryViewData: Identifiable {
return L10n.emojiPickerCategorySymbols return L10n.emojiPickerCategorySymbols
case "flags": case "flags":
return L10n.emojiPickerCategoryFlags return L10n.emojiPickerCategoryFlags
case EmojiCategory.frequentlyUsedCategoryIdentifier:
return L10n.commonFrequentlyUsed
default: default:
MXLog.failure("Missing translation for emoji category with id \(id)") MXLog.failure("Missing translation for emoji category with id \(id)")
return "" return ""

View File

@ -36,6 +36,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr
state.categories = convert(emojiCategories: categories) state.categories = convert(emojiCategories: categories)
} }
case let .emojiTapped(emoji: emoji): case let .emojiTapped(emoji: emoji):
emojiProvider.markEmojiAsFrequentlyUsed(emoji.value)
actionsSubject.send(.emojiSelected(emoji: emoji.value)) actionsSubject.send(.emojiSelected(emoji: emoji.value))
case .dismiss: case .dismiss:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)

View File

@ -81,7 +81,7 @@ struct EmojiPickerScreen: View {
// MARK: - Previews // MARK: - Previews
struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview { struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()) static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
static var previews: some View { static var previews: some View {
EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"]) EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"])
@ -91,7 +91,7 @@ struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
} }
struct EmojiPickerScreenSheet_Previews: PreviewProvider { struct EmojiPickerScreenSheet_Previews: PreviewProvider {
static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider()) static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
static var previews: some View { static var previews: some View {
Text("Timeline view") Text("Timeline view")

View File

@ -15,6 +15,7 @@ struct PinnedEventsTimelineScreenCoordinatorParameters {
let mediaPlayerProvider: MediaPlayerProviderProtocol let mediaPlayerProvider: MediaPlayerProviderProtocol
let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol
let appMediator: AppMediatorProtocol let appMediator: AppMediatorProtocol
let emojiProvider: EmojiProviderProtocol
} }
enum PinnedEventsTimelineScreenCoordinatorAction { enum PinnedEventsTimelineScreenCoordinatorAction {
@ -49,7 +50,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: parameters.appMediator, appMediator: parameters.appMediator,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: parameters.emojiProvider)
} }
func start() { func start() {

View File

@ -37,7 +37,8 @@ struct PinnedEventsTimelineScreen: View {
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline) isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline,
emojiProvider: timelineContext.viewState.emojiProvider)
.makeActions() .makeActions()
if let actions { if let actions {
TimelineItemMenu(item: info.item, actions: actions) TimelineItemMenu(item: info.item, actions: actions)
@ -96,7 +97,8 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview {
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
}() }()
static var previews: some View { static var previews: some View {

View File

@ -81,7 +81,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: parameters.appMediator, appMediator: parameters.appMediator,
appSettings: parameters.appSettings, appSettings: parameters.appSettings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: parameters.emojiProvider)
wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight,
maxCompressedHeight: ComposerConstant.maxHeight, maxCompressedHeight: ComposerConstant.maxHeight,

View File

@ -76,7 +76,8 @@ struct RoomScreen: View {
pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, pinnedEventIDs: timelineContext.viewState.pinnedEventIDs,
isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isDM: timelineContext.viewState.isEncryptedOneToOneRoom,
isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled,
isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline) isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline,
emojiProvider: timelineContext.viewState.emojiProvider)
.makeActions() .makeActions()
if let actions { if let actions {
TimelineItemMenu(item: info.item, actions: actions) TimelineItemMenu(item: info.item, actions: actions)
@ -229,7 +230,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {

View File

@ -51,6 +51,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var elementCallBaseURLOverride: URL? { get set } var elementCallBaseURLOverride: URL? { get set }
var identityPinningViolationNotificationsEnabled: Bool { get set } var identityPinningViolationNotificationsEnabled: Bool { get set }
var knockingEnabled: Bool { get set } var knockingEnabled: Bool { get set }
var frequentEmojisEnabled: Bool { get set }
} }
extension AppSettings: DeveloperOptionsProtocol { } extension AppSettings: DeveloperOptionsProtocol { }

View File

@ -53,6 +53,10 @@ struct DeveloperOptionsScreen: View {
Toggle(isOn: $context.identityPinningViolationNotificationsEnabled) { Toggle(isOn: $context.identityPinningViolationNotificationsEnabled) {
Text("Identity pinning violation notifications") Text("Identity pinning violation notifications")
} }
Toggle(isOn: $context.frequentEmojisEnabled) {
Text("Show frequently used emojis")
}
} }
Section("Join rules") { Section("Join rules") {

View File

@ -111,6 +111,8 @@ struct TimelineViewState: BindableState {
/// A closure providing the associated audio player state for an item in the timeline. /// A closure providing the associated audio player state for an item in the timeline.
var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)? var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)?
var emojiProvider: EmojiProviderProtocol
} }
struct TimelineViewStateBindings { struct TimelineViewStateBindings {

View File

@ -28,6 +28,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
private let appMediator: AppMediatorProtocol private let appMediator: AppMediatorProtocol
private let appSettings: AppSettings private let appSettings: AppSettings
private let analyticsService: AnalyticsService private let analyticsService: AnalyticsService
private let emojiProvider: EmojiProviderProtocol
private let timelineInteractionHandler: TimelineInteractionHandler private let timelineInteractionHandler: TimelineInteractionHandler
@ -50,7 +51,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
userIndicatorController: UserIndicatorControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol,
appMediator: AppMediatorProtocol, appMediator: AppMediatorProtocol,
appSettings: AppSettings, appSettings: AppSettings,
analyticsService: AnalyticsService) { analyticsService: AnalyticsService,
emojiProvider: EmojiProviderProtocol) {
self.timelineController = timelineController self.timelineController = timelineController
self.mediaPlayerProvider = mediaPlayerProvider self.mediaPlayerProvider = mediaPlayerProvider
self.roomProxy = roomProxy self.roomProxy = roomProxy
@ -58,6 +60,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
self.analyticsService = analyticsService self.analyticsService = analyticsService
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.appMediator = appMediator self.appMediator = appMediator
self.emojiProvider = emojiProvider
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider) let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
@ -79,7 +82,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
ownUserID: roomProxy.ownUserID, ownUserID: roomProxy.ownUserID,
isViewSourceEnabled: appSettings.viewSourceEnabled, isViewSourceEnabled: appSettings.viewSourceEnabled,
hideTimelineMedia: appSettings.hideTimelineMedia, hideTimelineMedia: appSettings.hideTimelineMedia,
bindings: .init(reactionsCollapsed: [:])), bindings: .init(reactionsCollapsed: [:]),
emojiProvider: emojiProvider),
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
if focussedEventID != nil { if focussedEventID != nil {
@ -132,6 +136,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
case .itemSendInfoTapped(let itemID): case .itemSendInfoTapped(let itemID):
handleItemSendInfoTapped(itemID: itemID) handleItemSendInfoTapped(itemID: itemID)
case .toggleReaction(let emoji, let itemID): case .toggleReaction(let emoji, let itemID):
emojiProvider.markEmojiAsFrequentlyUsed(emoji)
guard case let .event(_, eventOrTransactionID) = itemID else { guard case let .event(_, eventOrTransactionID) = itemID else {
fatalError() fatalError()
} }
@ -861,7 +867,8 @@ extension TimelineViewModel {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), static let pinnedEventsTimelineMock = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")),
focussedEventID: nil, focussedEventID: nil,
@ -872,7 +879,8 @@ extension TimelineViewModel {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
} }
extension EnvironmentValues { extension EnvironmentValues {

View File

@ -308,7 +308,8 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem, guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem,
let actions = TimelineItemMenuActions(isReactable: true, let actions = TimelineItemMenuActions(isReactable: true,
actions: [.copy, .edit, .reply(isThread: false), .pin, .redact], actions: [.copy, .edit, .reply(isThread: false), .pin, .redact],
debugActions: [.viewSource]) else { debugActions: [.viewSource],
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) else {
return nil return nil
} }

View File

@ -8,26 +8,38 @@
import SFSafeSymbols import SFSafeSymbols
import SwiftUI import SwiftUI
@MainActor
struct TimelineItemMenuActions { struct TimelineItemMenuActions {
let reactions: [TimelineItemMenuReaction] let reactions: [TimelineItemMenuReaction]
let actions: [TimelineItemMenuAction] let actions: [TimelineItemMenuAction]
let debugActions: [TimelineItemMenuAction] let debugActions: [TimelineItemMenuAction]
init?(isReactable: Bool, actions: [TimelineItemMenuAction], debugActions: [TimelineItemMenuAction]) { init?(isReactable: Bool,
actions: [TimelineItemMenuAction],
debugActions: [TimelineItemMenuAction],
emojiProvider: EmojiProviderProtocol) {
if !isReactable, actions.isEmpty, debugActions.isEmpty { if !isReactable, actions.isEmpty, debugActions.isEmpty {
return nil return nil
} }
self.actions = actions self.actions = actions
self.debugActions = debugActions self.debugActions = debugActions
reactions = if isReactable {
[ // Only process 5 of the most frequently used emojis instead of all of them
var frequentlyUsed = emojiProvider.frequentlyUsedSystemEmojis().prefix(5).map { TimelineItemMenuReaction(key: $0, symbol: .heart) }
frequentlyUsed += [
.init(key: "👍️", symbol: .handThumbsup), .init(key: "👍️", symbol: .handThumbsup),
.init(key: "👎️", symbol: .handThumbsdown), .init(key: "👎️", symbol: .handThumbsdown),
.init(key: "🔥", symbol: .flame), .init(key: "🔥", symbol: .flame),
.init(key: "❤️", symbol: .heart), .init(key: "❤️", symbol: .heart),
.init(key: "👏", symbol: .handsClap) .init(key: "👏", symbol: .handsClap)
] ]
frequentlyUsed = Array(frequentlyUsed.prefix(5))
reactions = if isReactable {
frequentlyUsed
} else { } else {
[] []
} }

View File

@ -7,6 +7,7 @@
import Foundation import Foundation
@MainActor
struct TimelineItemMenuActionProvider { struct TimelineItemMenuActionProvider {
let timelineItem: RoomTimelineItemProtocol let timelineItem: RoomTimelineItemProtocol
let canCurrentUserRedactSelf: Bool let canCurrentUserRedactSelf: Bool
@ -16,6 +17,7 @@ struct TimelineItemMenuActionProvider {
let isDM: Bool let isDM: Bool
let isViewSourceEnabled: Bool let isViewSourceEnabled: Bool
let isPinnedEventsTimeline: Bool let isPinnedEventsTimeline: Bool
let emojiProvider: EmojiProviderProtocol
// swiftlint:disable:next cyclomatic_complexity // swiftlint:disable:next cyclomatic_complexity
func makeActions() -> TimelineItemMenuActions? { func makeActions() -> TimelineItemMenuActions? {
@ -42,7 +44,10 @@ struct TimelineItemMenuActionProvider {
break break
} }
return .init(isReactable: false, actions: [.copyPermalink], debugActions: debugActions) return .init(isReactable: false,
actions: [.copyPermalink],
debugActions: debugActions,
emojiProvider: emojiProvider)
} }
var actions: [TimelineItemMenuAction] = [] var actions: [TimelineItemMenuAction] = []
@ -100,7 +105,10 @@ struct TimelineItemMenuActionProvider {
actions = actions.filter(\.canAppearInPinnedEventsTimeline) actions = actions.filter(\.canAppearInPinnedEventsTimeline)
} }
return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable, actions: actions, debugActions: debugActions) return .init(isReactable: isPinnedEventsTimeline ? false : item.isReactable,
actions: actions,
debugActions: debugActions,
emojiProvider: emojiProvider)
} }
private func canRedactItem(_ item: EventBasedTimelineItemProtocol) -> Bool { private func canRedactItem(_ item: EventBasedTimelineItemProtocol) -> Bool {

View File

@ -51,7 +51,8 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
userIndicatorController: UserIndicatorControllerMock(), userIndicatorController: UserIndicatorControllerMock(),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
return mock return mock
}() }()

View File

@ -148,7 +148,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
pinnedEventIDs: context.viewState.pinnedEventIDs, pinnedEventIDs: context.viewState.pinnedEventIDs,
isDM: context.viewState.isEncryptedOneToOneRoom, isDM: context.viewState.isEncryptedOneToOneRoom,
isViewSourceEnabled: context.viewState.isViewSourceEnabled, isViewSourceEnabled: context.viewState.isViewSourceEnabled,
isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline) isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline,
emojiProvider: context.viewState.emojiProvider)
TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in
context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action)) context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action))
} }
@ -364,7 +365,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
}() }()
static var previews: some View { static var previews: some View {

View File

@ -89,7 +89,8 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")] static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),

View File

@ -96,7 +96,8 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {

View File

@ -89,7 +89,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
static var previews: some View { static var previews: some View {
NavigationStack { NavigationStack {

View File

@ -1,13 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
struct EmojiCategory: Equatable, Identifiable {
let id: String
let emojis: [EmojiItem]
}

View File

@ -1,19 +0,0 @@
//
// Copyright 2022-2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
struct EmojiItem: Equatable, Identifiable {
var id: String {
label
}
let label: String
let unicode: String
let keywords: [String]
let shortcodes: [String]
}

View File

@ -7,31 +7,38 @@
import Emojibase import Emojibase
import Foundation import Foundation
import OrderedCollections
@MainActor
protocol EmojiProviderProtocol {
func categories(searchString: String?) async -> [EmojiCategory]
}
private enum EmojiProviderState {
case notLoaded
case inProgress(Task<[EmojiCategory], Never>)
case loaded([EmojiCategory])
}
class EmojiProvider: EmojiProviderProtocol { class EmojiProvider: EmojiProviderProtocol {
private let loader: EmojiLoaderProtocol private let loader: EmojiLoaderProtocol
private var state: EmojiProviderState = .notLoaded private let appSettings: AppSettings
init(loader: EmojiLoaderProtocol = EmojibaseDatasource()) { private(set) var state: EmojiProviderState = .notLoaded
init(loader: EmojiLoaderProtocol = EmojibaseDatasource(), appSettings: AppSettings) {
self.loader = loader self.loader = loader
self.appSettings = appSettings
Task { Task {
await loadIfNeeded() await loadIfNeeded()
} }
} }
func categories(searchString: String? = nil) async -> [EmojiCategory] { func categories(searchString: String? = nil) async -> [EmojiCategory] {
let emojiCategories = await loadIfNeeded() var emojiCategories = await loadIfNeeded()
let allEmojis = emojiCategories.reduce([]) { partialResult, category in
partialResult + category.emojis
}
// Map frequently used system unicode emojis to our emoji provider ones
let frequentlyUsedEmojis = frequentlyUsedSystemEmojis().prefix(20)
let emojis = allEmojis.filter { frequentlyUsedEmojis.contains($0.unicode) }
if !emojis.isEmpty {
emojiCategories.insert(.init(id: EmojiCategory.frequentlyUsedCategoryIdentifier, emojis: emojis), at: 0)
}
if let searchString, searchString.isEmpty == false { if let searchString, searchString.isEmpty == false {
return search(searchString: searchString, emojiCategories: emojiCategories) return search(searchString: searchString, emojiCategories: emojiCategories)
} else { } else {
@ -39,6 +46,40 @@ class EmojiProvider: EmojiProviderProtocol {
} }
} }
func frequentlyUsedSystemEmojis() -> [String] {
guard appSettings.frequentEmojisEnabled, !ProcessInfo.processInfo.isiOSAppOnMac else {
return []
}
guard let preferences = UserDefaults(suiteName: "com.apple.EmojiPreferences"),
let defaults = preferences.dictionary(forKey: "EMFDefaultsKey"),
let recents = defaults["EMFRecentsKey"] as? [String]
else {
return []
}
return recents
}
func markEmojiAsFrequentlyUsed(_ emoji: String) {
guard appSettings.frequentEmojisEnabled else {
return
}
guard let preferences = UserDefaults(suiteName: "com.apple.EmojiPreferences"),
let defaults = preferences.dictionary(forKey: "EMFDefaultsKey"),
let recents = defaults["EMFRecentsKey"] as? [String] else {
return
}
var uniqueOrderedRecents = OrderedSet(recents)
uniqueOrderedRecents.insert(emoji, at: 0)
preferences.setValue(["EMFRecentsKey": Array(uniqueOrderedRecents)], forKey: "EMFDefaultsKey")
}
// MARK: - Private
private func search(searchString: String, emojiCategories: [EmojiCategory]) -> [EmojiCategory] { private func search(searchString: String, emojiCategories: [EmojiCategory]) -> [EmojiCategory] {
emojiCategories.compactMap { category in emojiCategories.compactMap { category in
let emojis = category.emojis.filter { emoji in let emojis = category.emojis.filter { emoji in

View File

@ -0,0 +1,42 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
struct EmojiItem: Equatable, Identifiable {
var id: String {
label
}
let label: String
let unicode: String
let keywords: [String]
let shortcodes: [String]
}
struct EmojiCategory: Equatable, Identifiable {
static let frequentlyUsedCategoryIdentifier = "io.element.elementx.frequently_used"
let id: String
let emojis: [EmojiItem]
}
enum EmojiProviderState {
case notLoaded
case inProgress(Task<[EmojiCategory], Never>)
case loaded([EmojiCategory])
}
@MainActor
protocol EmojiProviderProtocol {
var state: EmojiProviderState { get }
func categories(searchString: String?) async -> [EmojiCategory]
func frequentlyUsedSystemEmojis() -> [String]
func markEmojiAsFrequentlyUsed(_ emoji: String)
}

View File

@ -239,7 +239,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -258,7 +258,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -277,7 +277,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -296,7 +296,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -318,7 +318,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -340,7 +340,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -362,7 +362,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -385,7 +385,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -407,7 +407,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -428,7 +428,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -463,7 +463,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -485,7 +485,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
@ -507,7 +507,7 @@ class MockScreen: Identifiable {
mediaProvider: MediaProviderMock(configuration: .init()), mediaProvider: MediaProviderMock(configuration: .init()),
mediaPlayerProvider: MediaPlayerProviderMock(), mediaPlayerProvider: MediaPlayerProviderMock(),
voiceMessageMediaManager: VoiceMessageMediaManagerMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(),
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,

View File

@ -18,7 +18,7 @@ final class EmojiProviderTests: XCTestCase {
let emojiLoaderMock = EmojiLoaderMock() let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = [category] emojiLoaderMock.categories = [category]
let emojiProvider = EmojiProvider(loader: emojiLoaderMock) let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
let categories = await emojiProvider.categories() let categories = await emojiProvider.categories()
XCTAssertEqual(emojiLoaderMock.categories, categories) XCTAssertEqual(emojiLoaderMock.categories, categories)
@ -31,7 +31,7 @@ final class EmojiProviderTests: XCTestCase {
let emojiLoaderMock = EmojiLoaderMock() let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = [category] emojiLoaderMock.categories = [category]
let emojiProvider = EmojiProvider(loader: emojiLoaderMock) let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
let categories = await emojiProvider.categories(searchString: "") let categories = await emojiProvider.categories(searchString: "")
XCTAssertEqual(emojiLoaderMock.categories, categories) XCTAssertEqual(emojiLoaderMock.categories, categories)
@ -48,7 +48,7 @@ final class EmojiProviderTests: XCTestCase {
let emojiLoaderMock = EmojiLoaderMock() let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = categoriesForFirstLoad emojiLoaderMock.categories = categoriesForFirstLoad
let emojiProvider = EmojiProvider(loader: emojiLoaderMock) let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
_ = await emojiProvider.categories() _ = await emojiProvider.categories()
emojiLoaderMock.categories = categoriesForSecondLoad emojiLoaderMock.categories = categoriesForSecondLoad
@ -78,7 +78,7 @@ final class EmojiProviderTests: XCTestCase {
let emojiLoaderMock = EmojiLoaderMock() let emojiLoaderMock = EmojiLoaderMock()
emojiLoaderMock.categories = categories emojiLoaderMock.categories = categories
let emojiProvider = EmojiProvider(loader: emojiLoaderMock) let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings)
_ = await emojiProvider.categories() _ = await emojiProvider.categories()
let result = await emojiProvider.categories(searchString: searchString) let result = await emojiProvider.categories(searchString: searchString)

View File

@ -25,7 +25,8 @@ class PillContextTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
XCTAssertFalse(context.viewState.isOwnMention) XCTAssertFalse(context.viewState.isOwnMention)
@ -53,7 +54,8 @@ class PillContextTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .user(userID: id), font: .preferredFont(forTextStyle: .body)))
XCTAssertTrue(context.viewState.isOwnMention) XCTAssertTrue(context.viewState.isOwnMention)
@ -74,7 +76,8 @@ class PillContextTests: XCTestCase {
userIndicatorController: ServiceLocator.shared.userIndicatorController, userIndicatorController: ServiceLocator.shared.userIndicatorController,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body))) let context = PillContext(timelineContext: mock.context, data: PillTextAttachmentData(type: .allUsers, font: .preferredFont(forTextStyle: .body)))
XCTAssertTrue(context.viewState.isOwnMention) XCTAssertTrue(context.viewState.isOwnMention)

View File

@ -298,7 +298,7 @@ class RoomFlowCoordinatorTests: XCTestCase {
isChildFlow: asChildFlow, isChildFlow: asChildFlow,
roomTimelineControllerFactory: timelineControllerFactory, roomTimelineControllerFactory: timelineControllerFactory,
navigationStackCoordinator: navigationStackCoordinator, navigationStackCoordinator: navigationStackCoordinator,
emojiProvider: EmojiProvider(), emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings),
ongoingCallRoomIDPublisher: .init(.init(nil)), ongoingCallRoomIDPublisher: .init(.init(nil)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,

View File

@ -310,7 +310,8 @@ class TimelineViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock, userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
return (viewModel, roomProxy, timelineProxy, timelineController) return (viewModel, roomProxy, timelineProxy, timelineController)
} }
@ -334,7 +335,8 @@ class TimelineViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock, userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
let deferred = deferFulfillment(viewModel.context.$viewState) { value in let deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts
@ -360,7 +362,8 @@ class TimelineViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock, userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
var deferred = deferFulfillment(viewModel.context.$viewState) { value in var deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.pinnedEventIDs == ["test1"] value.pinnedEventIDs == ["test1"]
@ -388,7 +391,8 @@ class TimelineViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock, userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
var deferred = deferFulfillment(viewModel.context.$viewState) { value in var deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserPin value.canCurrentUserPin
@ -417,7 +421,8 @@ class TimelineViewModelTests: XCTestCase {
userIndicatorController: userIndicatorControllerMock, userIndicatorController: userIndicatorControllerMock,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
} }
} }