From 0e73444c5033832d0d0d84590b6735d417b07964 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Oct 2024 16:12:08 +0300 Subject: [PATCH 1/2] Fix #1934 - Hook reaction pickers into the system's recently used keyboard emojis --- ElementX.xcodeproj/project.pbxproj | 12 ++-- .../en.lproj/Localizable.strings | 28 +++++--- .../Sources/Application/AppSettings.swift | 4 ++ .../PinnedEventsTimelineFlowCoordinator.swift | 8 ++- .../RoomFlowCoordinator.swift | 3 +- .../UserSessionFlowCoordinator.swift | 2 +- ElementX/Sources/Generated/Strings.swift | 37 +++++++--- .../EmojiPickerScreenModels.swift | 2 + .../EmojiPickerScreenViewModel.swift | 1 + .../View/EmojiPickerScreen.swift | 4 +- ...innedEventsTimelineScreenCoordinator.swift | 4 +- .../View/PinnedEventsTimelineScreen.swift | 6 +- .../RoomScreen/RoomScreenCoordinator.swift | 3 +- .../Screens/RoomScreen/View/RoomScreen.swift | 6 +- .../DeveloperOptionsScreenModels.swift | 1 + .../View/DeveloperOptionsScreen.swift | 4 ++ .../Screens/Timeline/TimelineModels.swift | 2 + .../Screens/Timeline/TimelineViewModel.swift | 16 +++-- .../View/ItemMenu/TimelineItemMenu.swift | 3 +- .../ItemMenu/TimelineItemMenuAction.swift | 27 +++++--- .../TimelineItemMenuActionProvider.swift | 12 +++- .../ReadReceiptsSummaryView.swift | 3 +- .../Style/TimelineItemBubbledStylerView.swift | 6 +- .../TimelineReadReceiptsView.swift | 3 +- .../HighlightedTimelineItemModifier.swift | 3 +- .../Screens/Timeline/View/TimelineView.swift | 3 +- .../Services/Emojis/EmojiCategory.swift | 13 ---- .../Sources/Services/Emojis/EmojiItem.swift | 19 ----- .../Services/Emojis/EmojiProvider.swift | 69 +++++++++++++++---- .../Emojis/EmojiProviderProtocol.swift | 42 +++++++++++ .../UITests/UITestsAppCoordinator.swift | 26 +++---- UnitTests/Sources/EmojiProviderTests.swift | 8 +-- UnitTests/Sources/PillContextTests.swift | 9 ++- .../Sources/RoomFlowCoordinatorTests.swift | 2 +- .../Sources/TimelineViewModelTests.swift | 15 ++-- 35 files changed, 273 insertions(+), 133 deletions(-) delete mode 100644 ElementX/Sources/Services/Emojis/EmojiCategory.swift delete mode 100644 ElementX/Sources/Services/Emojis/EmojiItem.swift create mode 100644 ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 5f5493965f..13a12b0268 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -511,7 +511,6 @@ 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.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 */; }; 73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.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 */; }; A51C65E5A3C9F2464A91A380 /* AuthenticationClientBuilderFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0554FEA301486A8CFA475D5A /* AuthenticationClientBuilderFactoryMock.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 */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.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 */; }; D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.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 */; }; D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.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 = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; 376D941BF8BB294389C0DE24 /* MapTilerURLBuildersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerURLBuildersTests.swift; sourceTree = ""; }; - 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItem.swift; sourceTree = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = ""; }; 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreen.swift; sourceTree = ""; }; @@ -1474,7 +1472,6 @@ 3BAC027034248429A438886B /* AppMediatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediatorMock.swift; sourceTree = ""; }; 3BC1B7CB061C9865B2B91B56 /* QRCodeLoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenViewModel.swift; sourceTree = ""; }; 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenCoordinator.swift; sourceTree = ""; }; - 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCategory.swift; sourceTree = ""; }; 3C368CAB3063EF275357ECD4 /* LoginScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModel.swift; sourceTree = ""; }; 3C3E67E09FE5A35D73818C39 /* AppLockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenModels.swift; sourceTree = ""; }; 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRoomPlaybackView.swift; sourceTree = ""; }; @@ -1815,6 +1812,7 @@ 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = ""; }; 8AE78FA0011E07920AE83135 /* PlainMentionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainMentionBuilder.swift; sourceTree = ""; }; 8AFCE895ECFFA53FEE64D62B /* MediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoader.swift; sourceTree = ""; }; + 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderProtocol.swift; sourceTree = ""; }; 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -3080,10 +3078,9 @@ 39557ADF21345E18F3865B9E /* Emojis */ = { isa = PBXGroup; children = ( - 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */, - 37A243E04B58DC6E41FDCD82 /* EmojiItem.swift */, 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */, 6C113E0CB7E15E9765B1817A /* EmojiProvider.swift */, + 8BCCE3D12B0A9C6E559B5B5A /* EmojiProviderProtocol.swift */, ); path = Emojis; sourceTree = ""; @@ -6448,9 +6445,7 @@ 370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */, 3F997171C3C79A45E92BF9EF /* ElementWellKnown.swift in Sources */, 7C1A7B594B2F8143F0DD0005 /* ElementXAttributeScope.swift in Sources */, - 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */, E45C9FA22BC13B477FD3B4AC /* EmojiDetection.swift in Sources */, - D5C805F49B2C75DC3793E780 /* EmojiItem.swift in Sources */, 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */, 340D39DB87F3800D53A6A621 /* EmojiPickerScreen.swift in Sources */, C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */, @@ -6459,6 +6454,7 @@ 2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */, 1D69E31913DF66426985909B /* EmojiPickerScreenViewModelProtocol.swift in Sources */, FBF09B6C900415800DDF2A21 /* EmojiProvider.swift in Sources */, + A5B455D1A6DADF7476F7B417 /* EmojiProviderProtocol.swift in Sources */, 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */, 8B41D0357B91CD3B6F6A3BCA /* EmoteRoomTimelineItemContent.swift in Sources */, 661EF50C1F7D4B0BC8A7AAE3 /* EmoteRoomTimelineView.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 4aeb3bef7b..9a1b11ba00 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -139,6 +139,7 @@ "common_edited_suffix" = "(edited)"; "common_editing" = "Editing"; "common_emote" = "* %1$@ %2$@"; +"common_encryption" = "Encryption"; "common_encryption_enabled" = "Encryption enabled"; "common_enter_your_pin" = "Enter your PIN"; "common_error" = "Error"; @@ -149,6 +150,7 @@ "common_favourited" = "Favourited"; "common_file" = "File"; "common_forward_message" = "Forward message"; +"common_frequently_used" = "Frequently used"; "common_gif" = "GIF"; "common_image" = "Image"; "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_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_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_title" = "Anyone"; "screen_create_room_access_section_header" = "Room Access"; @@ -449,9 +454,12 @@ "screen_change_server_title" = "Select your server"; "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_description" = "Backup ensures that you don't lose your message history. %1$@."; -"screen_chat_backup_key_backup_title" = "Backup"; +"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" = "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_description" = "Recover your cryptographic identity and message history with a recovery key if you’ve 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_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."; @@ -473,10 +481,10 @@ "screen_create_poll_title" = "Create Poll"; "screen_create_room_action_create_room" = "New 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 can’t be disabled afterwards."; -"screen_create_room_private_option_title" = "Private room (invite only)"; -"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_title" = "Public room (anyone)"; +"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"; +"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"; "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_delete_all_messages" = "Delete all my messages"; @@ -624,7 +632,6 @@ "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_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_title" = "Change 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_generating_key" = "Generating…"; "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_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_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_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_title" = "Set up recovery"; "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_notification_settings_mentions_section_title" = "Mentions"; "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_report_content_block_user" = "Block user"; "screen_reset_encryption_password_placeholder" = "Enter…"; diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index fe6d11d335..7ec77ce53b 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -48,6 +48,7 @@ final class AppSettings { case enableOnlySignedDeviceIsolationMode case identityPinningViolationNotificationsEnabled case knockingEnabled + case frequentEmojisEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -289,6 +290,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store)) var knockingEnabled + + @UserPreference(key: UserDefaultsKeys.frequentEmojisEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store)) + var frequentEmojisEnabled #endif diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index fd021d3cbb..f900329307 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -22,6 +22,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { private let roomProxy: JoinedRoomProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let appMediator: AppMediatorProtocol + private let emojiProvider: EmojiProviderProtocol private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { @@ -35,13 +36,15 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol, roomProxy: JoinedRoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol, - appMediator: AppMediatorProtocol) { + appMediator: AppMediatorProtocol, + emojiProvider: EmojiProviderProtocol) { self.navigationStackCoordinator = navigationStackCoordinator self.userSession = userSession self.roomTimelineControllerFactory = roomTimelineControllerFactory self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController self.appMediator = appMediator + self.emojiProvider = emojiProvider } func start() { @@ -71,7 +74,8 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { mediaProvider: userSession.mediaProvider, mediaPlayerProvider: MediaPlayerProvider(), voiceMessageMediaManager: userSession.voiceMessageMediaManager, - appMediator: appMediator)) + appMediator: appMediator, + emojiProvider: emojiProvider)) coordinator.actions .sink { [weak self] action in diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 5eabe89580..b5af3ad085 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -1340,7 +1340,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { roomTimelineControllerFactory: roomTimelineControllerFactory, roomProxy: roomProxy, userIndicatorController: userIndicatorController, - appMediator: appMediator) + appMediator: appMediator, + emojiProvider: emojiProvider) coordinator.actionsPublisher.sink { [weak self] action in guard let self else { diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index e99fc21f61..c5b64b2208 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -492,7 +492,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { isChildFlow: false, roomTimelineControllerFactory: roomTimelineControllerFactory, navigationStackCoordinator: detailNavigationStackCoordinator, - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: appSettings), ongoingCallRoomIDPublisher: elementCallService.ongoingCallRoomIDPublisher, appMediator: appMediator, appSettings: appSettings, diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index a9fef6619b..04e3312aad 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -312,6 +312,8 @@ internal enum L10n { internal static func commonEmote(_ p1: Any, _ p2: Any) -> String { 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 internal static var commonEncryptionEnabled: String { return L10n.tr("Localizable", "common_encryption_enabled") } /// Enter your PIN @@ -332,6 +334,8 @@ internal enum L10n { internal static var commonFile: String { return L10n.tr("Localizable", "common_file") } /// 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 internal static var commonGif: String { return L10n.tr("Localizable", "common_gif") } /// Image @@ -1005,14 +1009,20 @@ internal enum L10n { internal static var screenChatBackupKeyBackupActionDisable: String { return L10n.tr("Localizable", "screen_chat_backup_key_backup_action_disable") } /// Turn on backup 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 { 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") } + /// 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 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 you’ve lost all your existing devices. + internal static var screenChatBackupRecoveryActionChangeDescription: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_change_description") } /// Enter recovery key internal static var screenChatBackupRecoveryActionConfirm: String { return L10n.tr("Localizable", "screen_chat_backup_recovery_action_confirm") } /// 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") } /// An error occurred when creating the room internal static var screenCreateRoomErrorCreatingRoom: String { return L10n.tr("Localizable", "screen_create_room_error_creating_room") } - /// Messages in this room are encrypted. Encryption can’t 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") } - /// Private room (invite only) + /// Private room 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") } - /// Public room (anyone) + /// Public room 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 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 internal static var screenCreateRoomTitle: String { return L10n.tr("Localizable", "screen_create_room_title") } /// Topic (optional) @@ -1475,7 +1492,7 @@ internal enum L10n { internal static var screenRecoveryKeyChangeDescription: String { return L10n.tr("Localizable", "screen_recovery_key_change_description") } /// Generate a new recovery 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") } /// Recovery key changed 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") } /// Save recovery key 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") } /// Tap to copy recovery key 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") } /// 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") } @@ -1519,7 +1536,7 @@ internal enum L10n { internal static var screenRecoveryKeySetupDescription: String { return L10n.tr("Localizable", "screen_recovery_key_setup_description") } /// Generate your recovery 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") } /// Recovery setup successful internal static var screenRecoveryKeySetupSuccess: String { return L10n.tr("Localizable", "screen_recovery_key_setup_success") } diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift index 5aa2c4cd95..a56940a6dd 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenModels.swift @@ -44,6 +44,8 @@ struct EmojiPickerEmojiCategoryViewData: Identifiable { return L10n.emojiPickerCategorySymbols case "flags": return L10n.emojiPickerCategoryFlags + case EmojiCategory.frequentlyUsedCategoryIdentifier: + return L10n.commonFrequentlyUsed default: MXLog.failure("Missing translation for emoji category with id \(id)") return "" diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index ce71384ca6..a50238258f 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -36,6 +36,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr state.categories = convert(emojiCategories: categories) } case let .emojiTapped(emoji: emoji): + emojiProvider.markEmojiAsFrequentlyUsed(emoji.value) actionsSubject.send(.emojiSelected(emoji: emoji.value)) case .dismiss: actionsSubject.send(.dismiss) diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift index 9f5e39325f..7f139f79a1 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift @@ -81,7 +81,7 @@ struct EmojiPickerScreen: View { // MARK: - Previews 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 { EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"]) @@ -91,7 +91,7 @@ struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview { } 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 { Text("Timeline view") diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 850f7026eb..3c7cf46e0f 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -15,6 +15,7 @@ struct PinnedEventsTimelineScreenCoordinatorParameters { let mediaPlayerProvider: MediaPlayerProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol let appMediator: AppMediatorProtocol + let emojiProvider: EmojiProviderProtocol } enum PinnedEventsTimelineScreenCoordinatorAction { @@ -49,7 +50,8 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: parameters.appMediator, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: parameters.emojiProvider) } func start() { diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index c144ae4666..8a18ec2744 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -37,7 +37,8 @@ struct PinnedEventsTimelineScreen: View { pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, - isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline) + isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, + emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() if let actions { TimelineItemMenu(item: info.item, actions: actions) @@ -96,7 +97,8 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { userIndicatorController: UserIndicatorControllerMock(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 16b4e2f495..1f63793857 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -81,7 +81,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: parameters.appMediator, appSettings: parameters.appSettings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: parameters.emojiProvider) wysiwygViewModel = WysiwygComposerViewModel(minHeight: ComposerConstant.minHeight, maxCompressedHeight: ComposerConstant.maxHeight, diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 33023ad766..9e187d8ab3 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -76,7 +76,8 @@ struct RoomScreen: View { pinnedEventIDs: timelineContext.viewState.pinnedEventIDs, isDM: timelineContext.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: timelineContext.viewState.isViewSourceEnabled, - isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline) + isPinnedEventsTimeline: timelineContext.viewState.isPinnedEventsTimeline, + emojiProvider: timelineContext.viewState.emojiProvider) .makeActions() if let actions { TimelineItemMenu(item: info.item, actions: actions) @@ -229,7 +230,8 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 1d282d462d..c416a8ec1b 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -51,6 +51,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var elementCallBaseURLOverride: URL? { get set } var identityPinningViolationNotificationsEnabled: Bool { get set } var knockingEnabled: Bool { get set } + var frequentEmojisEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 22f7044896..5768901b14 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -53,6 +53,10 @@ struct DeveloperOptionsScreen: View { Toggle(isOn: $context.identityPinningViolationNotificationsEnabled) { Text("Identity pinning violation notifications") } + + Toggle(isOn: $context.frequentEmojisEnabled) { + Text("Show frequently used emojis") + } } Section("Join rules") { diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 44a7ed0834..363ddb9fa1 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -111,6 +111,8 @@ struct TimelineViewState: BindableState { /// A closure providing the associated audio player state for an item in the timeline. var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)? + + var emojiProvider: EmojiProviderProtocol } struct TimelineViewStateBindings { diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 7d35cb7a73..56307073d6 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -28,6 +28,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { private let appMediator: AppMediatorProtocol private let appSettings: AppSettings private let analyticsService: AnalyticsService + private let emojiProvider: EmojiProviderProtocol private let timelineInteractionHandler: TimelineInteractionHandler @@ -50,7 +51,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { userIndicatorController: UserIndicatorControllerProtocol, appMediator: AppMediatorProtocol, appSettings: AppSettings, - analyticsService: AnalyticsService) { + analyticsService: AnalyticsService, + emojiProvider: EmojiProviderProtocol) { self.timelineController = timelineController self.mediaPlayerProvider = mediaPlayerProvider self.roomProxy = roomProxy @@ -58,6 +60,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { self.analyticsService = analyticsService self.userIndicatorController = userIndicatorController self.appMediator = appMediator + self.emojiProvider = emojiProvider let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider) @@ -79,7 +82,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { ownUserID: roomProxy.ownUserID, isViewSourceEnabled: appSettings.viewSourceEnabled, hideTimelineMedia: appSettings.hideTimelineMedia, - bindings: .init(reactionsCollapsed: [:])), + bindings: .init(reactionsCollapsed: [:]), + emojiProvider: emojiProvider), mediaProvider: mediaProvider) if focussedEventID != nil { @@ -132,6 +136,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { case .itemSendInfoTapped(let itemID): handleItemSendInfoTapped(itemID: itemID) case .toggleReaction(let emoji, let itemID): + emojiProvider.markEmojiAsFrequentlyUsed(emoji) + guard case let .event(_, eventOrTransactionID) = itemID else { fatalError() } @@ -861,7 +867,8 @@ extension TimelineViewModel { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, 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")), focussedEventID: nil, @@ -872,7 +879,8 @@ extension TimelineViewModel { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) } extension EnvironmentValues { diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index 527e54f3d8..86a5ccead6 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -308,7 +308,8 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem, let actions = TimelineItemMenuActions(isReactable: true, actions: [.copy, .edit, .reply(isThread: false), .pin, .redact], - debugActions: [.viewSource]) else { + debugActions: [.viewSource], + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) else { return nil } diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift index 0ef3a10d35..1cca357c44 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift @@ -8,26 +8,37 @@ import SFSafeSymbols import SwiftUI +@MainActor struct TimelineItemMenuActions { let reactions: [TimelineItemMenuReaction] let actions: [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 { return nil } self.actions = actions self.debugActions = debugActions + + var frequentlyUsed = emojiProvider.frequentlyUsedSystemEmojis().prefix(5).map { TimelineItemMenuReaction(key: $0, symbol: .heart) } + + frequentlyUsed += [ + .init(key: "👍️", symbol: .handThumbsup), + .init(key: "👎️", symbol: .handThumbsdown), + .init(key: "🔥", symbol: .flame), + .init(key: "❤️", symbol: .heart), + .init(key: "👏", symbol: .handsClap) + ] + + frequentlyUsed = Array(frequentlyUsed.prefix(5)) + reactions = if isReactable { - [ - .init(key: "👍️", symbol: .handThumbsup), - .init(key: "👎️", symbol: .handThumbsdown), - .init(key: "🔥", symbol: .flame), - .init(key: "❤️", symbol: .heart), - .init(key: "👏", symbol: .handsClap) - ] + frequentlyUsed } else { [] } diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index f3b79fb5df..9fcc47575a 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor struct TimelineItemMenuActionProvider { let timelineItem: RoomTimelineItemProtocol let canCurrentUserRedactSelf: Bool @@ -16,6 +17,7 @@ struct TimelineItemMenuActionProvider { let isDM: Bool let isViewSourceEnabled: Bool let isPinnedEventsTimeline: Bool + let emojiProvider: EmojiProviderProtocol // swiftlint:disable:next cyclomatic_complexity func makeActions() -> TimelineItemMenuActions? { @@ -42,7 +44,10 @@ struct TimelineItemMenuActionProvider { break } - return .init(isReactable: false, actions: [.copyPermalink], debugActions: debugActions) + return .init(isReactable: false, + actions: [.copyPermalink], + debugActions: debugActions, + emojiProvider: emojiProvider) } var actions: [TimelineItemMenuAction] = [] @@ -100,7 +105,10 @@ struct TimelineItemMenuActionProvider { 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 { diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift index 414266fa45..2de58b9edd 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -51,7 +51,8 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { userIndicatorController: UserIndicatorControllerMock(), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) return mock }() diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index 049a788311..61ed78ee82 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -148,7 +148,8 @@ struct TimelineItemBubbledStylerView: View { pinnedEventIDs: context.viewState.pinnedEventIDs, isDM: context.viewState.isEncryptedOneToOneRoom, isViewSourceEnabled: context.viewState.isViewSourceEnabled, - isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline) + isPinnedEventsTimeline: context.viewState.isPinnedEventsTimeline, + emojiProvider: context.viewState.emojiProvider) TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action)) } @@ -364,7 +365,8 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) }() static var previews: some View { diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index f3f88fedd1..6f37e7f56e 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -89,7 +89,8 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, 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 doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift index efb0566cd1..cb5695bdff 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift @@ -96,7 +96,8 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index c9133b242e..5315b57884 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -89,7 +89,8 @@ struct TimelineView_Previews: PreviewProvider, TestablePreview { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) static var previews: some View { NavigationStack { diff --git a/ElementX/Sources/Services/Emojis/EmojiCategory.swift b/ElementX/Sources/Services/Emojis/EmojiCategory.swift deleted file mode 100644 index 7e42a1f819..0000000000 --- a/ElementX/Sources/Services/Emojis/EmojiCategory.swift +++ /dev/null @@ -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] -} diff --git a/ElementX/Sources/Services/Emojis/EmojiItem.swift b/ElementX/Sources/Services/Emojis/EmojiItem.swift deleted file mode 100644 index d08cbb0abf..0000000000 --- a/ElementX/Sources/Services/Emojis/EmojiItem.swift +++ /dev/null @@ -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] -} diff --git a/ElementX/Sources/Services/Emojis/EmojiProvider.swift b/ElementX/Sources/Services/Emojis/EmojiProvider.swift index 5272902183..a5dfcc06fe 100644 --- a/ElementX/Sources/Services/Emojis/EmojiProvider.swift +++ b/ElementX/Sources/Services/Emojis/EmojiProvider.swift @@ -7,31 +7,38 @@ import Emojibase import Foundation - -@MainActor -protocol EmojiProviderProtocol { - func categories(searchString: String?) async -> [EmojiCategory] -} - -private enum EmojiProviderState { - case notLoaded - case inProgress(Task<[EmojiCategory], Never>) - case loaded([EmojiCategory]) -} +import OrderedCollections class EmojiProvider: EmojiProviderProtocol { 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.appSettings = appSettings + Task { await loadIfNeeded() } } 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 + } + + 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 { return search(searchString: searchString, emojiCategories: emojiCategories) } 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] { emojiCategories.compactMap { category in let emojis = category.emojis.filter { emoji in diff --git a/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift b/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift new file mode 100644 index 0000000000..b0137716e8 --- /dev/null +++ b/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift @@ -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) +} diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index fb12887859..1d4e6ca035 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -239,7 +239,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -258,7 +258,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -277,7 +277,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -296,7 +296,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -318,7 +318,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -340,7 +340,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -362,7 +362,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -385,7 +385,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -407,7 +407,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -428,7 +428,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -463,7 +463,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -485,7 +485,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, @@ -507,7 +507,7 @@ class MockScreen: Identifiable { mediaProvider: MediaProviderMock(configuration: .init()), mediaPlayerProvider: MediaPlayerProviderMock(), voiceMessageMediaManager: VoiceMessageMediaManagerMock(), - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, diff --git a/UnitTests/Sources/EmojiProviderTests.swift b/UnitTests/Sources/EmojiProviderTests.swift index 4733122b8c..799a67ee2a 100644 --- a/UnitTests/Sources/EmojiProviderTests.swift +++ b/UnitTests/Sources/EmojiProviderTests.swift @@ -18,7 +18,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = [category] - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) let categories = await emojiProvider.categories() XCTAssertEqual(emojiLoaderMock.categories, categories) @@ -31,7 +31,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = [category] - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) let categories = await emojiProvider.categories(searchString: "") XCTAssertEqual(emojiLoaderMock.categories, categories) @@ -48,7 +48,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = categoriesForFirstLoad - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) _ = await emojiProvider.categories() emojiLoaderMock.categories = categoriesForSecondLoad @@ -78,7 +78,7 @@ final class EmojiProviderTests: XCTestCase { let emojiLoaderMock = EmojiLoaderMock() emojiLoaderMock.categories = categories - let emojiProvider = EmojiProvider(loader: emojiLoaderMock) + let emojiProvider = EmojiProvider(loader: emojiLoaderMock, appSettings: ServiceLocator.shared.settings) _ = await emojiProvider.categories() let result = await emojiProvider.categories(searchString: searchString) diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index 03b1b462e0..7f7f72e556 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -25,7 +25,8 @@ class PillContextTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, 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))) XCTAssertFalse(context.viewState.isOwnMention) @@ -53,7 +54,8 @@ class PillContextTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, 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))) XCTAssertTrue(context.viewState.isOwnMention) @@ -74,7 +76,8 @@ class PillContextTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, appMediator: AppMediatorMock.default, 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))) XCTAssertTrue(context.viewState.isOwnMention) diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index a10fa0f591..18e20f4524 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -298,7 +298,7 @@ class RoomFlowCoordinatorTests: XCTestCase { isChildFlow: asChildFlow, roomTimelineControllerFactory: timelineControllerFactory, navigationStackCoordinator: navigationStackCoordinator, - emojiProvider: EmojiProvider(), + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings), ongoingCallRoomIDPublisher: .init(.init(nil)), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index d0a95e8131..28f2ad0d32 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -310,7 +310,8 @@ class TimelineViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) return (viewModel, roomProxy, timelineProxy, timelineController) } @@ -334,7 +335,8 @@ class TimelineViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, 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 value.bindings.readReceiptsSummaryInfo?.orderedReceipts == receipts @@ -360,7 +362,8 @@ class TimelineViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, 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 value.pinnedEventIDs == ["test1"] @@ -388,7 +391,8 @@ class TimelineViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, 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 value.canCurrentUserPin @@ -417,7 +421,8 @@ class TimelineViewModelTests: XCTestCase { userIndicatorController: userIndicatorControllerMock, appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings, - analyticsService: ServiceLocator.shared.analytics) + analyticsService: ServiceLocator.shared.analytics, + emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) } } From 4f0c88b46b2ca106ced17189aae6f4a74e93731b Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 25 Oct 2024 17:07:49 +0300 Subject: [PATCH 2/2] Address PR comments --- .../Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift | 1 + ElementX/Sources/Services/Emojis/EmojiProvider.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift index 1cca357c44..a2e7de4f20 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuAction.swift @@ -25,6 +25,7 @@ struct TimelineItemMenuActions { self.actions = actions self.debugActions = debugActions + // 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 += [ diff --git a/ElementX/Sources/Services/Emojis/EmojiProvider.swift b/ElementX/Sources/Services/Emojis/EmojiProvider.swift index a5dfcc06fe..dfa0cb70a2 100644 --- a/ElementX/Sources/Services/Emojis/EmojiProvider.swift +++ b/ElementX/Sources/Services/Emojis/EmojiProvider.swift @@ -31,8 +31,8 @@ class EmojiProvider: EmojiProviderProtocol { 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 {