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 */; };
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 = "<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>"; };
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>"; };
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>"; };
@ -1474,7 +1472,6 @@
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>"; };
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>"; };
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>"; };
@ -1815,6 +1812,7 @@
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>"; };
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>"; };
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>"; };
@ -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 = "<group>";
@ -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 */,

View File

@ -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 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_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 cant 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…";

View File

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

View File

@ -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<PinnedEventsTimelineFlowCoordinatorAction, Never> = .init()
var actionsPublisher: AnyPublisher<PinnedEventsTimelineFlowCoordinatorAction, Never> {
@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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") {

View File

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

View File

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

View File

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

View File

@ -8,26 +8,38 @@
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
// 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: .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 {
[]
}

View File

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

View File

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

View File

@ -148,7 +148,8 @@ struct TimelineItemBubbledStylerView<Content: View>: 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 {

View File

@ -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"),

View File

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

View File

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

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 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
}
// 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 {
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

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

View File

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

View File

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

View File

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

View File

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