diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 0c5550c0d..db486a6bc 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -95,6 +95,7 @@ 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; 0F4709282FCCFBEFED427B8A /* AuthenticationClientBuilderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4760CE2128FBC217304272AB /* AuthenticationClientBuilderMock.swift */; }; 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.swift */; }; + 0FA03F5A33C0857231B32B44 /* ReportRoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1FDB0B87D925AE830E32621 /* ReportRoomScreenViewModel.swift */; }; 108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */; }; 109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA894BC09972DC45E497D37 /* TimelineInteractionHandler.swift */; }; 10D60D287025B71F4743A425 /* RoomDirectorySearchProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471BB7276C97AF60B3A5463B /* RoomDirectorySearchProxy.swift */; }; @@ -138,6 +139,7 @@ 1795EA6A6C4942CAE0459DF0 /* SecureBackupKeyBackupScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82B612853BFB68373249777B /* SecureBackupKeyBackupScreenViewModel.swift */; }; 17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; }; 1801F1467ABCEA080419E150 /* preview_avatar_user.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 87FC42213E86E8182CFD3A49 /* preview_avatar_user.jpg */; }; + 182D532B736178A1DED9F76E /* ReportRoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FCAE847556719BBE7A0882 /* ReportRoomScreenModels.swift */; }; 18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; }; 18978C9438206828C1D5AF2A /* test_animated_image.gif in Resources */ = {isa = PBXBuildFile; fileRef = 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */; }; 18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; }; @@ -162,6 +164,7 @@ 1B8E30B35BF8F541C1318F19 /* SecureBackupScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */; }; 1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF34A2FD6797535C95AC918D /* PlaceholderScreenCoordinator.swift */; }; 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; }; + 1C598D3B785645AAC7B35760 /* ReportRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */; }; 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; }; 1C9BB74711E5F24C77B7FED0 /* RoomMembersListScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */; }; 1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */; }; @@ -218,6 +221,7 @@ 298F9EC30E918F12AB7F1EE8 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81F0325E252B057FAEEE1B2D /* TypingIndicatorView.swift */; }; 29EE1791E0AFA1ABB7F23D2F /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; 2A56B00B070F83E0FE571193 /* TimelineMediaPreviewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18A454132A5A5247802821E /* TimelineMediaPreviewDataSource.swift */; }; + 2A61D2B4A225332CECA3B937 /* ReportRoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20403084A320D588ACED200 /* ReportRoomScreenViewModelProtocol.swift */; }; 2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */; }; 2AB9D4146C8748CF1D007B67 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = BE98688578F8B0541D853695 /* test_pdf.pdf */; }; 2AED12987603157C32C2114D /* TimelineBubbleLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D8FEB1FED10E995CB002F7 /* TimelineBubbleLayout.swift */; }; @@ -415,6 +419,7 @@ 50539366B408780B232C1910 /* EstimatedWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD0FF64B0E6470F66F42E182 /* EstimatedWaveformView.swift */; }; 5100F53E6884A15F9BA07CC3 /* AttributedStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */; }; 5139F4BD5A5DF6F8D11A9BDE /* NotificationPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */; }; + 513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */; }; 518C93DC6516D3D018DE065F /* UNNotificationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */; }; 51B3B19FA5F91B455C807BA7 /* RoomPollsHistoryScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E964AF2DFEB31E2B799999F /* RoomPollsHistoryScreenModels.swift */; }; 523C6800ED85D5810CF18C19 /* OIDCAccountSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D737F4672021D0A7D218CD /* OIDCAccountSettingsPresenter.swift */; }; @@ -797,6 +802,7 @@ 9C55746D8F6A3E35CFCF4A7A /* AuthenticationStartLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 598F01EBD0C4CC550C644418 /* AuthenticationStartLogo.swift */; }; 9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1627F2D56477BD331F6D732C /* RoomHeaderView.swift */; }; 9CBB04365408F9D6F46BA3A7 /* PinnedEventsTimelineFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54AAF72E821B4084B7E4298 /* PinnedEventsTimelineFlowCoordinator.swift */; }; + 9CCF6711DD50BFF8B5ACE9CF /* ReportRoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276833816F5DEB1CE3B8BE1E /* ReportRoomScreen.swift */; }; 9D2E03DB175A6AB14589076D /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; 9D79B94493FB32249F7E472F /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; }; @@ -1408,6 +1414,7 @@ 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModel.swift; sourceTree = ""; }; 0BD116096CAA9139B95EEA9C /* UserProfileScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenViewModel.swift; sourceTree = ""; }; 0C34667458773B02AB5FB0B2 /* LegalInformationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenViewModel.swift; sourceTree = ""; }; + 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenViewModelTests.swift; sourceTree = ""; }; 0C62E07C1164F5120727A2A8 /* AppLockSetupBiometricsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenCoordinator.swift; sourceTree = ""; }; 0CB569EAA5017B5B23970655 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1433,6 +1440,7 @@ 11151E78D6BB2B04A8FBD389 /* EmojiPickerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenViewModelProtocol.swift; sourceTree = ""; }; 111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = ""; }; 113356152C099951A6D17D85 /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + 11FCAE847556719BBE7A0882 /* ReportRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenModels.swift; sourceTree = ""; }; 1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = ""; }; @@ -1548,6 +1556,7 @@ 2711E5996016ABD6EAAEB58A /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; 2721D7B051F0159AA919DA05 /* RoomChangePermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenViewModelProtocol.swift; sourceTree = ""; }; 2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterProtocol.swift; sourceTree = ""; }; + 276833816F5DEB1CE3B8BE1E /* ReportRoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreen.swift; sourceTree = ""; }; 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfigurationScreenViewStateTests.swift; sourceTree = ""; }; 27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 27A9E3FBE8A66B5A17AD7F74 /* AppRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRoutes.swift; sourceTree = ""; }; @@ -1561,6 +1570,7 @@ 28C202C1C7E330F124981A31 /* GenericCallLinkWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericCallLinkWidgetDriver.swift; sourceTree = ""; }; 28EA8BE9EEDBD17555141C7E /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = el; path = el.lproj/Localizable.stringsdict; sourceTree = ""; }; 2910422CB628D3B2BBE47449 /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; + 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenCoordinator.swift; sourceTree = ""; }; 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummary.swift; sourceTree = ""; }; 2A2BB38DF61F5100B8723112 /* TimelineMediaPreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewModels.swift; sourceTree = ""; }; 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilder.swift; sourceTree = ""; }; @@ -2201,6 +2211,7 @@ B172057567E049007A5C4D92 /* Strings+SAS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+SAS.swift"; sourceTree = ""; }; B18A454132A5A5247802821E /* TimelineMediaPreviewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewDataSource.swift; sourceTree = ""; }; B1E227F34BE43B08E098796E /* TestablePreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestablePreview.swift; sourceTree = ""; }; + B1FDB0B87D925AE830E32621 /* ReportRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenViewModel.swift; sourceTree = ""; }; B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorProtocol.swift; sourceTree = ""; }; B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogMock.swift; sourceTree = ""; }; B2AF1828A5B76B7C371240FE /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; @@ -2426,6 +2437,7 @@ E1A5FEF17ED7E6176D922D4F /* RoomDetailsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreen.swift; sourceTree = ""; }; E1E0B4A34E69BD2132BEC521 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = ""; }; E1ED17433ADC77287F8904F9 /* CallNotificationRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallNotificationRoomTimelineItem.swift; sourceTree = ""; }; + E20403084A320D588ACED200 /* ReportRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenViewModelProtocol.swift; sourceTree = ""; }; E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenTests.swift; sourceTree = ""; }; E2776E63E02719B20758EB78 /* EditRoomAddressListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressListRow.swift; sourceTree = ""; }; E2B1CC9AA154F4D5435BF60A /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; @@ -3093,6 +3105,14 @@ path = AppLockSetupSettingsScreen; sourceTree = ""; }; + 2B27F01BB7B839E543DFE025 /* View */ = { + isa = PBXGroup; + children = ( + 276833816F5DEB1CE3B8BE1E /* ReportRoomScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 2C0F49BD446849654C0D24E0 /* RoomMember */ = { isa = PBXGroup; children = ( @@ -3765,6 +3785,18 @@ path = RoomMembershipDetails; sourceTree = ""; }; + 4DDB6973795DA09189CF64A5 /* ReportRoomScreen */ = { + isa = PBXGroup; + children = ( + 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */, + 11FCAE847556719BBE7A0882 /* ReportRoomScreenModels.swift */, + B1FDB0B87D925AE830E32621 /* ReportRoomScreenViewModel.swift */, + E20403084A320D588ACED200 /* ReportRoomScreenViewModelProtocol.swift */, + 2B27F01BB7B839E543DFE025 /* View */, + ); + path = ReportRoomScreen; + sourceTree = ""; + }; 4EC4EBBC4F6885775F198875 /* Sources */ = { isa = PBXGroup; children = ( @@ -4247,6 +4279,7 @@ 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, + 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */, 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */, A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */, 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */, @@ -5718,6 +5751,7 @@ 3E535010B850B53DDD3CFF2A /* PinnedEventsTimelineScreen */, 3D733E8352DD4C461CFD8B8A /* QRCodeLoginScreen */, 5970F275D6014548DCED6106 /* ReportContentScreen */, + 4DDB6973795DA09189CF64A5 /* ReportRoomScreen */, A040ACE4D778FFCD65DDF5F8 /* ResolveVerifiedUserSendFailureScreen */, DAB7DC51866A6D1B51BDC3A2 /* RoomChangePermissionsScreen */, D8388454B5909D862CAC78F7 /* RoomChangeRolesScreen */, @@ -6731,6 +6765,7 @@ E3EBC3BF7CE3960B41757BAA /* Publisher.swift in Sources */, BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, + 513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */, 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */, C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */, 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */, @@ -7347,6 +7382,11 @@ 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */, 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */, 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */, + 9CCF6711DD50BFF8B5ACE9CF /* ReportRoomScreen.swift in Sources */, + 1C598D3B785645AAC7B35760 /* ReportRoomScreenCoordinator.swift in Sources */, + 182D532B736178A1DED9F76E /* ReportRoomScreenModels.swift in Sources */, + 0FA03F5A33C0857231B32B44 /* ReportRoomScreenViewModel.swift in Sources */, + 2A61D2B4A225332CECA3B937 /* ReportRoomScreenViewModelProtocol.swift in Sources */, 4715FE33667C5899E64DD0E6 /* ResolveVerifiedUserSendFailureScreen.swift in Sources */, 583A41A4BE76E2E9E0B97881 /* ResolveVerifiedUserSendFailureScreenCoordinator.swift in Sources */, 108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */, diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index b2a04f424..659b6fb46 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -56,6 +56,7 @@ final class AppSettings { case fuzzyRoomListSearchEnabled case enableOnlySignedDeviceIsolationMode case knockingEnabled + case reportRoomEnabled } private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier @@ -330,6 +331,8 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store)) var knockingEnabled + @UserPreference(key: UserDefaultsKeys.reportRoomEnabled, defaultValue: false, storageType: .userDefaults(store)) var reportRoomEnabled + #endif // MARK: - Shared diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 15eabc62c..810a9ad96 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -414,6 +414,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { return .securityAndPrivacy(previousState: fromState) case (.securityAndPrivacy(let previousState), .dismissSecurityAndPrivacyScreen): return previousState + + case (.roomDetails, .presentReportRoomScreen): + return .reportRoom(previousState: fromState) + case (.reportRoom(let previousState), .dismissReportRoomScreen): + return previousState default: return nil @@ -585,6 +590,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentSecurityAndPrivacyScreen() case (.securityAndPrivacy, .dismissSecurityAndPrivacyScreen, .roomDetails): break + + case (.roomDetails, .presentReportRoomScreen, .reportRoom): + presentReportRoom() + case (.reportRoom, .dismissReportRoomScreen, .roomDetails): + break // Child flow case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild): @@ -869,6 +879,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentSecurityAndPrivacyScreen) case .presentRecipientDetails(let userID): stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID)) + case .presentReportRoomScreen: + stateMachine.tryEvent(.presentReportRoomScreen) } } .store(in: &cancellables) @@ -1516,6 +1528,29 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { navigationStackCoordinator.setSheetCoordinator(stackCoordinator) } + private func presentReportRoom() { + let stackCoordinator = NavigationStackCoordinator() + let coordinator = ReportRoomScreenCoordinator(parameters: .init(roomProxy: roomProxy, + userIndicatorController: userIndicatorController)) + + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss(let shouldLeaveRoom): + if shouldLeaveRoom { + stateMachine.tryEvent(.dismissFlow) + } + navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + stackCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissReportRoomScreen) + } + } + // MARK: - Other flows private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async { @@ -1686,6 +1721,7 @@ private extension RoomFlowCoordinator { case knockRequestsList(previousState: State) case mediaEventsTimeline(previousState: State) case securityAndPrivacy(previousState: State) + case reportRoom(previousState: State) /// A child flow is in progress. case presentingChild(childRoomID: String, previousState: State) @@ -1772,5 +1808,8 @@ private extension RoomFlowCoordinator { case presentSecurityAndPrivacyScreen case dismissSecurityAndPrivacyScreen + + case presentReportRoomScreen + case dismissReportRoomScreen } } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 27da9c669..243c442f0 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -172,7 +172,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } case .roomDetails(let roomID): - if stateMachine.state.selectedRoomID == roomID { + if stateMachine.state.roomListSelectedRoomID == roomID { roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) } else { stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .roomDetails), userInfo: .init(animated: animated)) @@ -245,8 +245,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case (.initial, .start, .roomList): presentHomeScreen() attemptStartingOnboarding() - case(.roomList(let selectedRoomID), .selectRoom(let roomID, let via, let entryPoint), .roomList): - if selectedRoomID == roomID, + case(.roomList(let roomListSelectedRoomID), .selectRoom(let roomID, let via, let entryPoint), .roomList): + if roomListSelectedRoomID == roomID, !entryPoint.isEventID, // Don't reuse the existing room so the live timeline is hidden while the detached timeline is loading. let roomFlowCoordinator { let route: AppRoute = switch entryPoint { @@ -307,9 +307,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case (.userProfileScreen, .dismissedUserProfileScreen, .roomList): break - case (.roomList(let selectedRoomID), .showShareExtensionRoomList, .shareExtensionRoomList(let sharePayload)): + case (.roomList, .presentReportRoomScreen(let roomID), .reportRoomScreen): + Task { await self.presentReportRoom(for: roomID) } + case (.reportRoomScreen, .dismissedReportRoomScreen, .roomList): + break + + case (.roomList(let roomListSelectedRoomID), .showShareExtensionRoomList, .shareExtensionRoomList(let sharePayload)): Task { - if selectedRoomID != nil { + if roomListSelectedRoomID != nil { self.clearRoute(animated: animated) try? await Task.sleep(for: .seconds(1.5)) } @@ -326,8 +331,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { stateMachine.addTransitionHandler { [weak self] context in switch context.toState { - case .roomList(let selectedRoomID): - self?.selectedRoomSubject.send(selectedRoomID) + case .roomList(let roomListSelectedRoomID): + self?.selectedRoomSubject.send(roomListSelectedRoomID) default: break } @@ -496,9 +501,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { handleAppRoute(.room(roomID: roomID, via: []), animated: true) case .presentRoomDetails(let roomID): handleAppRoute(.roomDetails(roomID: roomID), animated: true) + case .presentReportRoom(let roomID): + stateMachine.processEvent(.presentReportRoomScreen(roomID: roomID)) case .roomLeft(let roomID): - if case .roomList(selectedRoomID: let selectedRoomID) = stateMachine.state, - selectedRoomID == roomID { + if case .roomList(roomListSelectedRoomID: let roomListSelectedRoomID) = stateMachine.state, + roomListSelectedRoomID == roomID { clearRoute(animated: true) } case .presentSettingsScreen: @@ -528,6 +535,35 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { navigationRootCoordinator.setRootCoordinator(navigationSplitCoordinator) } + private func presentReportRoom(for roomID: String) async { + guard let roomProxyType = await userSession.clientProxy.roomForIdentifier(roomID), + case let .joined(roomProxy) = roomProxyType else { + MXLog.error("Failed to get room proxy for room: \(roomID)") + return + } + + let navigationStackCoordinator = NavigationStackCoordinator() + let coordinator = ReportRoomScreenCoordinator(parameters: .init(roomProxy: roomProxy, + userIndicatorController: ServiceLocator.shared.userIndicatorController)) + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss(let shouldLeaveRoom): + if shouldLeaveRoom, + case .roomList(let roomListSelectedRoomID) = stateMachine.state, + roomListSelectedRoomID == roomID { + clearRoute(animated: true) + } + navigationSplitCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + navigationStackCoordinator.setRootCoordinator(coordinator) + navigationSplitCoordinator.setSheetCoordinator(navigationStackCoordinator) { [weak self] in + self?.stateMachine.processEvent(.dismissedReportRoomScreen) + } + } + private func runLogoutFlow() async { let secureBackupController = userSession.clientProxy.secureBackupController diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift index b452863f6..d8db947fa 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinatorStateMachine.swift @@ -15,49 +15,53 @@ class UserSessionFlowCoordinatorStateMachine { /// The initial state, used before the coordinator starts case initial - /// Showing the home screen. The `selectedRoomID` represents the timeline shown on the detail panel (if any) - case roomList(selectedRoomID: String?) + /// Showing the home screen. The `roomListSelectedRoomID` represents the timeline shown on the detail panel (if any) + case roomList(roomListSelectedRoomID: String?) /// Showing the feedback screen. - case feedbackScreen(selectedRoomID: String?) + case feedbackScreen(roomListSelectedRoomID: String?) /// Showing the settings screen - case settingsScreen(selectedRoomID: String?) + case settingsScreen(roomListSelectedRoomID: String?) /// Showing the recovery key screen. - case recoveryKeyScreen(selectedRoomID: String?) + case recoveryKeyScreen(roomListSelectedRoomID: String?) /// Showing the encryption reset flow. - case encryptionResetFlow(selectedRoomID: String?) + case encryptionResetFlow(roomListSelectedRoomID: String?) /// Showing the start chat screen - case startChatScreen(selectedRoomID: String?) + case startChatScreen(roomListSelectedRoomID: String?) /// Showing the logout flows - case logoutConfirmationScreen(selectedRoomID: String?) + case logoutConfirmationScreen(roomListSelectedRoomID: String?) /// Showing Room Directory Search screen - case roomDirectorySearchScreen(selectedRoomID: String?) + case roomDirectorySearchScreen(roomListSelectedRoomID: String?) /// Showing the user profile screen. This screen clears the navigation. case userProfileScreen + /// Showing the report room screen, for the given room identrifier + case reportRoomScreen(roomListSelectedRoomID: String?) + case shareExtensionRoomList(sharePayload: ShareExtensionPayload) /// The selected room ID from the state if available. - var selectedRoomID: String? { + var roomListSelectedRoomID: String? { switch self { case .initial, .userProfileScreen, .shareExtensionRoomList: nil - case .roomList(let selectedRoomID), - .feedbackScreen(let selectedRoomID), - .settingsScreen(let selectedRoomID), - .recoveryKeyScreen(let selectedRoomID), - .encryptionResetFlow(let selectedRoomID), - .startChatScreen(let selectedRoomID), - .logoutConfirmationScreen(let selectedRoomID), - .roomDirectorySearchScreen(let selectedRoomID): - selectedRoomID + case .roomList(let roomListSelectedRoomID), + .feedbackScreen(let roomListSelectedRoomID), + .settingsScreen(let roomListSelectedRoomID), + .recoveryKeyScreen(let roomListSelectedRoomID), + .encryptionResetFlow(let roomListSelectedRoomID), + .startChatScreen(let roomListSelectedRoomID), + .logoutConfirmationScreen(let roomListSelectedRoomID), + .roomDirectorySearchScreen(let roomListSelectedRoomID), + .reportRoomScreen(let roomListSelectedRoomID): + roomListSelectedRoomID } } } @@ -121,6 +125,9 @@ class UserSessionFlowCoordinatorStateMachine { case showShareExtensionRoomList(sharePayload: ShareExtensionPayload) case dismissedShareExtensionRoomList + + case presentReportRoomScreen(roomID: String) + case dismissedReportRoomScreen } private let stateMachine: StateMachine @@ -140,59 +147,64 @@ class UserSessionFlowCoordinatorStateMachine { } private func configure() { - stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(selectedRoomID: nil)]) + stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(roomListSelectedRoomID: nil)]) stateMachine.addRouteMapping { event, fromState, _ in switch (fromState, event) { case (.roomList, .selectRoom(let roomID, _, _)): - return .roomList(selectedRoomID: roomID) + return .roomList(roomListSelectedRoomID: roomID) case (.roomList, .deselectRoom): - return .roomList(selectedRoomID: nil) + return .roomList(roomListSelectedRoomID: nil) - case (.roomList(let selectedRoomID), .showSettingsScreen): - return .settingsScreen(selectedRoomID: selectedRoomID) - case (.settingsScreen(let selectedRoomID), .dismissedSettingsScreen): - return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let roomListSelectedRoomID), .showSettingsScreen): + return .settingsScreen(roomListSelectedRoomID: roomListSelectedRoomID) + case (.settingsScreen(let roomListSelectedRoomID), .dismissedSettingsScreen): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) - case (.roomList(let selectedRoomID), .feedbackScreen): - return .feedbackScreen(selectedRoomID: selectedRoomID) - case (.feedbackScreen(let selectedRoomID), .dismissedFeedbackScreen): - return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let roomListSelectedRoomID), .feedbackScreen): + return .feedbackScreen(roomListSelectedRoomID: roomListSelectedRoomID) + case (.feedbackScreen(let roomListSelectedRoomID), .dismissedFeedbackScreen): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) - case (.roomList(let selectedRoomID), .showRecoveryKeyScreen): - return .recoveryKeyScreen(selectedRoomID: selectedRoomID) - case (.recoveryKeyScreen(let selectedRoomID), .dismissedRecoveryKeyScreen): - return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let roomListSelectedRoomID), .showRecoveryKeyScreen): + return .recoveryKeyScreen(roomListSelectedRoomID: roomListSelectedRoomID) + case (.recoveryKeyScreen(let roomListSelectedRoomID), .dismissedRecoveryKeyScreen): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) - case (.roomList(let selectedRoomID), .startEncryptionResetFlow): - return .encryptionResetFlow(selectedRoomID: selectedRoomID) - case (.encryptionResetFlow(let selectedRoomID), .finishedEncryptionResetFlow): - return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let roomListSelectedRoomID), .startEncryptionResetFlow): + return .encryptionResetFlow(roomListSelectedRoomID: roomListSelectedRoomID) + case (.encryptionResetFlow(let roomListSelectedRoomID), .finishedEncryptionResetFlow): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) - case (.roomList(let selectedRoomID), .showStartChatScreen): - return .startChatScreen(selectedRoomID: selectedRoomID) - case (.startChatScreen(let selectedRoomID), .dismissedStartChatScreen): - return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let roomListSelectedRoomID), .showStartChatScreen): + return .startChatScreen(roomListSelectedRoomID: roomListSelectedRoomID) + case (.startChatScreen(let roomListSelectedRoomID), .dismissedStartChatScreen): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) - case (.roomList(let selectedRoomID), .showLogoutConfirmationScreen): - return .logoutConfirmationScreen(selectedRoomID: selectedRoomID) - case (.logoutConfirmationScreen(let selectedRoomID), .dismissedLogoutConfirmationScreen): - return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let roomListSelectedRoomID), .showLogoutConfirmationScreen): + return .logoutConfirmationScreen(roomListSelectedRoomID: roomListSelectedRoomID) + case (.logoutConfirmationScreen(let roomListSelectedRoomID), .dismissedLogoutConfirmationScreen): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) - case (.roomList(let selectedRoomID), .showRoomDirectorySearchScreen): - return .roomDirectorySearchScreen(selectedRoomID: selectedRoomID) - case (.roomDirectorySearchScreen(let selectedRoomID), .dismissedRoomDirectorySearchScreen): - return .roomList(selectedRoomID: selectedRoomID) + case (.roomList(let roomListSelectedRoomID), .showRoomDirectorySearchScreen): + return .roomDirectorySearchScreen(roomListSelectedRoomID: roomListSelectedRoomID) + case (.roomDirectorySearchScreen(let roomListSelectedRoomID), .dismissedRoomDirectorySearchScreen): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) case (_, .showUserProfileScreen): return .userProfileScreen case (.userProfileScreen, .dismissedUserProfileScreen): - return .roomList(selectedRoomID: nil) + return .roomList(roomListSelectedRoomID: nil) case (.roomList, .showShareExtensionRoomList(let sharePayload)): return .shareExtensionRoomList(sharePayload: sharePayload) case (.shareExtensionRoomList, .dismissedShareExtensionRoomList): - return .roomList(selectedRoomID: nil) + return .roomList(roomListSelectedRoomID: nil) + + case (.roomList(let roomListSelectedRoomID), .presentReportRoomScreen): + return .reportRoomScreen(roomListSelectedRoomID: roomListSelectedRoomID) + case (.reportRoomScreen(let roomListSelectedRoomID), .dismissedReportRoomScreen): + return .roomList(roomListSelectedRoomID: roomListSelectedRoomID) default: return nil @@ -231,8 +243,8 @@ class UserSessionFlowCoordinatorStateMachine { /// Flag indicating the machine is displaying room screen with given room identifier func isDisplayingRoomScreen(withRoomID roomID: String) -> Bool { switch stateMachine.state { - case .roomList(let selectedRoomID): - return roomID == selectedRoomID + case .roomList(let roomListSelectedRoomID): + return roomID == roomListSelectedRoomID default: return false } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 8075e3cdf..a61d5c986 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -6689,6 +6689,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable { return reportContentReasonReturnValue } } + //MARK: - reportRoom + + var reportRoomReasonUnderlyingCallsCount = 0 + var reportRoomReasonCallsCount: Int { + get { + if Thread.isMainThread { + return reportRoomReasonUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = reportRoomReasonUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + reportRoomReasonUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + reportRoomReasonUnderlyingCallsCount = newValue + } + } + } + } + var reportRoomReasonCalled: Bool { + return reportRoomReasonCallsCount > 0 + } + var reportRoomReasonReceivedReason: String? + var reportRoomReasonReceivedInvocations: [String?] = [] + + var reportRoomReasonUnderlyingReturnValue: Result! + var reportRoomReasonReturnValue: Result! { + get { + if Thread.isMainThread { + return reportRoomReasonUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = reportRoomReasonUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + reportRoomReasonUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + reportRoomReasonUnderlyingReturnValue = newValue + } + } + } + } + var reportRoomReasonClosure: ((String?) async -> Result)? + + func reportRoom(reason: String?) async -> Result { + reportRoomReasonCallsCount += 1 + reportRoomReasonReceivedReason = reason + DispatchQueue.main.async { + self.reportRoomReasonReceivedInvocations.append(reason) + } + if let reportRoomReasonClosure = reportRoomReasonClosure { + return await reportRoomReasonClosure(reason) + } else { + return reportRoomReasonReturnValue + } + } //MARK: - leaveRoom var leaveRoomUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 5a15dab73..2e36b54ac 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -17,6 +17,7 @@ struct HomeScreenCoordinatorParameters { enum HomeScreenCoordinatorAction { case presentRoom(roomIdentifier: String) case presentRoomDetails(roomIdentifier: String) + case presentReportRoom(roomIdentifier: String) case roomLeft(roomIdentifier: String) case presentSettingsScreen case presentFeedbackScreen @@ -58,6 +59,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier)) case .presentRoomDetails(roomIdentifier: let roomIdentifier): actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier)) + case .presentReportRoom(let roomIdentifier): + actionsSubject.send(.presentReportRoom(roomIdentifier: roomIdentifier)) case .roomLeft(roomIdentifier: let roomIdentifier): actionsSubject.send(.roomLeft(roomIdentifier: roomIdentifier)) case .presentFeedbackScreen: diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index b089efa1e..69c9bbeb4 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -12,6 +12,7 @@ import UIKit enum HomeScreenViewModelAction: Equatable { case presentRoom(roomIdentifier: String) case presentRoomDetails(roomIdentifier: String) + case presentReportRoom(roomIdentifier: String) case roomLeft(roomIdentifier: String) case presentSecureBackupSettings case presentRecoveryKeyScreen @@ -29,6 +30,7 @@ enum HomeScreenViewAction { case showRoomDetails(roomIdentifier: String) case leaveRoom(roomIdentifier: String) case confirmLeaveRoom(roomIdentifier: String) + case reportRoom(roomIdentifier: String) case showSettings case startChat case setupRecovery @@ -98,6 +100,10 @@ struct HomeScreenViewState: BindableState { var selectedRoomID: String? + var hideInviteAvatars = false + + var reportRoomEnabled = false + var visibleRooms: [HomeScreenRoom] { if roomListMode == .skeletons { return placeholderRooms @@ -105,9 +111,7 @@ struct HomeScreenViewState: BindableState { return rooms } - - var hideInviteAvatars = false - + var bindings = HomeScreenViewStateBindings() var placeholderRooms: [HomeScreenRoom] { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 335dfa572..de252dd59 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -105,6 +105,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .weakAssign(to: \.state.hideInviteAvatars, on: self) .store(in: &cancellables) + appSettings.$reportRoomEnabled + .weakAssign(to: \.state.reportRoomEnabled, on: self) + .store(in: &cancellables) + let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused) let searchQuery = context.$viewState.map(\.bindings.searchQuery) let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters) @@ -136,12 +140,14 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol switch viewAction { case .selectRoom(let roomIdentifier): actionsSubject.send(.presentRoom(roomIdentifier: roomIdentifier)) - case .showRoomDetails(roomIdentifier: let roomIdentifier): + case .showRoomDetails(let roomIdentifier): actionsSubject.send(.presentRoomDetails(roomIdentifier: roomIdentifier)) - case .leaveRoom(roomIdentifier: let roomIdentifier): + case .leaveRoom(let roomIdentifier): startLeaveRoomProcess(roomID: roomIdentifier) - case .confirmLeaveRoom(roomIdentifier: let roomIdentifier): + case .confirmLeaveRoom(let roomIdentifier): Task { await leaveRoom(roomID: roomIdentifier) } + case .reportRoom(let roomIdentifier): + actionsSubject.send(.presentReportRoom(roomIdentifier: roomIdentifier)) case .showSettings: actionsSubject.send(.presentSettingsScreen) case .setupRecovery: diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift index 461c76596..525fea3ef 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomList.swift @@ -69,6 +69,14 @@ struct HomeScreenRoomList: View { Label(L10n.commonSettings, icon: \.settings) } + if context.viewState.reportRoomEnabled { + Button(role: .destructive) { + context.send(viewAction: .reportRoom(roomIdentifier: room.id)) + } label: { + Label(L10n.actionReportRoom, icon: \.chatProblem) + } + } + Button(role: .destructive) { context.send(viewAction: .leaveRoom(roomIdentifier: room.id)) } label: { diff --git a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenCoordinator.swift b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenCoordinator.swift new file mode 100644 index 000000000..184b1afd2 --- /dev/null +++ b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenCoordinator.swift @@ -0,0 +1,54 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +// periphery:ignore:all - this is just a reportRoom remove this comment once generating the final file + +import Combine +import SwiftUI + +struct ReportRoomScreenCoordinatorParameters { + let roomProxy: JoinedRoomProxyProtocol + let userIndicatorController: UserIndicatorControllerProtocol +} + +enum ReportRoomScreenCoordinatorAction { + case dismiss(shouldLeaveRoom: Bool) +} + +final class ReportRoomScreenCoordinator: CoordinatorProtocol { + private let parameters: ReportRoomScreenCoordinatorParameters + private let viewModel: ReportRoomScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: ReportRoomScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = ReportRoomScreenViewModel(roomProxy: parameters.roomProxy, + userIndicatorController: parameters.userIndicatorController) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .dismiss(let shouldLeaveRoom): + actionsSubject.send(.dismiss(shouldLeaveRoom: shouldLeaveRoom)) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(ReportRoomScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenModels.swift b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenModels.swift new file mode 100644 index 000000000..d397001a3 --- /dev/null +++ b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenModels.swift @@ -0,0 +1,31 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Foundation + +enum ReportRoomScreenViewModelAction: Equatable { + case dismiss(shouldLeaveRoom: Bool) +} + +struct ReportRoomScreenViewState: BindableState { + var bindings = ReportRoomScreenViewStateBindings() +} + +struct ReportRoomScreenViewStateBindings { + var reason = "" + var shouldLeaveRoom = false + var alert: AlertInfo? +} + +enum ReportRoomScreenViewAction { + case report + case dismiss +} + +enum ReportRoomScreenAlertType { + case leaveRoomFailed +} diff --git a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift new file mode 100644 index 000000000..f62b8e6dc --- /dev/null +++ b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift @@ -0,0 +1,91 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import SwiftUI + +typealias ReportRoomScreenViewModelType = StateStoreViewModel + +class ReportRoomScreenViewModel: ReportRoomScreenViewModelType, ReportRoomScreenViewModelProtocol { + let roomProxy: JoinedRoomProxyProtocol + let userIndicatorController: UserIndicatorControllerProtocol + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(roomProxy: JoinedRoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { + self.roomProxy = roomProxy + self.userIndicatorController = userIndicatorController + super.init(initialViewState: ReportRoomScreenViewState()) + } + + // MARK: - Public + + override func process(viewAction: ReportRoomScreenViewAction) { + switch viewAction { + case .report: + Task { await report() } + case .dismiss: + actionsSubject.send(.dismiss(shouldLeaveRoom: false)) + } + } + + private func report() async { + showLoadingIndicator() + let result = await roomProxy.reportRoom(reason: state.bindings.reason.isBlank ? nil : state.bindings.reason) + + switch result { + case .success: + if state.bindings.shouldLeaveRoom { + await leaveRoom(showLoading: false) + } else { + hideLoadingIndicator() + userIndicatorController.submitIndicator(.init(title: L10n.dialogRoomReported, iconName: "checkmark")) + actionsSubject.send(.dismiss(shouldLeaveRoom: false)) + } + case .failure: + hideLoadingIndicator() + userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) + } + } + + private func leaveRoom(showLoading: Bool) async { + if showLoading { + showLoadingIndicator() + } + + let result = await roomProxy.leaveRoom() + hideLoadingIndicator() + + switch result { + case .success: + userIndicatorController.submitIndicator(.init(title: L10n.dialogRoomReportedAndLeft, iconName: "checkmark")) + actionsSubject.send(.dismiss(shouldLeaveRoom: true)) + case .failure: + state.bindings.alert = .init(id: .leaveRoomFailed, + title: L10n.screenReportRoomLeaveFailedAlertTitle, + message: L10n.screenReportRoomLeaveFailedAlertMessage, + primaryButton: .init(title: L10n.actionDismiss, role: .cancel) { [weak self] in self?.actionsSubject.send(.dismiss(shouldLeaveRoom: false)) }, + secondaryButton: .init(title: L10n.actionRetry) { [weak self] in Task { await self?.leaveRoom(showLoading: true) } }) + } + } + + private static let loadingIndicatorIdentifier = "\(BugReportScreenCoordinator.self)-Loading" + + private func showLoadingIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), + title: L10n.commonLoading, + persistent: true)) + } + + private func hideLoadingIndicator() { + userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } +} diff --git a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModelProtocol.swift b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModelProtocol.swift new file mode 100644 index 000000000..be6c1deb6 --- /dev/null +++ b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModelProtocol.swift @@ -0,0 +1,14 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine + +@MainActor +protocol ReportRoomScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: ReportRoomScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/ReportRoomScreen/View/ReportRoomScreen.swift b/ElementX/Sources/Screens/ReportRoomScreen/View/ReportRoomScreen.swift new file mode 100644 index 000000000..ff0fdd404 --- /dev/null +++ b/ElementX/Sources/Screens/ReportRoomScreen/View/ReportRoomScreen.swift @@ -0,0 +1,70 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +struct ReportRoomScreen: View { + @ObservedObject var context: ReportRoomScreenViewModel.Context + + var body: some View { + Form { + reasonSection + leaveRoomSection + } + .compoundList() + .navigationTitle(L10n.screenReportRoomTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbar } + .alert(item: $context.alert) + } + + private var reasonSection: some View { + Section { + ListRow(label: .plain(title: L10n.screenReportRoomReasonPlaceholder), + kind: .textField(text: $context.reason, axis: .vertical)) + .lineLimit(4, reservesSpace: true) + } footer: { + Text(L10n.screenReportRoomReasonFooter) + .compoundListSectionFooter() + } + } + + private var leaveRoomSection: some View { + Section { + ListRow(label: .plain(title: L10n.actionLeaveRoom), + kind: .toggle($context.shouldLeaveRoom)) + } + } + + @ToolbarContentBuilder + private var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button(L10n.actionCancel) { + context.send(viewAction: .dismiss) + } + } + + ToolbarItem(placement: .confirmationAction) { + Button(L10n.actionReport) { + context.send(viewAction: .report) + } + } + } +} + +// MARK: - Previews + +struct ReportRoomScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = ReportRoomScreenViewModel(roomProxy: JoinedRoomProxyMock(.init()), + userIndicatorController: UserIndicatorControllerMock()) + static var previews: some View { + NavigationStack { + ReportRoomScreen(context: viewModel.context) + } + } +} diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index a57719477..8a4bda3d7 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -33,6 +33,7 @@ enum RoomDetailsScreenCoordinatorAction { case presentMediaEventsTimeline case presentKnockingRequestsListScreen case presentSecurityAndPrivacyScreen + case presentReportRoomScreen } final class RoomDetailsScreenCoordinator: CoordinatorProtocol { @@ -91,6 +92,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentSecurityAndPrivacyScreen) case .requestRecipientDetailsPresentation(let userID): actionsSubject.send(.presentRecipientDetails(userID: userID)) + case .displayReportRoom: + actionsSubject.send(.presentReportRoomScreen) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 1997799bd..2e2f45680 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -26,6 +26,7 @@ enum RoomDetailsScreenViewModelAction: Equatable { case displayMediaEventsTimeline case displayKnockingRequests case displaySecurityAndPrivacy + case displayReportRoom } // MARK: View @@ -64,6 +65,8 @@ struct RoomDetailsScreenViewState: BindableState { var isKnockableRoom = false var knockRequestsCount = 0 + var reportRoomEnabled = false + var canSeeKnockingRequests: Bool { knockingEnabled && dmRecipientInfo == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers) } @@ -218,6 +221,7 @@ enum RoomDetailsScreenViewAction { case processTapPinnedEvents case processTapMediaEvents case processTapRequestsToJoin + case processTapReport } enum RoomDetailsScreenViewShortcut { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index d865d1277..698d18106 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -78,6 +78,10 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr .weakAssign(to: \.state.knockingEnabled, on: self) .store(in: &cancellables) + appSettings.$reportRoomEnabled + .weakAssign(to: \.state.reportRoomEnabled, on: self) + .store(in: &cancellables) + appMediator.networkMonitor.reachabilityPublisher .filter { $0 == .reachable } .receive(on: DispatchQueue.main) @@ -174,6 +178,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr return } actionsSubject.send(.requestRecipientDetailsPresentation(userID: userID)) + case .processTapReport: + actionsSubject.send(.displayReportRoom) } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index f0a79b01a..557f7639e 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -275,9 +275,19 @@ struct RoomDetailsScreen: View { private var leaveRoomTitle: String { context.viewState.dmRecipientInfo == nil ? L10n.screenRoomDetailsLeaveRoomTitle : L10n.screenRoomDetailsLeaveConversationTitle } + + private var reportRoomTitle: String { + context.viewState.dmRecipientInfo == nil ? L10n.actionReportRoom : L10n.actionReport + } private var leaveRoomSection: some View { Section { + if context.viewState.reportRoomEnabled { + ListRow(label: .action(title: reportRoomTitle, + icon: \.chatProblem, + role: .destructive), + kind: .button { context.send(viewAction: .processTapReport) }) + } ListRow(label: .action(title: leaveRoomTitle, icon: \.leave, role: .destructive), diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index fbf3441f9..e17a2cded 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -53,7 +53,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe case .kickMember(let member): var reason: String? let binding: Binding = .init(get: { reason ?? "" }, - set: { reason = $0.isEmpty ? nil : $0 }) + set: { reason = $0.isBlank ? nil : $0 }) state.bindings.alertInfo = .init(id: .kickConfirmation, title: L10n.screenRoomMemberListKickMemberConfirmationTitle, message: L10n.screenRoomMemberListKickMemberConfirmationDescription, @@ -66,7 +66,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe case .banMember(let member): var reason: String? let binding: Binding = .init(get: { reason ?? "" }, - set: { reason = $0.isEmpty ? nil : $0 }) + set: { reason = $0.isBlank ? nil : $0 }) state.bindings.alertInfo = .init(id: .banConfirmation, title: L10n.screenRoomMemberListBanMemberConfirmationTitle, message: L10n.screenRoomMemberListBanMemberConfirmationDescription, diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 388027773..bcd92a627 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -44,6 +44,7 @@ protocol DeveloperOptionsProtocol: AnyObject { var enableOnlySignedDeviceIsolationMode: Bool { get set } var elementCallBaseURLOverride: URL? { get set } var knockingEnabled: Bool { get set } + var reportRoomEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 3c17c054d..c15eb60c8 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -62,6 +62,13 @@ struct DeveloperOptionsScreen: View { } footer: { Text("This setting controls how end-to-end encryption (E2EE) keys are exchanged. Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners.") } + + Section("Reporting") { + Toggle(isOn: $context.reportRoomEnabled) { + Text("Report rooms") + Text("Report API might not work properly") + } + } Section { TextField("Leave empty to use EC locally", text: $elementCallURLOverrideString) diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 213d04cb0..80f27bf07 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -255,6 +255,16 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } } + func reportRoom(reason: String?) async -> Result { + do { + try await room.reportRoom(reason: reason) + return .success(()) + } catch { + MXLog.error("Failed reporting room: \(id) with error: \(error)") + return .failure(.sdkError(error)) + } + } + func updateMembers() async { // We always update members first using the no sync API in case internet is not readily available // To get the members stored on disk first, this API call is very fast. diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 48cae0671..cd82fbefc 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -92,6 +92,8 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { func redact(_ eventID: String) async -> Result func reportContent(_ eventID: String, reason: String?) async -> Result + + func reportRoom(reason: String?) async -> Result func leaveRoom() async -> Result diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index a752c73e0..f0a897054 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -593,6 +593,12 @@ extension PreviewTests { } } + func testReportRoomScreen() async throws { + for (index, preview) in ReportRoomScreen_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + func testResolveVerifiedUserSendFailureScreen() async throws { for (index, preview) in ResolveVerifiedUserSendFailureScreen_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPad-en-GB-0.png new file mode 100644 index 000000000..52100f78c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac131e760c33abc71ff2561532486148184b2145ab8908fc93c8f59fd52a63bd +size 111036 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPad-pseudo-0.png new file mode 100644 index 000000000..3459f0ea7 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce3deae0c993d297780b9983f7864252b38ec5eb139b82c34eac5f66c344f30c +size 124615 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPhone-16-en-GB-0.png new file mode 100644 index 000000000..d6e3d03fb --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPhone-16-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71fabe839f7069969d2064f39430d6044ae176b057d355f26871e86f563b4bf1 +size 63941 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPhone-16-pseudo-0.png new file mode 100644 index 000000000..ec3c8a9f5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/reportRoomScreen.iPhone-16-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1277567cb86dde2a5d0af84a0c0ec71405fea0a094a5c246757220316662e6f9 +size 83244 diff --git a/UnitTests/Sources/ReportRoomScreenViewModelTests.swift b/UnitTests/Sources/ReportRoomScreenViewModelTests.swift new file mode 100644 index 000000000..4807a2071 --- /dev/null +++ b/UnitTests/Sources/ReportRoomScreenViewModelTests.swift @@ -0,0 +1,104 @@ +// +// Copyright 2022-2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import XCTest + +@testable import ElementX + +@MainActor +class ReportRoomScreenViewModelTests: XCTestCase { + var viewModel: ReportRoomScreenViewModelProtocol! + var roomProxy: JoinedRoomProxyMock! + + var context: ReportRoomScreenViewModelType.Context { + viewModel.context + } + + override func setUp() { + roomProxy = JoinedRoomProxyMock(.init()) + viewModel = ReportRoomScreenViewModel(roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock()) + } + + func testInitialState() { + XCTAssertTrue(context.viewState.bindings.reason.isEmpty) + XCTAssertFalse(context.viewState.bindings.shouldLeaveRoom) + } + + func testReportSuccess() async throws { + let reason = "Spam" + let expectation = XCTestExpectation(description: "Report success") + roomProxy.reportRoomReasonClosure = { reasonArgument in + defer { expectation.fulfill() } + XCTAssertEqual(reasonArgument, reason) + return .success(()) + } + + let deferred = deferFulfillment(viewModel.actionsPublisher) { action in + action == .dismiss(shouldLeaveRoom: false) + } + + context.reason = reason + context.send(viewAction: .report) + + try await deferred.fulfill() + await fulfillment(of: [expectation]) + } + + func testReportAndLeaveSuccess() async throws { + let reason = "Spam" + let reportExpectation = XCTestExpectation(description: "Report success") + roomProxy.reportRoomReasonClosure = { reasonArgument in + defer { reportExpectation.fulfill() } + XCTAssertEqual(reasonArgument, reason) + return .success(()) + } + + let leaveExpectation = XCTestExpectation(description: "Leave success") + roomProxy.leaveRoomClosure = { + defer { leaveExpectation.fulfill() } + return .success(()) + } + + let deferred = deferFulfillment(viewModel.actionsPublisher) { action in + action == .dismiss(shouldLeaveRoom: true) + } + + context.reason = reason + context.shouldLeaveRoom = true + context.send(viewAction: .report) + + await fulfillment(of: [reportExpectation, leaveExpectation]) + try await deferred.fulfill() + } + + func testReportSuccessLeaveFails() async throws { + let reason = "Spam" + let reportExpectation = XCTestExpectation(description: "Report success") + roomProxy.reportRoomReasonClosure = { reasonArgument in + defer { reportExpectation.fulfill() } + XCTAssertEqual(reasonArgument, reason) + return .success(()) + } + + let leaveExpectation = XCTestExpectation(description: "Leave fails") + roomProxy.leaveRoomClosure = { + defer { leaveExpectation.fulfill() } + return .failure(.eventNotFound) + } + + let deferred = deferFulfillment(context.$viewState) { state in + state.bindings.alert != nil + } + + context.reason = reason + context.shouldLeaveRoom = true + context.send(viewAction: .report) + + await fulfillment(of: [reportExpectation, leaveExpectation]) + try await deferred.fulfill() + } +} diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 6b93dd9c3..185578ce1 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -46,29 +46,29 @@ class UserSessionFlowCoordinatorTests: XCTestCase { notificationManager: notificationManager, isNewLogin: false) - let deferred = deferFulfillment(userSessionFlowCoordinator.statePublisher) { $0 == .roomList(selectedRoomID: nil) } + let deferred = deferFulfillment(userSessionFlowCoordinator.statePublisher) { $0 == .roomList(roomListSelectedRoomID: nil) } userSessionFlowCoordinator.start() try await deferred.fulfill() } func testRoomPresentation() async throws { - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(selectedRoomID: "2")) + try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(roomListSelectedRoomID: "2")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) @@ -78,25 +78,25 @@ class UserSessionFlowCoordinatorTests: XCTestCase { func testRoomAliasPresentation() async throws { clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "1", servers: [])) - try await process(route: .roomAlias("#alias:matrix.org"), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .roomAlias("#alias:matrix.org"), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) clientProxy.resolveRoomAliasReturnValue = .success(.init(roomId: "2", servers: [])) - try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(selectedRoomID: "2")) + try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(roomListSelectedRoomID: "2")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) @@ -104,27 +104,27 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testRoomDetailsPresentation() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil)) + try await process(route: .roomList, expectedState: .roomList(roomListSelectedRoomID: nil)) XCTAssertNil(detailNavigationStack?.rootCoordinator) XCTAssertNil(detailCoordinator) } func testStackUnwinding() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(selectedRoomID: "2")) + try await process(route: .room(roomID: "2", via: []), expectedState: .roomList(roomListSelectedRoomID: "2")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) } func testNoOp() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) @@ -137,17 +137,17 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testSwitchToDifferentDetails() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .roomDetails(roomID: "2"), expectedState: .roomList(selectedRoomID: "2")) + try await process(route: .roomDetails(roomID: "2"), expectedState: .roomList(roomListSelectedRoomID: "2")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) } func testPushDetails() async throws { - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) @@ -162,17 +162,17 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testReplaceDetailsWithTimeline() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) } func testUserProfileClearsStack() async throws { - try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator) XCTAssertNotNil(detailCoordinator) XCTAssertNil(splitCoordinator?.sheetCoordinator) @@ -187,7 +187,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testRoomClearsStack() async throws { - try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .room(roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -199,7 +199,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomScreenCoordinator) XCTAssertNotNil(detailCoordinator) - try await process(route: .room(roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3")) + try await process(route: .room(roomID: "3", via: []), expectedState: .roomList(roomListSelectedRoomID: "3")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -207,7 +207,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { func testEventRoutes() async throws { // A regular event route should set its room as the root of the stack and focus on the event. - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -225,7 +225,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "2") // A subsequent regular event route should clear the stack and set the new room as the root of the stack. - try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3")) + try await process(route: .event(eventID: "3", roomID: "3", via: []), expectedState: .roomList(roomListSelectedRoomID: "3")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -233,7 +233,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { XCTAssertEqual(timelineControllerFactory.buildTimelineControllerRoomProxyInitialFocussedEventIDTimelineItemFactoryMediaProviderReceivedArguments?.initialFocussedEventID, "3") // A regular event route for the same room should set a new instance of the room as the root of the stack. - try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(selectedRoomID: "3")) + try await process(route: .event(eventID: "4", roomID: "3", via: []), expectedState: .roomList(roomListSelectedRoomID: "3")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 0) XCTAssertNotNil(detailCoordinator) @@ -243,7 +243,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testShareMediaRouteWithoutRoom() async throws { - try await process(route: .settings, expectedState: .settingsScreen(selectedRoomID: nil)) + try await process(route: .settings, expectedState: .settingsScreen(roomListSelectedRoomID: nil)) XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) let sharePayload: ShareExtensionPayload = .mediaFile(roomID: nil, mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) @@ -254,19 +254,19 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testShareMediaRouteWithRoom() async throws { - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) let sharePayload: ShareExtensionPayload = .mediaFile(roomID: "2", mediaFile: .init(url: .picturesDirectory, suggestedName: nil)) try await process(route: .share(sharePayload), - expectedState: .roomList(selectedRoomID: "2")) + expectedState: .roomList(roomListSelectedRoomID: "2")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is MediaUploadPreviewScreenCoordinator) } func testShareTextRouteWithoutRoom() async throws { - try await process(route: .settings, expectedState: .settingsScreen(selectedRoomID: nil)) + try await process(route: .settings, expectedState: .settingsScreen(roomListSelectedRoomID: nil)) XCTAssertTrue((splitCoordinator?.sheetCoordinator as? NavigationStackCoordinator)?.rootCoordinator is SettingsScreenCoordinator) let sharePayload: ShareExtensionPayload = .text(roomID: nil, text: "Important Text") @@ -277,12 +277,12 @@ class UserSessionFlowCoordinatorTests: XCTestCase { } func testShareTextRouteWithRoom() async throws { - try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(selectedRoomID: "1")) + try await process(route: .event(eventID: "1", roomID: "1", via: []), expectedState: .roomList(roomListSelectedRoomID: "1")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) let sharePayload: ShareExtensionPayload = .text(roomID: "2", text: "Important text") try await process(route: .share(sharePayload), - expectedState: .roomList(selectedRoomID: "2")) + expectedState: .roomList(roomListSelectedRoomID: "2")) XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator) XCTAssertNil(splitCoordinator?.sheetCoordinator, "The media upload sheet shouldn't be shown when sharing text.")