diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index a17008a70..5d4f1c690 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -217,6 +217,7 @@ 35E975CFDA60E05362A7CF79 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 1222DB76B917EB8A55365BA5 /* target.yml */; }; 366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */; }; 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; }; + 369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */; }; 36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; }; 36AD4DD4C798E22584ED3200 /* Emojibase in Frameworks */ = {isa = PBXBuildFile; productRef = C05729B1684C331F5FFE9232 /* Emojibase */; }; 36CD6E11B37396E14F032CB6 /* WysiwygComposer in Frameworks */ = {isa = PBXBuildFile; productRef = CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */; }; @@ -562,6 +563,7 @@ 8C1A5ECAF895D4CAF8C4D461 /* AppActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F21ED7205048668BEB44A38 /* AppActivityView.swift */; }; 8C42B5B1642D189C362A5EDF /* SecureBackupScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91831D7042EADD0CC2B5EC36 /* SecureBackupScreenUITests.swift */; }; 8C706DA7EAC0974CA2F8F1CD /* MentionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15748C254911E3654C93B0ED /* MentionBuilder.swift */; }; + 8C91D242BEEC657FABCC0B95 /* BlockedUsersScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8642512079EEFD622E3AA66B /* BlockedUsersScreenModels.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; 8D3E1FADD78E72504DE0E402 /* UserAgentBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */; }; 8D71E5E53F372202379BECCE /* BugReportScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */; }; @@ -582,6 +584,7 @@ 92720AB0DA9AB5EEF1DAF56B /* SecureBackupLogoutConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DC017C3CB6B0F7C63F460F2 /* SecureBackupLogoutConfirmationScreenViewModel.swift */; }; 9278EC51D24E57445B290521 /* AudioSessionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */; }; 92D9088B901CEBB1A99ECA4E /* RoomMemberProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */; }; + 934051B17A884AB0635DF81B /* BlockedUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; 93A549135E6C027A0D823BFE /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 593FBBF394712F2963E98A0B /* DTCoreText */; }; 93BA4A81B6D893271101F9F0 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = A7CA6F33C553805035C3B114 /* DeviceKit */; }; @@ -593,6 +596,7 @@ 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 95690DDD9D547D3D842ACBE3 /* AnalyticsSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */; }; 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */; }; + 95E7B236F7116CACE05A6BC9 /* BlockedUsersScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */; }; 962A4F8AD6312804E2C6BB6E /* PhotoLibraryPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */; }; 964B9D2EC38C488C360CE0C9 /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B902EA6CD3296B0E10EE432B /* HomeScreen.swift */; }; 968A5B890004526AB58A217C /* AvatarSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24B88AD3D1599E8CB1376E0 /* AvatarSize.swift */; }; @@ -657,6 +661,7 @@ A440D4BC02088482EC633A88 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; A494741843F087881299ACF0 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FA515B3B0D61EF1E907D2D /* BadgeView.swift */; }; + A4B123C635F70DDD4BC2FAC9 /* BlockedUsersScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */; }; A4C29D373986AFE4559696D5 /* SecureBackupKeyBackupScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4525E8C0FBDD27D1ACE90952 /* SecureBackupKeyBackupScreenViewModelProtocol.swift */; }; A4E885358D7DD5A072A06824 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CCE5BF78B125320CBF3BB834 /* PostHog */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; @@ -831,6 +836,7 @@ CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CE6F237360875D3D573FD0B2 /* RoomNotificationSettingsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6B522BD637845AB9570B10 /* RoomNotificationSettingsProxy.swift */; }; CE9530A4CA661E090635C2F2 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; }; + CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */; }; CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; CF3827071B0BC9638BD44F5D /* WaitlistScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB58EF0176D4CFB1040DA22 /* WaitlistScreenViewModel.swift */; }; CF38B70D8C6DD42C00A56A27 /* LogViewerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */; }; @@ -1223,6 +1229,7 @@ 2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProvider.swift; sourceTree = ""; }; 2389732B0E115A999A069083 /* NotificationSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreenCoordinator.swift; sourceTree = ""; }; 23AA3F4B285570805CB0CCDD /* MapTiler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTiler.swift; sourceTree = ""; }; + 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModelTests.swift; sourceTree = ""; }; 24227FF9A2797F6EA7F69CDD /* HomeScreenInvitesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenInvitesButton.swift; sourceTree = ""; }; 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerTests.swift; sourceTree = ""; }; 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderTests.swift; sourceTree = ""; }; @@ -1582,6 +1589,7 @@ 85EB16E7FE59A947CA441531 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; 8610C1D21565C950BCA6A454 /* AppLockSetupSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 86376BEE425704AEE197CA54 /* PillContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContext.swift; sourceTree = ""; }; + 8642512079EEFD622E3AA66B /* BlockedUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenModels.swift; sourceTree = ""; }; 86873A768B13069BB5CAECF6 /* InvitesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenViewModelProtocol.swift; sourceTree = ""; }; 86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenViewModel.swift; sourceTree = ""; }; 86C8CE2630F54D5FE1591786 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1665,6 +1673,7 @@ 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineController.swift; sourceTree = ""; }; 9FB4F169D653296023ED65E6 /* NSESettingsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESettingsProtocol.swift; sourceTree = ""; }; A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenViewModelProtocol.swift; sourceTree = ""; }; + A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModel.swift; sourceTree = ""; }; A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModel.swift; sourceTree = ""; }; A02D1A490944BF01A37586E1 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/SAS.strings; sourceTree = ""; }; A05707BF550D770168A406DB /* LoginViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModelTests.swift; sourceTree = ""; }; @@ -1673,6 +1682,7 @@ A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerProtocol.swift; sourceTree = ""; }; A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = ""; }; A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = ""; }; + A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenCoordinator.swift; sourceTree = ""; }; A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = ""; }; A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = ""; }; @@ -1957,6 +1967,7 @@ E6F5D66F158A6662F953733E /* NotificationSettingsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxy.swift; sourceTree = ""; }; E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerFactory.swift; sourceTree = ""; }; E71C28CF29CD05B6D6AE8580 /* HomeScreenSessionVerificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenSessionVerificationBanner.swift; sourceTree = ""; }; + E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenViewModelProtocol.swift; sourceTree = ""; }; E78FC546F28E045A560F2963 /* EncryptionKeyProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProviderProtocol.swift; sourceTree = ""; }; E8294DB9E95C0C0630418466 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; E8774CF614849664B5B3C2A1 /* UserSessionFlowCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorStateMachine.swift; sourceTree = ""; }; @@ -1980,6 +1991,7 @@ ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -2510,6 +2522,14 @@ path = ServerSelectionScreen; sourceTree = ""; }; + 2E035B978E415C77423FA3C2 /* View */ = { + isa = PBXGroup; + children = ( + ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 2ECFF6B05DAA37EB10DBF7E8 /* UITests */ = { isa = PBXGroup; children = ( @@ -3352,6 +3372,7 @@ C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */, 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */, 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */, + 240610DF32F3213BEC5611D7 /* BlockedUsersScreenViewModelTests.swift */, EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */, 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */, CAD9547E47C58930E2CE8306 /* CallScreenViewModelTests.swift */, @@ -4623,6 +4644,7 @@ 669239C03835CD8B51E0FFDB /* AnalyticsPromptScreen */, 13263FFEA7749D822B51AA90 /* AppLock */, E74CD7681375AD2EAA34D66B /* Authentication */, + EFD4F7FCAAAB3EF45EE7A067 /* BlockedUsersScreen */, 53FB148CD26AFB6A5B9E20B3 /* BugReportScreen */, 1185EECDD07495D65AC84AFC /* CallScreen */, 27F2500AC8736AAE774520C0 /* ComposerToolbar */, @@ -4771,6 +4793,18 @@ path = StartChatScreen; sourceTree = ""; }; + EFD4F7FCAAAB3EF45EE7A067 /* BlockedUsersScreen */ = { + isa = PBXGroup; + children = ( + A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */, + 8642512079EEFD622E3AA66B /* BlockedUsersScreenModels.swift */, + A010B8EAD1A9F6B4686DF2F4 /* BlockedUsersScreenViewModel.swift */, + E76A706B3EEA32B882DA5E2D /* BlockedUsersScreenViewModelProtocol.swift */, + 2E035B978E415C77423FA3C2 /* View */, + ); + path = BlockedUsersScreen; + sourceTree = ""; + }; F12966DF3DA87FEF21348D60 /* InviteUsersScreen */ = { isa = PBXGroup; children = ( @@ -5393,6 +5427,7 @@ 3042527CB344A9EF1157FC26 /* AudioRecorderStateTests.swift in Sources */, 192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */, 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */, + CEAEA57B7665C8E790599A78 /* BlockedUsersScreenViewModelTests.swift in Sources */, 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */, 366D5BFE52CB79E804C7D095 /* CallScreenViewModelTests.swift in Sources */, @@ -5590,6 +5625,11 @@ A4B0BAD62A12ED76BD611B79 /* BadgeView.swift in Sources */, 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */, EB9F4688006B52E69DF5358F /* BlankFormCoordinator.swift in Sources */, + 369BF960E52BBEE61F8A5BD1 /* BlockedUsersScreen.swift in Sources */, + 95E7B236F7116CACE05A6BC9 /* BlockedUsersScreenCoordinator.swift in Sources */, + 8C91D242BEEC657FABCC0B95 /* BlockedUsersScreenModels.swift in Sources */, + 934051B17A884AB0635DF81B /* BlockedUsersScreenViewModel.swift in Sources */, + A4B123C635F70DDD4BC2FAC9 /* BlockedUsersScreenViewModelProtocol.swift in Sources */, 5EE1D4E316D66943E97FDCF2 /* BloomView.swift in Sources */, B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */, E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 00b13b500..def40a46a 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -336,6 +336,7 @@ "screen_app_lock_setup_pin_mismatch_dialog_title" = "PINs don't match"; "screen_app_lock_signout_alert_message" = "You’ll need to re-login and create a new PIN to proceed"; "screen_app_lock_signout_alert_title" = "You are being signed out"; +"screen_blocked_users_empty" = "You have no blocked users"; "screen_blocked_users_unblocking" = "Unblocking..."; "screen_bug_report_attach_screenshot" = "Attach screenshot"; "screen_bug_report_contact_me" = "You may contact me if you have any follow up questions."; @@ -491,6 +492,16 @@ "screen_room_attachment_source_location" = "Location"; "screen_room_attachment_source_poll" = "Poll"; "screen_room_attachment_text_formatting" = "Text Formatting"; +"screen_room_change_permissions_administrators" = "Admins only"; +"screen_room_change_permissions_ban_people" = "Ban people"; +"screen_room_change_permissions_delete_messages" = "Delete messages"; +"screen_room_change_permissions_invite_people" = "Invite people"; +"screen_room_change_permissions_moderators" = "Admins and moderators"; +"screen_room_change_permissions_remove_people" = "Remove people"; +"screen_room_change_permissions_room_avatar" = "Change Room Avatar"; +"screen_room_change_permissions_room_name" = "Change Room Name"; +"screen_room_change_permissions_room_topic" = "Change Room Topic"; +"screen_room_change_permissions_send_messages" = "Send messages"; "screen_room_change_role_confirm_add_admin_description" = "You will not be able to undo this action. You are promoting the user to have the same power level as you."; "screen_room_change_role_confirm_add_admin_title" = "Add Admin?"; "screen_room_change_role_confirm_demote_self_action" = "Demote"; @@ -736,6 +747,10 @@ "screen_login_subtitle" = "Matrix is an open network for secure, decentralised communication."; "screen_report_content_block_user" = "Block user"; "screen_room_attachment_source_camera_photo" = "Take photo"; +"screen_room_change_permissions_everyone" = "Everyone"; +"screen_room_change_permissions_member_moderation" = "Member moderation"; +"screen_room_change_permissions_messages_and_content" = "Messages and content"; +"screen_room_change_permissions_room_details" = "Room details"; "screen_room_details_invite_people_title" = "Invite people"; "screen_room_details_leave_conversation_title" = "Leave conversation"; "screen_room_details_leave_room_title" = "Leave room"; diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 13e1ccdd6..fce528ce7 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -419,7 +419,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, Task { let credentials = SoftLogoutScreenCredentials(userID: userSession.userID, homeserverName: userSession.homeserver, - userDisplayName: userSession.clientProxy.userDisplayName.value ?? "", + userDisplayName: userSession.clientProxy.userDisplayNamePublisher.value ?? "", deviceID: userSession.deviceID) let authenticationService = AuthenticationServiceProxy(userSessionStore: userSessionStore, @@ -866,7 +866,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationCoordinatorDelegate, // Be a good citizen, run for a max of 10 SS responses or 10 seconds // An SS request will time out after 30 seconds if no new data is available backgroundRefreshSyncObserver = userSession.clientProxy - .callbacks + .actionsPublisher .filter(\.isSyncUpdate) .collect(.byTimeOrCount(DispatchQueue.main, .seconds(10), 10)) .sink(receiveValue: { [weak self] _ in diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index f47eab276..20c6813d5 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -128,6 +128,8 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { bugReportFlowCoordinator?.start() case .about: presentLegalInformationScreen() + case .blockedUsers: + presentBlockedUsersScreen() case .sessionVerification: presentSessionVerificationScreen() case .accountSessions: @@ -203,6 +205,12 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.push(LegalInformationScreenCoordinator(appSettings: parameters.appSettings)) } + private func presentBlockedUsersScreen() { + let coordinator = BlockedUsersScreenCoordinator(parameters: .init(clientProxy: parameters.userSession.clientProxy, + userIndicatorController: parameters.userIndicatorController)) + navigationStackCoordinator.push(coordinator) + } + private func presentSessionVerificationScreen() { guard let sessionVerificationController = parameters.userSession.sessionVerificationController else { fatalError("The sessionVerificationController should aways be valid at this point") diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 403cc415c..189f7312d 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -830,6 +830,8 @@ internal enum L10n { internal static func screenAppLockSubtitleWrongPin(_ p1: Int) -> String { return L10n.tr("Localizable", "screen_app_lock_subtitle_wrong_pin", p1) } + /// You have no blocked users + internal static var screenBlockedUsersEmpty: String { return L10n.tr("Localizable", "screen_blocked_users_empty") } /// Unblock internal static var screenBlockedUsersUnblockAlertAction: String { return L10n.tr("Localizable", "screen_blocked_users_unblock_alert_action") } /// You'll be able to see all messages from them again. @@ -1205,6 +1207,34 @@ internal enum L10n { internal static var screenRoomAttachmentSourcePoll: String { return L10n.tr("Localizable", "screen_room_attachment_source_poll") } /// Text Formatting internal static var screenRoomAttachmentTextFormatting: String { return L10n.tr("Localizable", "screen_room_attachment_text_formatting") } + /// Admins only + internal static var screenRoomChangePermissionsAdministrators: String { return L10n.tr("Localizable", "screen_room_change_permissions_administrators") } + /// Ban people + internal static var screenRoomChangePermissionsBanPeople: String { return L10n.tr("Localizable", "screen_room_change_permissions_ban_people") } + /// Delete messages + internal static var screenRoomChangePermissionsDeleteMessages: String { return L10n.tr("Localizable", "screen_room_change_permissions_delete_messages") } + /// Everyone + internal static var screenRoomChangePermissionsEveryone: String { return L10n.tr("Localizable", "screen_room_change_permissions_everyone") } + /// Invite people + internal static var screenRoomChangePermissionsInvitePeople: String { return L10n.tr("Localizable", "screen_room_change_permissions_invite_people") } + /// Member moderation + internal static var screenRoomChangePermissionsMemberModeration: String { return L10n.tr("Localizable", "screen_room_change_permissions_member_moderation") } + /// Messages and content + internal static var screenRoomChangePermissionsMessagesAndContent: String { return L10n.tr("Localizable", "screen_room_change_permissions_messages_and_content") } + /// Admins and moderators + internal static var screenRoomChangePermissionsModerators: String { return L10n.tr("Localizable", "screen_room_change_permissions_moderators") } + /// Remove people + internal static var screenRoomChangePermissionsRemovePeople: String { return L10n.tr("Localizable", "screen_room_change_permissions_remove_people") } + /// Change Room Avatar + internal static var screenRoomChangePermissionsRoomAvatar: String { return L10n.tr("Localizable", "screen_room_change_permissions_room_avatar") } + /// Room details + internal static var screenRoomChangePermissionsRoomDetails: String { return L10n.tr("Localizable", "screen_room_change_permissions_room_details") } + /// Change Room Name + internal static var screenRoomChangePermissionsRoomName: String { return L10n.tr("Localizable", "screen_room_change_permissions_room_name") } + /// Change Room Topic + internal static var screenRoomChangePermissionsRoomTopic: String { return L10n.tr("Localizable", "screen_room_change_permissions_room_topic") } + /// Send messages + internal static var screenRoomChangePermissionsSendMessages: String { return L10n.tr("Localizable", "screen_room_change_permissions_send_messages") } /// You will not be able to undo this action. You are promoting the user to have the same power level as you. internal static var screenRoomChangeRoleConfirmAddAdminDescription: String { return L10n.tr("Localizable", "screen_room_change_role_confirm_add_admin_description") } /// Add Admin? diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 1aa5df358..c456c0c7c 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1892,16 +1892,16 @@ class RoomProxyMock: RoomProxyProtocol { var name: String? var topic: String? var avatarURL: URL? - var members: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { - get { return underlyingMembers } - set(value) { underlyingMembers = value } + var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { + get { return underlyingMembersPublisher } + set(value) { underlyingMembersPublisher = value } } - var underlyingMembers: CurrentValuePublisher<[RoomMemberProxyProtocol], Never>! - var typingMembers: CurrentValuePublisher<[String], Never> { - get { return underlyingTypingMembers } - set(value) { underlyingTypingMembers = value } + var underlyingMembersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never>! + var typingMembersPublisher: CurrentValuePublisher<[String], Never> { + get { return underlyingTypingMembersPublisher } + set(value) { underlyingTypingMembersPublisher = value } } - var underlyingTypingMembers: CurrentValuePublisher<[String], Never>! + var underlyingTypingMembersPublisher: CurrentValuePublisher<[String], Never>! var joinedMembersCount: Int { get { return underlyingJoinedMembersCount } set(value) { underlyingJoinedMembersCount = value } @@ -1912,11 +1912,11 @@ class RoomProxyMock: RoomProxyProtocol { set(value) { underlyingActiveMembersCount = value } } var underlyingActiveMembersCount: Int! - var actions: AnyPublisher { - get { return underlyingActions } - set(value) { underlyingActions = value } + var actionsPublisher: AnyPublisher { + get { return underlyingActionsPublisher } + set(value) { underlyingActionsPublisher = value } } - var underlyingActions: AnyPublisher! + var underlyingActionsPublisher: AnyPublisher! var timeline: TimelineProxyProtocol { get { return underlyingTimeline } set(value) { underlyingTimeline = value } diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index 0cd3fe0c6..f707eb6a1 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -69,15 +69,15 @@ extension RoomProxyMock { ownUserID = configuration.ownUserID - members = CurrentValueSubject(configuration.members).asCurrentValuePublisher() - typingMembers = CurrentValueSubject([]).asCurrentValuePublisher() + membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher() + typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher() joinedMembersCount = configuration.members.filter { $0.membership == .join }.count activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count updateMembersClosure = { } acceptInvitationClosure = { .success(()) } - underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher() + underlyingActionsPublisher = Empty(completeImmediately: false).eraseToAnyPublisher() setNameClosure = { _ in .success(()) } setTopicClosure = { _ in .success(()) } getMemberUserIDReturnValue = .success(configuration.memberForID) diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index ef0a68514..2dec3ab73 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -210,6 +210,7 @@ enum A11yIdentifiers { let screenLock = "settings-screen_lock" let reportBug = "settings-report_bug" let about = "settings_about" + let blockedUsers = "settings_blocked-users" let advancedSettings = "settings_advanced-settings" let developerOptions = "settings_developer-options" let logout = "settings-logout" diff --git a/ElementX/Sources/Other/AvatarSize.swift b/ElementX/Sources/Other/AvatarSize.swift index a031bbfe8..551ede302 100644 --- a/ElementX/Sources/Other/AvatarSize.swift +++ b/ElementX/Sources/Other/AvatarSize.swift @@ -53,6 +53,7 @@ enum UserAvatarSizeOnScreen { case readReceiptSheet case editUserDetails case suggestions + case blockedUsers var value: CGFloat { switch self { @@ -66,6 +67,8 @@ enum UserAvatarSizeOnScreen { return 32 case .suggestions: return 32 + case .blockedUsers: + return 32 case .settings: return 52 case .roomDetails: diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift new file mode 100644 index 000000000..14808b3f6 --- /dev/null +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift @@ -0,0 +1,49 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +struct BlockedUsersScreenCoordinatorParameters { + let clientProxy: ClientProxyProtocol + let userIndicatorController: UserIndicatorControllerProtocol +} + +enum BlockedUsersScreenCoordinatorAction { } + +final class BlockedUsersScreenCoordinator: CoordinatorProtocol { + private let viewModel: BlockedUsersScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: BlockedUsersScreenCoordinatorParameters) { + viewModel = BlockedUsersScreenViewModel(clientProxy: parameters.clientProxy, + userIndicatorController: parameters.userIndicatorController) + } + + func stop() { + viewModel.stop() + } + + func toPresentable() -> AnyView { + AnyView(BlockedUsersScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift new file mode 100644 index 000000000..6ab4d3280 --- /dev/null +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift @@ -0,0 +1,39 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum BlockedUsersScreenViewModelAction { } + +struct BlockedUsersScreenViewState: BindableState { + var blockedUsers: [String] + var processingUserID: String? + + var bindings = BlockedUsersScreenViewStateBindings() +} + +struct BlockedUsersScreenViewStateBindings { + var alertInfo: AlertInfo? +} + +enum BlockedUsersScreenViewAction { + case unblockUser(userID: String) +} + +enum BlockedUsersScreenViewStateAlertType: Hashable { + case unblock + case error +} diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift new file mode 100644 index 000000000..08d6afabe --- /dev/null +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift @@ -0,0 +1,103 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import SwiftUI + +typealias BlockedUsersScreenViewModelType = StateStoreViewModel + +class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsersScreenViewModelProtocol { + let clientProxy: ClientProxyProtocol + let userIndicatorController: UserIndicatorControllerProtocol + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(clientProxy: ClientProxyProtocol, + userIndicatorController: UserIndicatorControllerProtocol) { + self.clientProxy = clientProxy + self.userIndicatorController = userIndicatorController + + super.init(initialViewState: BlockedUsersScreenViewState(blockedUsers: clientProxy.ignoredUsersPublisher.value ?? [])) + + showLoadingIndicator() + + clientProxy.ignoredUsersPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] blockedUsers in + guard let self else { return } + + if let blockedUsers { + hideLoadingIndicator() + state.blockedUsers = blockedUsers + } + } + .store(in: &cancellables) + } + + // MARK: - Public + + override func process(viewAction: BlockedUsersScreenViewAction) { + switch viewAction { + case .unblockUser(let userID): + state.bindings.alertInfo = .init(id: .unblock, + title: L10n.screenBlockedUsersUnblockAlertTitle, + message: L10n.screenBlockedUsersUnblockAlertDescription, + primaryButton: .init(title: L10n.screenBlockedUsersUnblockAlertAction, role: .destructive) { [weak self] in + self?.unblockUser(userID) + }, + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + } + } + + func stop() { + hideLoadingIndicator() + } + + // MARK: - Private + + private func unblockUser(_ userID: String) { + showLoadingIndicator() + state.processingUserID = userID + + Task { + if case .failure = await clientProxy.unignoreUser(userID) { + state.bindings.alertInfo = .init(id: .error) + } + + state.processingUserID = nil + hideLoadingIndicator() + } + } + + // MARK: Loading indicator + + private static let loadingIndicatorIdentifier = "BlockedUsersLoading" + + private func showLoadingIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: true), + title: L10n.commonLoading, + persistent: true), + delay: .milliseconds(100)) + } + + private func hideLoadingIndicator() { + userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } +} diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModelProtocol.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModelProtocol.swift new file mode 100644 index 000000000..ae7e425ba --- /dev/null +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModelProtocol.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine + +@MainActor +protocol BlockedUsersScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: BlockedUsersScreenViewModelType.Context { get } + + func stop() +} diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift new file mode 100644 index 000000000..cdd253e53 --- /dev/null +++ b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift @@ -0,0 +1,73 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Compound +import SwiftUI + +struct BlockedUsersScreen: View { + @ObservedObject var context: BlockedUsersScreenViewModel.Context + + var body: some View { + content + .compoundList() + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(L10n.commonBlockedUsers) + .alert(item: $context.alertInfo) + .disabled(context.viewState.processingUserID != nil) + } + + // MARK: - Private + + @ViewBuilder + private var content: some View { + if context.viewState.blockedUsers.isEmpty { + Text(L10n.screenBlockedUsersEmpty) + .font(.compound.bodyMD) + .foregroundColor(.compound.textSecondary) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + Form { + ForEach(context.viewState.blockedUsers, id: \.self) { userID in + ListRow(label: .avatar(title: userID, icon: avatar(for: userID)), + details: .isWaiting(context.viewState.processingUserID == userID), + kind: .button(action: { context.send(viewAction: .unblockUser(userID: userID)) })) + } + } + } + } + + private func avatar(for userID: String) -> some View { + LoadableAvatarImage(url: nil, + name: String(userID.dropFirst()), + contentID: userID, + avatarSize: .user(on: .blockedUsers), + imageProvider: nil) + .accessibilityHidden(true) + } +} + +// MARK: - Previews + +struct BlockedUsersScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = BlockedUsersScreenViewModel(clientProxy: MockClientProxy(userID: RoomMemberProxyMock.mockMe.userID), + userIndicatorController: UserIndicatorControllerMock()) + + static var previews: some View { + NavigationStack { + BlockedUsersScreen(context: viewModel.context) + } + } +} diff --git a/ElementX/Sources/Screens/ComposerToolbar/CompletionSuggestionService.swift b/ElementX/Sources/Screens/ComposerToolbar/CompletionSuggestionService.swift index 79be7919d..bcda41b13 100644 --- a/ElementX/Sources/Screens/ComposerToolbar/CompletionSuggestionService.swift +++ b/ElementX/Sources/Screens/ComposerToolbar/CompletionSuggestionService.swift @@ -27,7 +27,7 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol { init(roomProxy: RoomProxyProtocol) { self.roomProxy = roomProxy suggestionsPublisher = suggestionTriggerSubject - .combineLatest(roomProxy.members) + .combineLatest(roomProxy.membersPublisher) .map { [weak self] suggestionPattern, members -> [SuggestionItem] in guard let self, let suggestionPattern else { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index e4a62dc0e..40adca1b4 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -54,12 +54,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol super.init(initialViewState: .init(userID: userSession.userID), imageProvider: userSession.mediaProvider) - userSession.clientProxy.userAvatarURL + userSession.clientProxy.userAvatarURLPublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userAvatarURL, on: self) .store(in: &cancellables) - userSession.clientProxy.userDisplayName + userSession.clientProxy.userDisplayNamePublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userDisplayName, on: self) .store(in: &cancellables) @@ -256,7 +256,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol MXLog.info("Account not migrated, setting view room list mode to \"\(state.roomListMode)\"") - migrationCancellable = userSession.clientProxy.callbacks + migrationCancellable = userSession.clientProxy.actionsPublisher .receive(on: DispatchQueue.main) .sink { [weak self] callback in guard let self, case .receivedSyncUpdate = callback else { return } diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift index b1f4cf680..1d19d87c1 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift @@ -118,7 +118,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr hideLoader() } - roomProxy.members + roomProxy.membersPublisher .filter { !$0.isEmpty } .first() .receive(on: DispatchQueue.main) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index d224f5ec1..b9c725241 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -132,7 +132,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr // MARK: - Private private func setupRoomSubscription() { - roomProxy.actions + roomProxy.actionsPublisher .filter { $0 == .roomInfoUpdate } .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in @@ -167,7 +167,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr return } - roomProxy.members + roomProxy.membersPublisher .receive(on: DispatchQueue.main) .sink { [weak self] members in guard let self else { return } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index de5461f03..d3ac46398 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -80,7 +80,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe hideLoader() } - roomProxy.members + roomProxy.membersPublisher .filter { !$0.isEmpty } .receive(on: DispatchQueue.main) .sink { [weak self] members in diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift index 150a398e7..722e4c3ab 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift @@ -31,12 +31,12 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM self.roomProxy = roomProxy super.init(initialViewState: RoomRolesAndPermissionsScreenViewState()) - roomProxy.members.sink { [weak self] members in + roomProxy.membersPublisher.sink { [weak self] members in self?.updateMembers(members) } .store(in: &cancellables) - updateMembers(roomProxy.members.value) + updateMembers(roomProxy.membersPublisher.value) } // MARK: - Public diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index ed9b40ca7..43a01f02d 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -292,7 +292,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .store(in: &cancellables) roomProxy - .actions + .actionsPublisher .filter { $0 == .roomInfoUpdate } .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in @@ -319,7 +319,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .weakAssign(to: \.state.showReadReceipts, on: self) .store(in: &cancellables) - roomProxy.members + roomProxy.membersPublisher .map { members in members.reduce(into: [String: RoomMemberState]()) { dictionary, member in dictionary[member.userID] = RoomMemberState(displayName: member.displayName, avatarURL: member.avatarURL) @@ -329,7 +329,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .weakAssign(to: \.state.members, on: self) .store(in: &cancellables) - roomProxy.typingMembers + roomProxy.typingMembersPublisher .receive(on: DispatchQueue.main) .filter { [weak self] _ in self?.appSettings.sharePresence ?? false } .weakAssign(to: \.state.typingMembers, on: self) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift index 3d2256169..cc3d8afd5 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenCoordinator.swift @@ -32,6 +32,7 @@ enum SettingsScreenCoordinatorAction { case appLock case bugReport case about + case blockedUsers case sessionVerification case accountSessions case notifications @@ -74,6 +75,8 @@ final class SettingsScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.bugReport) case .about: actionsSubject.send(.about) + case .blockedUsers: + actionsSubject.send(.blockedUsers) case .sessionVerification: actionsSubject.send(.sessionVerification) case .secureBackup: diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index d3b4f1e0a..ae931ec08 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -25,6 +25,7 @@ enum SettingsScreenViewModelAction { case appLock case reportBug case about + case blockedUsers case sessionVerification case secureBackup case accountSessionsList @@ -61,6 +62,7 @@ enum SettingsScreenViewAction { case appLock case reportBug case about + case blockedUsers case sessionVerification case secureBackup case accountSessionsList diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 0411575c3..a74d296b9 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -34,12 +34,12 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo showDeveloperOptions: appSettings.isDevelopmentBuild), imageProvider: userSession.mediaProvider) - userSession.clientProxy.userAvatarURL + userSession.clientProxy.userAvatarURLPublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userAvatarURL, on: self) .store(in: &cancellables) - userSession.clientProxy.userDisplayName + userSession.clientProxy.userDisplayNamePublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userDisplayName, on: self) .store(in: &cancellables) @@ -94,6 +94,8 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo actionsSubject.send(.reportBug) case .about: actionsSubject.send(.about) + case .blockedUsers: + actionsSubject.send(.blockedUsers) case .logout: actionsSubject.send(.logout) case .sessionVerification: diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index 606fd2b0f..a8ac247ad 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -143,6 +143,13 @@ struct SettingsScreen: View { context.send(viewAction: .about) }) .accessibilityIdentifier(A11yIdentifiers.settingsScreen.about) + + ListRow(label: .default(title: L10n.commonBlockedUsers, + icon: \.block), + kind: .navigationLink { + context.send(viewAction: .blockedUsers) + }) + .accessibilityIdentifier(A11yIdentifiers.settingsScreen.blockedUsers) } } diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift index 7d0f1ded7..d9ea18e34 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift @@ -38,22 +38,22 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe super.init(initialViewState: UserDetailsEditScreenViewState(userID: clientProxy.userID, bindings: .init()), imageProvider: mediaProvider) - clientProxy.userAvatarURL + clientProxy.userAvatarURLPublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.currentAvatarURL, on: self) .store(in: &cancellables) - clientProxy.userAvatarURL + clientProxy.userAvatarURLPublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.selectedAvatarURL, on: self) .store(in: &cancellables) - clientProxy.userDisplayName + clientProxy.userDisplayNamePublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.currentDisplayName, on: self) .store(in: &cancellables) - clientProxy.userDisplayName + clientProxy.userDisplayNamePublisher .receive(on: DispatchQueue.main) .sink { [weak self] displayName in guard let self else { return } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index d4edfafde..68fb34ee7 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -38,6 +38,9 @@ class ClientProxy: ClientProxyProtocol { // periphery: ignore - only for retain private var syncServiceStateUpdateTaskHandle: TaskHandle? + // periphery:ignore - required for instance retention in the rust codebase + private var ignoredUsersListenerTaskHandle: TaskHandle? + private var delegateHandle: TaskHandle? // These following summary providers both operate on the same allRooms() list but @@ -69,15 +72,21 @@ class ClientProxy: ClientProxyProtocol { private var loadCachedAvatarURLTask: Task? private let userAvatarURLSubject = CurrentValueSubject(nil) - var userAvatarURL: CurrentValuePublisher { + var userAvatarURLPublisher: CurrentValuePublisher { userAvatarURLSubject.asCurrentValuePublisher() } private let userDisplayNameSubject = CurrentValueSubject(nil) - var userDisplayName: CurrentValuePublisher { + var userDisplayNamePublisher: CurrentValuePublisher { userDisplayNameSubject.asCurrentValuePublisher() } + private let ignoredUsersSubject = CurrentValueSubject<[String]?, Never>(nil) + var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { + ignoredUsersSubject + .asCurrentValuePublisher() + } + private var cancellables = Set() /// Will be `true` whilst the app cleans up and forces a logout. Prevents the sync service from restarting @@ -92,7 +101,10 @@ class ClientProxy: ClientProxyProtocol { } } - let callbacks = PassthroughSubject() + private let actionsSubject = PassthroughSubject() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } private let loadingStateSubject = CurrentValueSubject(.notLoading) var loadingStatePublisher: CurrentValuePublisher { @@ -119,7 +131,7 @@ class ClientProxy: ClientProxyProtocol { delegateHandle = client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in self?.hasEncounteredAuthError = true - self?.callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout)) + self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout)) }) networkMonitor.reachabilityPublisher @@ -135,6 +147,10 @@ class ClientProxy: ClientProxyProtocol { await configureAppService() loadUserAvatarURLFromCache() + + ignoredUsersListenerTaskHandle = client.subscribeToIgnoredUsers(listener: IgnoredUsersListenerProxy { [weak self] ignoredUsers in + self?.ignoredUsersSubject.send(ignoredUsers) + }) } var userID: String { @@ -472,6 +488,28 @@ class ClientProxy: ClientProxyProtocol { } } + // MARK: - Ignored users + + func ignoreUser(_ userID: String) async -> Result { + do { + try await client.ignoreUser(userId: userID) + return .success(()) + } catch { + MXLog.error("Failed ignoring user with error: \(error)") + return .failure(.failedIgnoringUser) + } + } + + func unignoreUser(_ userID: String) async -> Result { + do { + try await client.unignoreUser(userId: userID) + return .success(()) + } catch { + MXLog.error("Failed unignoring user with error: \(error)") + return .failure(.failedUnignoringUser) + } + } + // MARK: Private private func loadUserAvatarURLFromCache() { @@ -570,7 +608,11 @@ class ClientProxy: ClientProxyProtocol { } // Hide the sync spinner as soon as we get any update back - callbacks.send(.receivedSyncUpdate) + actionsSubject.send(.receivedSyncUpdate) + + if ignoredUsersSubject.value == nil { + updateIgnoredUsers() + } }) } @@ -620,6 +662,17 @@ class ClientProxy: ClientProxyProtocol { return (nil, nil) } } + + private func updateIgnoredUsers() { + Task { + do { + let ignoredUsers = try await client.ignoredUsers() + ignoredUsersSubject.send(ignoredUsers) + } catch { + MXLog.error("Failed fetching ignored users with error: \(error)") + } + } + } } extension ClientProxy: MediaLoaderProtocol { @@ -690,3 +743,15 @@ private class ClientDelegateWrapper: ClientDelegate { MXLog.info("Delegating session updates to the ClientSessionDelegate.") } } + +private class IgnoredUsersListenerProxy: IgnoredUsersListener { + private let onUpdateClosure: ([String]) -> Void + + init(onUpdateClosure: @escaping ([String]) -> Void) { + self.onUpdateClosure = onUpdateClosure + } + + func call(ignoredUserIds: [String]) { + onUpdateClosure(ignoredUserIds) + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 76368c719..a438463c1 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -18,7 +18,7 @@ import Combine import Foundation import MatrixRustSDK -enum ClientProxyCallback { +enum ClientProxyAction { case receivedSyncUpdate case receivedAuthError(isSoftLogout: Bool) @@ -50,6 +50,8 @@ enum ClientProxyError: Error { case failedGettingUserProfile case failedSettingUserAvatar case failedCheckingIsLastDevice(Error?) + case failedIgnoringUser + case failedUnignoringUser } enum SlidingSyncConstants { @@ -70,7 +72,7 @@ struct PusherConfiguration { } protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { - var callbacks: PassthroughSubject { get } + var actionsPublisher: AnyPublisher { get } var loadingStatePublisher: CurrentValuePublisher { get } @@ -80,9 +82,12 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var homeserver: String { get } - var userDisplayName: CurrentValuePublisher { get } + var userDisplayNamePublisher: CurrentValuePublisher { get } - var userAvatarURL: CurrentValuePublisher { get } + var userAvatarURLPublisher: CurrentValuePublisher { get } + + /// We delay fetching this until after the first sync. Nil until then + var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { get } var pusherNotificationClientIdentifier: String? { get } @@ -134,4 +139,10 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { func searchUsers(searchTerm: String, limit: UInt) async -> Result func profile(for userID: String) async -> Result + + // MARK: - Ignored users + + func ignoreUser(_ userID: String) async -> Result + + func unignoreUser(_ userID: String) async -> Result } diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index 1150498be..b17051da3 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -19,7 +19,10 @@ import Foundation import MatrixRustSDK class MockClientProxy: ClientProxyProtocol { - let callbacks = PassthroughSubject() + let actionsSubject = PassthroughSubject() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } let loadingStatePublisher = CurrentValuePublisher(.notLoading) @@ -34,9 +37,14 @@ class MockClientProxy: ClientProxyProtocol { var inviteSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider() - var userAvatarURL: CurrentValuePublisher { CurrentValueSubject(nil).asCurrentValuePublisher() } + var userAvatarURLPublisher: CurrentValuePublisher { CurrentValueSubject(nil).asCurrentValuePublisher() } - var userDisplayName: CurrentValuePublisher { CurrentValueSubject("User display name").asCurrentValuePublisher() } + var userDisplayNamePublisher: CurrentValuePublisher { CurrentValueSubject("User display name").asCurrentValuePublisher() } + + var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { + let ignoredUsers = [RoomMemberProxyMock].allMembers.map(\.userID) + return CurrentValueSubject<[String]?, Never>(ignoredUsers).asCurrentValuePublisher() + } var notificationSettings: NotificationSettingsProxyProtocol = NotificationSettingsProxyMock(with: .init()) @@ -166,4 +174,12 @@ class MockClientProxy: ClientProxyProtocol { getProfileCalled = true return getProfileResult } + + func ignoreUser(_ userID: String) async -> Result { + .failure(.failedIgnoringUser) + } + + func unignoreUser(_ userID: String) async -> Result { + .failure(.failedUnignoringUser) + } } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index c95a65040..5852b7d78 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -39,17 +39,17 @@ class RoomProxy: RoomProxyProtocol { private var subscribedForUpdates = false private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) - var members: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { + var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { membersSubject.asCurrentValuePublisher() } private let typingMembersSubject = CurrentValueSubject<[String], Never>([]) - var typingMembers: CurrentValuePublisher<[String], Never> { + var typingMembersPublisher: CurrentValuePublisher<[String], Never> { typingMembersSubject.asCurrentValuePublisher() } private let actionsSubject = PassthroughSubject() - var actions: AnyPublisher { + var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } @@ -217,7 +217,7 @@ class RoomProxy: RoomProxyProtocol { } func getMember(userID: String) async -> Result { - if let member = members.value.filter({ $0.userID == userID }).first { + if let member = membersPublisher.value.filter({ $0.userID == userID }).first { return .success(member) } @@ -473,23 +473,21 @@ class RoomProxy: RoomProxyProtocol { } private func subscribeToTypingNotifications() { - Task { - typingNotificationObservationToken = await room.subscribeToTypingNotifications(listener: RoomTypingNotificationUpdateListener { [weak self] typingUserIDs in - guard let self else { return } - - MXLog.info("Received typing notification update, typingUsers: \(typingUserIDs)") - - let typingMembers = typingUserIDs.compactMap { userID in - if let member = self.members.value.filter({ $0.userID == userID }).first { - return member.displayName ?? member.userID - } else { - return userID - } + typingNotificationObservationToken = room.subscribeToTypingNotifications(listener: RoomTypingNotificationUpdateListener { [weak self] typingUserIDs in + guard let self else { return } + + MXLog.info("Received typing notification update, typingUsers: \(typingUserIDs)") + + let typingMembers = typingUserIDs.compactMap { userID in + if let member = self.membersPublisher.value.filter({ $0.userID == userID }).first { + return member.displayName ?? member.userID + } else { + return userID } - - typingMembersSubject.send(typingMembers) - }) - } + } + + typingMembersSubject.send(typingMembers) + }) } } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 840026696..000e909f3 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -62,15 +62,15 @@ protocol RoomProxyProtocol { var avatarURL: URL? { get } - var members: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get } + var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get } - var typingMembers: CurrentValuePublisher<[String], Never> { get } + var typingMembersPublisher: CurrentValuePublisher<[String], Never> { get } var joinedMembersCount: Int { get } var activeMembersCount: Int { get } - var actions: AnyPublisher { get } + var actionsPublisher: AnyPublisher { get } var timeline: TimelineProxyProtocol { get } @@ -167,6 +167,6 @@ extension RoomProxyProtocol { func members() async -> [RoomMemberProxyProtocol]? { await updateMembers() - return members.value + return membersPublisher.value } } diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index 24c1eeabb..1a2ef2e5e 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -60,7 +60,7 @@ class UserSession: UserSessionProtocol { self.mediaProvider = mediaProvider self.voiceMessageMediaManager = voiceMessageMediaManager - clientProxy.callbacks + clientProxy.actionsPublisher .receive(on: DispatchQueue.main) .sink { [weak self] callback in if callback.isSyncUpdate { @@ -69,7 +69,7 @@ class UserSession: UserSessionProtocol { } .store(in: &cancellables) - authErrorCancellable = clientProxy.callbacks + authErrorCancellable = clientProxy.actionsPublisher .receive(on: DispatchQueue.main) .sink { [weak self] callback in guard let self else { return } diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_blockedUsersScreen.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_blockedUsersScreen.1.png new file mode 100644 index 000000000..98cf94cb7 --- /dev/null +++ b/PreviewTests/__Snapshots__/PreviewTests/test_blockedUsersScreen.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9fdecb8d69665bd20ce42c29a94df8fd912f1c3688207c02c2bf0ef955e80d1 +size 144194 diff --git a/PreviewTests/__Snapshots__/PreviewTests/test_settingsScreen.1.png b/PreviewTests/__Snapshots__/PreviewTests/test_settingsScreen.1.png index 46840bfd4..40935f2d2 100644 --- a/PreviewTests/__Snapshots__/PreviewTests/test_settingsScreen.1.png +++ b/PreviewTests/__Snapshots__/PreviewTests/test_settingsScreen.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0e4f20a3da82d547d5802aa2dc96db5de6520792be495069ef9c7a80168c655 -size 171367 +oid sha256:ef88023b3f423e15d540d755d987edf4a5d7ea2580a30509d79d23ff93235c13 +size 174925 diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift index d727dad61..d61ef99b5 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift @@ -29,11 +29,12 @@ enum TemplateScreenCoordinatorAction { final class TemplateScreenCoordinator: CoordinatorProtocol { private let parameters: TemplateScreenCoordinatorParameters - private var viewModel: TemplateScreenViewModelProtocol - private let actionsSubject: PassthroughSubject = .init() - private var cancellables = Set() + private let viewModel: TemplateScreenViewModelProtocol - var actions: AnyPublisher { + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } @@ -44,7 +45,7 @@ final class TemplateScreenCoordinator: CoordinatorProtocol { } func start() { - viewModel.actions.sink { [weak self] action in + viewModel.actionsPublisher.sink { [weak self] action in MXLog.info("Coordinator: received view model action: \(action)") guard let self else { return } diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift index f39432759..73fcbb870 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift @@ -20,9 +20,8 @@ import SwiftUI typealias TemplateScreenViewModelType = StateStoreViewModel class TemplateScreenViewModel: TemplateScreenViewModelType, TemplateScreenViewModelProtocol { - private var actionsSubject: PassthroughSubject = .init() - - var actions: AnyPublisher { + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModelProtocol.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModelProtocol.swift index 4bb8ee708..f8cea4a5e 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModelProtocol.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModelProtocol.swift @@ -18,6 +18,6 @@ import Combine @MainActor protocol TemplateScreenViewModelProtocol { - var actions: AnyPublisher { get } + var actionsPublisher: AnyPublisher { get } var context: TemplateScreenViewModelType.Context { get } } diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png index 206bb8c88..8953bebb9 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3c08ea0ffa71cf6eb554ddc394a7db01325588bb93e3ab094f18a76f669dd9e -size 138739 +oid sha256:1e6b1f4f3a3d38a5a55557a0458c46d2ac4c44fc438230ce059fa8b323a8c8ef +size 145116 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png index baf1c442e..0a395704f 100644 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25076c598482ee48360108f0acc67cde09d191a51c296afb763018bbd748291f -size 164268 +oid sha256:13a413c5db74eb8127c9c2c0ae6d0f5633bc40f5c100f7ae679f314785af7116 +size 172723 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png index c9cb63881..5fc5a5bac 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:400e3eba5557dd4138c7b8ff2cc305dd95e74c3e1b86f2bd5675a5c7c8dcf656 -size 148563 +oid sha256:30bebd1f04d98e9a62567e8dc51762b8d0e25baf437a296210652a16fe522c52 +size 155955 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png index 3e90a1b8d..14a39f96e 100644 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.settings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1eb3e2304004c1d0b21f8431678fdbf9c7e751c57ff4c686706d37fec9bc0d3 -size 184449 +oid sha256:666546d6087d733ad951d2d3a64eae1f6b2a2a43962d1f724b2b1d3da43531e0 +size 190544 diff --git a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift new file mode 100644 index 000000000..4c9c7bddb --- /dev/null +++ b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift @@ -0,0 +1,32 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Combine +import XCTest + +@testable import ElementX + +@MainActor +class BlockedUsersScreenViewModelTests: XCTestCase { + func testInitialState() { + let clientProxy = MockClientProxy(userID: RoomMemberProxyMock.mockMe.userID) + + let viewModel = BlockedUsersScreenViewModel(clientProxy: clientProxy, + userIndicatorController: ServiceLocator.shared.userIndicatorController) + + XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) + } +} diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index 0ec487d17..a75463a80 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -25,7 +25,7 @@ class PillContextTests: XCTestCase { let id = "@test:matrix.org" let proxyMock = RoomProxyMock(with: .init(name: "Test")) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) - proxyMock.members = subject.asCurrentValuePublisher() + proxyMock.membersPublisher = subject.asCurrentValuePublisher() let mock = RoomScreenViewModel(roomProxy: proxyMock, timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), @@ -54,7 +54,7 @@ class PillContextTests: XCTestCase { let id = "@test:matrix.org" let proxyMock = RoomProxyMock(with: .init(name: "Test", ownUserID: id)) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) - proxyMock.members = subject.asCurrentValuePublisher() + proxyMock.membersPublisher = subject.asCurrentValuePublisher() let mock = RoomScreenViewModel(roomProxy: proxyMock, timelineController: MockRoomTimelineController(), mediaProvider: MockMediaProvider(), diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 46456cdd2..f95103d3a 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -396,7 +396,7 @@ class RoomScreenViewModelTests: XCTestCase { let items: [RoomTimelineItemProtocol] = [TextRoomTimelineItem(eventID: "t1"), TextRoomTimelineItem(eventID: "t2"), SeparatorRoomTimelineItem(timelineID: "v3")] - let (viewModel, _, timelineProxy, _, _) = readReceiptsConfiguration(with: items) + let (viewModel, _, _, _, _) = readReceiptsConfiguration(with: items) // When sending a read receipt for the last item. viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id)) diff --git a/UnitTests/Sources/UserSession/UserSessionTests.swift b/UnitTests/Sources/UserSession/UserSessionTests.swift index bf1d6a1e5..a806b6db9 100644 --- a/UnitTests/Sources/UserSession/UserSessionTests.swift +++ b/UnitTests/Sources/UserSession/UserSessionTests.swift @@ -43,7 +43,7 @@ final class UserSessionTests: XCTestCase { isVerified: false, requestDelay: .zero) clientProxy.sessionVerificationControllerProxyResult = .success(controller) - clientProxy.callbacks.send(.receivedSyncUpdate) + clientProxy.actionsSubject.send(.receivedSyncUpdate) waitForExpectations(timeout: 1.0) } @@ -64,7 +64,7 @@ final class UserSessionTests: XCTestCase { } .store(in: &cancellables) - clientProxy.callbacks.send(.receivedSyncUpdate) + clientProxy.actionsSubject.send(.receivedSyncUpdate) controller.callbacks.send(.finished) waitForExpectations(timeout: 1.0) } diff --git a/changelog.d/2486.feature b/changelog.d/2486.feature new file mode 100644 index 000000000..06a557819 --- /dev/null +++ b/changelog.d/2486.feature @@ -0,0 +1 @@ +Add a blocked users section in the app settings \ No newline at end of file