diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 408fa1fabf..f4ff81fd6e 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 09713669577CDA8D012EE380 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */; }; 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; }; 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */; }; + 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */; }; 0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; }; 0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; @@ -80,6 +81,7 @@ 0EE5EBA18BA1FE10254BB489 /* UIFont+AttributedStringBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E8CA187FE656EE5A3F6C7DE5 /* UIFont+AttributedStringBuilder.m */; }; 0EEC614342F823E5BF966C2C /* AppLockTimerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */; }; 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A019A12C866D64CF072024B9 /* AppLockSetupPINScreenViewModel.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 */; }; 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0376C429FAB1687C3D905F3E /* MockCoder.swift */; }; @@ -313,6 +315,7 @@ 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565F1B2B300597C616B37888 /* FullscreenDialog.swift */; }; 46C9F8FE3810A04A005FE16B /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B19B2BCC779ED934E0BBC2A /* AudioPlayer.swift */; }; 46FCD999E92D9717D24AAB94 /* QRCodeLoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDEDD4D2DE0646DA724985D5 /* QRCodeLoginScreenModels.swift */; }; + 4715FE33667C5899E64DD0E6 /* ResolveVerifiedUserSendFailureScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */; }; 4716587A9BA69ED8FD1B986B /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B19D10B102956066AF117B /* PollOptionView.swift */; }; 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA90BD288E5AE6BC643AFDDF /* TemplateScreenCoordinator.swift */; }; 4799A852132F1744E2825994 /* CreateRoomViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */; }; @@ -388,6 +391,7 @@ 5710AAB27D5D866292C1FE06 /* SessionVerificationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */; }; 5732395A4F71F51F9C754C5A /* ElementCallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33AE897D86784CCA5E4E9227 /* ElementCallService.swift */; }; 5780E444F405AA1304E1C23E /* DeveloperOptionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */; }; + 583A41A4BE76E2E9E0B97881 /* ResolveVerifiedUserSendFailureScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */; }; 588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; }; 5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */; }; 5897A59DDBD3592282092223 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; }; @@ -688,6 +692,7 @@ 9912F9EB2D6589141A2957B4 /* AppLockScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = D661CAB418C075A94306A792 /* AnalyticsEvents */; }; + 9A0326D2375075871D2AB537 /* ResolveVerifiedUserSendFailureScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 574CB70E82D7EAEA538E4135 /* ResolveVerifiedUserSendFailureScreenViewModel.swift */; }; 9A3B0CDF097E3838FB1B9595 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 9A4E3D5AA44B041DAC3A0D81 /* OIDCAuthenticationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */; }; 9AC5F8142413862A9E3A2D98 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; @@ -754,7 +759,6 @@ A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; A8FA7671948E3DF27F320026 /* BugReportFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7367B3B9A8CAF902220F31D1 /* BugReportFlowCoordinator.swift */; }; - A9062C5F94DF651F0D21196F /* ResolveVerifiedUserSendFailureViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A849378B6EE12B44DBA4F9A6 /* ResolveVerifiedUserSendFailureViewStateTests.swift */; }; A93661C962B12942C08864B6 /* Prefire in Frameworks */ = {isa = PBXBuildFile; productRef = 2629CF48B33643CD5F69C612 /* Prefire */; }; A9482B967FC85DA611514D35 /* VoiceMessageRoomPlaybackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD41CD67DB5DA0D436BFE9 /* VoiceMessageRoomPlaybackView.swift */; }; A969147E0EEE0E27EE226570 /* MediaUploadPreviewScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */; }; @@ -798,7 +802,6 @@ B31E5493C99381D4E204438B /* RoomTimelineControllerFactoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = D479DF730528153665E5782E /* RoomTimelineControllerFactoryMock.swift */; }; B3D652AA1654270742072FB3 /* DeveloperOptionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86A6F283BC574FDB96ABBB07 /* DeveloperOptionsScreenViewModel.swift */; }; B402708F8728DD0DB7C324E2 /* StartChatScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78910787F967CBC6042A101E /* StartChatScreenViewModelProtocol.swift */; }; - B434548FA94576FA45D93E3C /* ResolveVerifiedUserSendFailureViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFA1F23507009DF279BFBC84 /* ResolveVerifiedUserSendFailureViewState.swift */; }; B444F9C184A377C1B481F07F /* XCUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E992D7B8BE54B2AB454613AF /* XCUIElement.swift */; }; B4A0C69370E6008A971463E7 /* BugReportScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C89820BB2B88D4EA28131C /* BugReportScreenViewModelProtocol.swift */; }; B4AAB3257A83B73F53FB2689 /* StateStoreViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */; }; @@ -944,7 +947,6 @@ D415764645491F10344FC6AC /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60F18AECC9D38C2B6D85F99C /* Publisher.swift */; }; D43F0503EF2CBC55272538FE /* SDKGeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2F079B5DBD0D85FEA687AAE /* SDKGeneratedMocks.swift */; }; D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; - D4AB1B84F8B4DE6C3EA7C5D1 /* ResolveVerifiedUserSendFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB5EDA17B8A23B70BC8D62C /* ResolveVerifiedUserSendFailureView.swift */; }; D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; }; D4D5595C4A2A702CFF4E94FF /* HeroImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EC2F1622C5BBABED6012E12 /* HeroImage.swift */; }; D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; }; @@ -1044,6 +1046,7 @@ EC280623A42904341363EAAF /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = A20EA00CCB9DBE0FFB17DD09 /* Collections */; }; EC3320639828BED8B3E5F2C6 /* EncryptionResetScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875F7C0A2398E9F134B1284 /* EncryptionResetScreenViewModel.swift */; }; ECA636DAF071C611FDC2BB57 /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; + ED3E91E6166E4923791ACA84 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56852036214ABA9D7D305768 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift */; }; ED564C8C7C43CF5F67000368 /* PlatformViewVersionPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */; }; ED90A59F068FD0CA27E602ED /* UserProfileListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */; }; EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; }; @@ -1571,7 +1574,10 @@ 55AEEF8142DF1B59DB40FB93 /* TimelineItemSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemSender.swift; sourceTree = ""; }; 5644919DB2022397D9D5825A /* MockSoftLogoutScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftLogoutScreenState.swift; sourceTree = ""; }; 565F1B2B300597C616B37888 /* FullscreenDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenDialog.swift; sourceTree = ""; }; + 56852036214ABA9D7D305768 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift; sourceTree = ""; }; 56D6F88FE35A0979D2821E06 /* AppLockScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreen.swift; sourceTree = ""; }; + 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenViewModelTests.swift; sourceTree = ""; }; + 574CB70E82D7EAEA538E4135 /* ResolveVerifiedUserSendFailureScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenViewModel.swift; sourceTree = ""; }; 57916A1578D8043BB0795441 /* GeneratedMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedMocks.swift; sourceTree = ""; }; 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConsentState.swift; sourceTree = ""; }; 57EAAF82432B0B53881CF826 /* AudioRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItem.swift; sourceTree = ""; }; @@ -1605,6 +1611,7 @@ 5F12E996BFBEB43815189ABF /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = ""; }; 5F4134FEFE4EB55759017408 /* UserSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionProtocol.swift; sourceTree = ""; }; 5FACD034DB52525A3CEF2BDF /* SessionVerificationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreen.swift; sourceTree = ""; }; + 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenModels.swift; sourceTree = ""; }; 6033779EB37259F27F938937 /* ClientProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyProtocol.swift; sourceTree = ""; }; 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileScreenModels.swift; sourceTree = ""; }; 60C9BAE9F9436B14E4E22E8F /* PinnedItemsBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedItemsBannerView.swift; sourceTree = ""; }; @@ -1784,7 +1791,6 @@ 8BEBF0E59F25E842EDB6FD11 /* LocationSharingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSharingScreenModels.swift; sourceTree = ""; }; 8C44BBC892499BE45B074F89 /* AppLockScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenCoordinator.swift; sourceTree = ""; }; 8C8616254EE40CA8BA5E9BC2 /* VideoRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineItemContent.swift; sourceTree = ""; }; - 8CB5EDA17B8A23B70BC8D62C /* ResolveVerifiedUserSendFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureView.swift; sourceTree = ""; }; 8CC23C63849452BC86EA2852 /* ButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyle.swift; sourceTree = ""; }; 8D1FA20DAB853C1156054912 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; @@ -1825,6 +1831,7 @@ 95BAC0F6C9644336E9567EE6 /* NSRegularExpresion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSRegularExpresion.swift; sourceTree = ""; }; 969694F67E844FCA51F7E051 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/InfoPlist.strings; sourceTree = ""; }; 96C4762F8D6112E43117DB2F /* CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomStringConvertible.swift; sourceTree = ""; }; + 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreen.swift; sourceTree = ""; }; 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPermissions.swift; sourceTree = ""; }; 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenViewModelProtocol.swift; sourceTree = ""; }; 97B2ACA28A854E41AE3AC9AD /* TimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModel.swift; sourceTree = ""; }; @@ -1894,7 +1901,6 @@ A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = ""; }; - A849378B6EE12B44DBA4F9A6 /* ResolveVerifiedUserSendFailureViewStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureViewStateTests.swift; sourceTree = ""; }; A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = ""; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; A8DF55467ED4CE76B7AE9A33 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2033,6 +2039,7 @@ C5599255A6C98EBDA77B76E6 /* ImageRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRoomTimelineView.swift; sourceTree = ""; }; C55CC239AE12339C565F6C9A /* AudioRecorderStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderStateTests.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; + C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureScreenCoordinator.swift; sourceTree = ""; }; C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenUITests.swift; sourceTree = ""; }; C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = ""; }; C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2078,7 +2085,6 @@ CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; - CFA1F23507009DF279BFBC84 /* ResolveVerifiedUserSendFailureViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolveVerifiedUserSendFailureViewState.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; @@ -2584,8 +2590,6 @@ isa = PBXGroup; children = ( 02EE0FABA8ED6D6C1D6CE71D /* ReactionsSummaryView.swift */, - 8CB5EDA17B8A23B70BC8D62C /* ResolveVerifiedUserSendFailureView.swift */, - CFA1F23507009DF279BFBC84 /* ResolveVerifiedUserSendFailureViewState.swift */, 66F91544AC136BF6477BDAB8 /* TimelineDeliveryStatusView.swift */, 72614BFF35B8394C6E13F55A /* TimelineItemStatusView.swift */, 54AD70D6E03D2031AE1B5A52 /* TimelineReactionsView.swift */, @@ -3780,7 +3784,7 @@ 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, - A849378B6EE12B44DBA4F9A6 /* ResolveVerifiedUserSendFailureViewStateTests.swift */, + 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */, A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */, 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */, 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */, @@ -4078,6 +4082,14 @@ path = Tools; sourceTree = ""; }; + 829DDE5AE36ADD18677C150C /* View */ = { + isa = PBXGroup; + children = ( + 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 82D5AD3EAE3A5C1068A44A88 /* Session */ = { isa = PBXGroup; children = ( @@ -4463,6 +4475,18 @@ path = View; sourceTree = ""; }; + A040ACE4D778FFCD65DDF5F8 /* ResolveVerifiedUserSendFailureScreen */ = { + isa = PBXGroup; + children = ( + C5AEB5907E24092D741718AF /* ResolveVerifiedUserSendFailureScreenCoordinator.swift */, + 60011EF0086E49DBD78E16E5 /* ResolveVerifiedUserSendFailureScreenModels.swift */, + 574CB70E82D7EAEA538E4135 /* ResolveVerifiedUserSendFailureScreenViewModel.swift */, + 56852036214ABA9D7D305768 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift */, + 829DDE5AE36ADD18677C150C /* View */, + ); + path = ResolveVerifiedUserSendFailureScreen; + sourceTree = ""; + }; A0C06C0F6A8621B22BFAEB56 /* Localizations */ = { isa = PBXGroup; children = ( @@ -5156,6 +5180,7 @@ 3E535010B850B53DDD3CFF2A /* PinnedEventsTimelineScreen */, 3D733E8352DD4C461CFD8B8A /* QRCodeLoginScreen */, 5970F275D6014548DCED6106 /* ReportContentScreen */, + A040ACE4D778FFCD65DDF5F8 /* ResolveVerifiedUserSendFailureScreen */, DAB7DC51866A6D1B51BDC3A2 /* RoomChangePermissionsScreen */, D8388454B5909D862CAC78F7 /* RoomChangeRolesScreen */, E71742A824A7192C8D378875 /* RoomDetailsEditScreen */, @@ -6043,7 +6068,7 @@ D415764645491F10344FC6AC /* Publisher.swift in Sources */, BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, - A9062C5F94DF651F0D21196F /* ResolveVerifiedUserSendFailureViewStateTests.swift in Sources */, + 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */, C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */, 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */, D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */, @@ -6585,8 +6610,11 @@ 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */, 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */, 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */, - D4AB1B84F8B4DE6C3EA7C5D1 /* ResolveVerifiedUserSendFailureView.swift in Sources */, - B434548FA94576FA45D93E3C /* ResolveVerifiedUserSendFailureViewState.swift in Sources */, + 4715FE33667C5899E64DD0E6 /* ResolveVerifiedUserSendFailureScreen.swift in Sources */, + 583A41A4BE76E2E9E0B97881 /* ResolveVerifiedUserSendFailureScreenCoordinator.swift in Sources */, + 108D3C0707A90B0F848CDBB9 /* ResolveVerifiedUserSendFailureScreenModels.swift in Sources */, + 9A0326D2375075871D2AB537 /* ResolveVerifiedUserSendFailureScreenViewModel.swift in Sources */, + ED3E91E6166E4923791ACA84 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, 6E391F7F628D984AF44385D9 /* RoomAttachmentPicker.swift in Sources */, 8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index fe4add059e..8394c61f23 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -358,6 +358,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.roomDetails, .presentRoomMemberDetails(let userID)): return .roomMemberDetails(userID: userID, previousState: fromState) + case (.room, .presentResolveSendFailure): + return .resolveSendFailure + case (.resolveSendFailure, .dismissResolveSendFailure): + return .room + // Child flow case (_, .startChildFlow(let roomID, _, _)): @@ -508,6 +513,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.roomMemberDetails, .dismissUserProfile, .roomDetails): break + case (.room, .presentResolveSendFailure(let failure, let itemID), .resolveSendFailure): + presentResolveSendFailure(failure: failure, itemID: itemID) + case (.resolveSendFailure, .dismissResolveSendFailure, .room): + break + // Child flow case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild): Task { await self.startChildFlow(for: roomID, via: via, entryPoint: entryPoint) } @@ -632,6 +642,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) case .presentPinnedEventsTimeline: stateMachine.tryEvent(.presentPinnedEventsTimeline) + case .presentResolveSendFailure(failure: let failure, itemID: let itemID): + stateMachine.tryEvent(.presentResolveSendFailure(failure: failure, itemID: itemID)) } } .store(in: &cancellables) @@ -1360,6 +1372,25 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { coordinator.start() } + private func presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) { + let coordinator = ResolveVerifiedUserSendFailureScreenCoordinator(parameters: .init(failure: failure, + itemID: itemID, + roomProxy: roomProxy)) + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + + switch action { + case .dismiss: + navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + navigationStackCoordinator.setSheetCoordinator(coordinator) { [weak self] in + self?.stateMachine.tryEvent(.dismissResolveSendFailure) + } + } + // MARK: - Child Flow private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) async { @@ -1434,6 +1465,7 @@ private extension RoomFlowCoordinator { case pollsHistoryForm case rolesAndPermissions case pinnedEventsTimeline(previousState: PinnedEventsTimelineSource) + case resolveSendFailure /// A child flow is in progress. case presentingChild(childRoomID: String, previousState: State) @@ -1506,6 +1538,9 @@ private extension RoomFlowCoordinator { case presentPinnedEventsTimeline case dismissPinnedEventsTimeline + case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) + case dismissResolveSendFailure + // Child room flow events case startChildFlow(roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) case dismissChildFlow diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 1225c6f1cc..0af37090e5 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -11664,6 +11664,52 @@ open class RoomSDKMock: MatrixRustSDK.Room { } } + //MARK: - ignoreDeviceTrustAndResend + + open var ignoreDeviceTrustAndResendDevicesTransactionIdThrowableError: Error? + var ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = 0 + open var ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount: Int { + get { + if Thread.isMainThread { + return ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + ignoreDeviceTrustAndResendDevicesTransactionIdUnderlyingCallsCount = newValue + } + } + } + } + open var ignoreDeviceTrustAndResendDevicesTransactionIdCalled: Bool { + return ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount > 0 + } + open var ignoreDeviceTrustAndResendDevicesTransactionIdReceivedArguments: (devices: [String: [String]], transactionId: String)? + open var ignoreDeviceTrustAndResendDevicesTransactionIdReceivedInvocations: [(devices: [String: [String]], transactionId: String)] = [] + open var ignoreDeviceTrustAndResendDevicesTransactionIdClosure: (([String: [String]], String) async throws -> Void)? + + open override func ignoreDeviceTrustAndResend(devices: [String: [String]], transactionId: String) async throws { + if let error = ignoreDeviceTrustAndResendDevicesTransactionIdThrowableError { + throw error + } + ignoreDeviceTrustAndResendDevicesTransactionIdCallsCount += 1 + ignoreDeviceTrustAndResendDevicesTransactionIdReceivedArguments = (devices: devices, transactionId: transactionId) + DispatchQueue.main.async { + self.ignoreDeviceTrustAndResendDevicesTransactionIdReceivedInvocations.append((devices: devices, transactionId: transactionId)) + } + try await ignoreDeviceTrustAndResendDevicesTransactionIdClosure?(devices, transactionId) + } + //MARK: - ignoreUser open var ignoreUserUserIdThrowableError: Error? @@ -14421,52 +14467,6 @@ open class RoomSDKMock: MatrixRustSDK.Room { } } - //MARK: - trustDevicesAndResend - - open var trustDevicesAndResendDevicesTransactionIdThrowableError: Error? - var trustDevicesAndResendDevicesTransactionIdUnderlyingCallsCount = 0 - open var trustDevicesAndResendDevicesTransactionIdCallsCount: Int { - get { - if Thread.isMainThread { - return trustDevicesAndResendDevicesTransactionIdUnderlyingCallsCount - } else { - var returnValue: Int? = nil - DispatchQueue.main.sync { - returnValue = trustDevicesAndResendDevicesTransactionIdUnderlyingCallsCount - } - - return returnValue! - } - } - set { - if Thread.isMainThread { - trustDevicesAndResendDevicesTransactionIdUnderlyingCallsCount = newValue - } else { - DispatchQueue.main.sync { - trustDevicesAndResendDevicesTransactionIdUnderlyingCallsCount = newValue - } - } - } - } - open var trustDevicesAndResendDevicesTransactionIdCalled: Bool { - return trustDevicesAndResendDevicesTransactionIdCallsCount > 0 - } - open var trustDevicesAndResendDevicesTransactionIdReceivedArguments: (devices: [String: [String]], transactionId: String)? - open var trustDevicesAndResendDevicesTransactionIdReceivedInvocations: [(devices: [String: [String]], transactionId: String)] = [] - open var trustDevicesAndResendDevicesTransactionIdClosure: (([String: [String]], String) async throws -> Void)? - - open override func trustDevicesAndResend(devices: [String: [String]], transactionId: String) async throws { - if let error = trustDevicesAndResendDevicesTransactionIdThrowableError { - throw error - } - trustDevicesAndResendDevicesTransactionIdCallsCount += 1 - trustDevicesAndResendDevicesTransactionIdReceivedArguments = (devices: devices, transactionId: transactionId) - DispatchQueue.main.async { - self.trustDevicesAndResendDevicesTransactionIdReceivedInvocations.append((devices: devices, transactionId: transactionId)) - } - try await trustDevicesAndResendDevicesTransactionIdClosure?(devices, transactionId) - } - //MARK: - typingNotice open var typingNoticeIsTypingThrowableError: Error? diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 52c0cb7159..be98451542 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -87,7 +87,9 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { case .viewInRoomTimeline(let eventID): actionsSubject.send(.displayRoomScreenWithFocussedPin(eventID: eventID)) // These other actions will not be handled in this view - case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, .composer, .hasScrolled: + case .displayEmojiPicker, .displayReportContent, .displayCameraPicker, .displayMediaPicker, + .displayDocumentPicker, .displayLocationPicker, .displayPollForm, .displayMediaUploadPreviewScreen, + .displayResolveSendFailure, .composer, .hasScrolled: // These actions are not handled in this coordinator break } diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift new file mode 100644 index 0000000000..d1c697b13f --- /dev/null +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift @@ -0,0 +1,65 @@ +// +// 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 ResolveVerifiedUserSendFailureScreenCoordinatorParameters { + let failure: TimelineItemSendFailure.VerifiedUser + let itemID: TimelineItemIdentifier + let roomProxy: JoinedRoomProxyProtocol +} + +enum ResolveVerifiedUserSendFailureScreenCoordinatorAction { + case dismiss +} + +final class ResolveVerifiedUserSendFailureScreenCoordinator: CoordinatorProtocol { + private let parameters: ResolveVerifiedUserSendFailureScreenCoordinatorParameters + private let viewModel: ResolveVerifiedUserSendFailureScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: ResolveVerifiedUserSendFailureScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: parameters.failure, + itemID: parameters.itemID, + roomProxy: parameters.roomProxy) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { + case .dismiss: + actionsSubject.send(.dismiss) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(ResolveVerifiedUserSendFailureScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift new file mode 100644 index 0000000000..cec02bfa46 --- /dev/null +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift @@ -0,0 +1,54 @@ +// +// 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 ResolveVerifiedUserSendFailureScreenViewModelAction { + case dismiss +} + +struct ResolveVerifiedUserSendFailureScreenViewState: BindableState { + var currentFailure: TimelineItemSendFailure.VerifiedUser + var currentMemberDisplayName: String + + var title: String { + switch currentFailure { + case .hasUnsignedDevice: UntranslatedL10n.screenRoomSendFailureUnsignedDeviceResolveTitle(currentMemberDisplayName) + case .changedIdentity: UntranslatedL10n.screenRoomSendFailureIdentityChangedResolveTitle(currentMemberDisplayName) + } + } + + var subtitle: String { + switch currentFailure { + case .hasUnsignedDevice: UntranslatedL10n.screenRoomSendFailureUnsignedDeviceResolveSubtitle(currentMemberDisplayName, + currentMemberDisplayName) + case .changedIdentity: UntranslatedL10n.screenRoomSendFailureIdentityChangedResolveSubtitle(currentMemberDisplayName) + } + } + + var primaryButtonTitle: String { + switch currentFailure { + case .hasUnsignedDevice: UntranslatedL10n.screenRoomSendFailureUnsignedDeviceResolvePrimaryButtonTitle + case .changedIdentity: UntranslatedL10n.screenRoomSendFailureIdentityChangedResolvePrimaryButtonTitle + } + } +} + +enum ResolveVerifiedUserSendFailureScreenViewAction { + case resolveAndResend + case resend + case cancel +} diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift new file mode 100644 index 0000000000..441f2f9572 --- /dev/null +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift @@ -0,0 +1,128 @@ +// +// 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 ResolveVerifiedUserSendFailureScreenViewModelType = StateStoreViewModel + +class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFailureScreenViewModelType, ResolveVerifiedUserSendFailureScreenViewModelProtocol { + private let iterator: VerifiedUserSendFailureIterator + private let failure: TimelineItemSendFailure.VerifiedUser + private let itemID: TimelineItemIdentifier + private let roomProxy: JoinedRoomProxyProtocol + private var members: [String: RoomMemberProxyProtocol] + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier, roomProxy: JoinedRoomProxyProtocol) { + iterator = switch failure { + case .hasUnsignedDevice(let devices): UnsignedDeviceFailureIterator(devices: devices) + case .changedIdentity(let users): ChangedIdentityFailureIterator(users: users) + } + + self.failure = failure + self.itemID = itemID + self.roomProxy = roomProxy + + members = Dictionary(uniqueKeysWithValues: roomProxy.membersPublisher.value.map { ($0.userID, $0) }) + + guard let (userID, failure) = iterator.next() else { + MXLog.error("There aren't any known users/devices to resolve the failure with.") + fatalError("There aren't any known users/devices to resolve the failure with.") + } + + super.init(initialViewState: ResolveVerifiedUserSendFailureScreenViewState(currentFailure: failure, + currentMemberDisplayName: members[userID]?.displayName ?? userID)) + } + + // MARK: Public + + override func process(viewAction: ResolveVerifiedUserSendFailureScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .resolveAndResend: + Task { await resolveAndResend() } + case .resend: + Task { await resend() } + case .cancel: + actionsSubject.send(.dismiss) + } + } + + private func resolveAndResend() async { + let result = switch failure { + case .hasUnsignedDevice(let devices): + await roomProxy.ignoreDeviceTrustAndResend(devices: devices, itemID: itemID) + case .changedIdentity(let users): + await roomProxy.withdrawVerificationAndResend(userIDs: users, itemID: itemID) + } + + if case let .failure(error) = result { + #warning("Show the error?") + return + } + + if let (userID, failure) = iterator.next() { + state.currentMemberDisplayName = members[userID]?.displayName ?? userID + state.currentFailure = failure + } else { + actionsSubject.send(.dismiss) + } + } + + private func resend() async { + // Do something with the room proxy here. + actionsSubject.send(.dismiss) + } +} + +// MARK: - Iterators + +@MainActor +private protocol VerifiedUserSendFailureIterator { + func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? +} + +private class UnsignedDeviceFailureIterator: VerifiedUserSendFailureIterator { + private var iterator: [String: [String]].Iterator + + init(devices: [String: [String]]) { + iterator = devices.makeIterator() + } + + func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? { + guard let nextUserDevices = iterator.next() else { return nil } + return (nextUserDevices.key, .hasUnsignedDevice(devices: [nextUserDevices.key: nextUserDevices.value])) + } +} + +private class ChangedIdentityFailureIterator: VerifiedUserSendFailureIterator { + private var iterator: [String].Iterator + + init(users: [String]) { + iterator = users.makeIterator() + } + + func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? { + guard let nextUserID = iterator.next() else { return nil } + return (nextUserID, .changedIdentity(users: [nextUserID])) + } +} diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift new file mode 100644 index 0000000000..2bdc7bb557 --- /dev/null +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// 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 ResolveVerifiedUserSendFailureScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: ResolveVerifiedUserSendFailureScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/ResolveVerifiedUserSendFailureView.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift similarity index 58% rename from ElementX/Sources/Screens/Timeline/View/Supplementary/ResolveVerifiedUserSendFailureView.swift rename to ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift index c13322753d..91750eeaa9 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/ResolveVerifiedUserSendFailureView.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift @@ -1,5 +1,5 @@ // -// Copyright 2024 New Vector Ltd +// 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. @@ -14,11 +14,11 @@ // limitations under the License. // +import Compound import SwiftUI -@MainActor -struct ResolveVerifiedUserSendFailureView: View { - @StateObject var viewState: ResolveVerifiedUserSendFailureViewState +struct ResolveVerifiedUserSendFailureScreen: View { + @ObservedObject var context: ResolveVerifiedUserSendFailureScreenViewModel.Context @State private var sheetFrame: CGRect = .zero var body: some View { @@ -41,12 +41,12 @@ struct ResolveVerifiedUserSendFailureView: View { HeroImage(icon: \.error, style: .critical) .padding(.bottom, 8) - Text(viewState.title) + Text(context.viewState.title) .font(.compound.headingMDBold) .multilineTextAlignment(.center) .foregroundColor(.compound.textPrimary) - Text(viewState.subtitle) + Text(context.viewState.subtitle) .font(.compound.bodyMD) .multilineTextAlignment(.center) .foregroundColor(.compound.textSecondary) @@ -55,17 +55,17 @@ struct ResolveVerifiedUserSendFailureView: View { var buttons: some View { VStack(spacing: 16) { - Button(viewState.primaryButtonTitle) { - viewState.resolveAndSend() + Button(context.viewState.primaryButtonTitle) { + context.send(viewAction: .resolveAndResend) } .buttonStyle(.compound(.primary)) Button(L10n.actionRetry) { - viewState.retry() + context.send(viewAction: .resend) } .buttonStyle(.compound(.secondary)) - Button { viewState.cancel() } label: { + Button { context.send(viewAction: .cancel) } label: { Text(UntranslatedL10n.actionCancelForNow) .padding(.vertical, 14) } @@ -76,31 +76,34 @@ struct ResolveVerifiedUserSendFailureView: View { // MARK: - Previews -struct TimelineSendFailureInfoView_Previews: PreviewProvider, TestablePreview { - static let viewModel = TimelineViewModel.mock +struct ResolveVerifiedUserSendFailureScreen_Previews: PreviewProvider, TestablePreview { + static let unsignedDeviceViewModel = makeViewModel(failure: .hasUnsignedDevice(devices: ["@alice:matrix.org": []])) + static let changedIdentityViewModel = makeViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"])) static var previews: some View { - ResolveVerifiedUserSendFailureView(viewState: .init(info: .init(id: .random, - failure: .hasUnsignedDevice(devices: ["@alice:matrix.org": []])), - context: viewModel.context)) + ResolveVerifiedUserSendFailureScreen(context: unsignedDeviceViewModel.context) .previewDisplayName("Unsigned Device") - ResolveVerifiedUserSendFailureView(viewState: .init(info: .init(id: .random, - failure: .changedIdentity(users: ["@alice:matrix.org"])), - context: viewModel.context)) + ResolveVerifiedUserSendFailureScreen(context: changedIdentityViewModel.context) .previewDisplayName("Identity Changed") } + + static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { + ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, + itemID: .random, + roomProxy: JoinedRoomProxyMock(.init())) + } } -struct TimelineSendFailureInfoViewSheet_Previews: PreviewProvider { - static let viewModel = TimelineViewModel.mock +struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider, TestablePreview { + static let viewModel = ResolveVerifiedUserSendFailureScreenViewModel(failure: .changedIdentity(users: ["@alice:matrix.org"]), + itemID: .random, + roomProxy: JoinedRoomProxyMock(.init())) static var previews: some View { Text("Hello") .sheet(isPresented: .constant(true)) { - ResolveVerifiedUserSendFailureView(viewState: .init(info: .init(id: .random, - failure: .changedIdentity(users: ["@alice:matrix.org"])), - context: viewModel.context)) + ResolveVerifiedUserSendFailureScreen(context: viewModel.context) } .previewDisplayName("Sheet") } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index f9bc2f8cf9..2eddd1dfbb 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -47,6 +47,7 @@ enum RoomScreenCoordinatorAction { case presentMessageForwarding(forwardingItem: MessageForwardingItem) case presentCallScreen case presentPinnedEventsTimeline + case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) } final class RoomScreenCoordinator: CoordinatorProtocol { @@ -135,6 +136,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem)) case .displayLocation(let body, let geoURI, let description): actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description)) + case .displayResolveSendFailure(let failure, let itemID): + actionsSubject.send(.presentResolveSendFailure(failure: failure, itemID: itemID)) case .composer(let action): composerViewModel.process(timelineAction: action) case .hasScrolled(direction: let direction): diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 49b0583da5..aa99ca1ccf 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -83,9 +83,6 @@ struct RoomScreen: View { .environmentObject(timelineContext) } } - .sheet(item: $timelineContext.sendFailureInfo) { - ResolveVerifiedUserSendFailureView(viewState: .init(info: $0, context: timelineContext)) - } .sheet(item: $timelineContext.reactionSummaryInfo) { ReactionsSummaryView(reactions: $0.reactions, members: timelineContext.viewState.members, diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index f5685a6805..733bb3d91e 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -187,24 +187,6 @@ class TimelineInteractionHandler { } } - func handleTimelineSendFailureAction(_ action: TimelineSendFailureAction) { - switch action { - case .resolveAndSend(let failure, let itemID): - switch failure { - case .hasUnsignedDevice(let devices): - #warning("How to handle failures??") - Task { await roomProxy.ignoreDeviceTrustAndResend(devices: devices, itemID: itemID) } - case .changedIdentity(let users): - #warning("How to handle failures??") - Task { await roomProxy.withdrawVerificationAndResend(userIDs: users, itemID: itemID) } - } - case .retrySending(let itemID): - break // retry sending. - case .cancel: - break - } - } - private func processEditMessageEvent(_ messageTimelineItem: EventBasedMessageTimelineItemProtocol) { let text: String var htmlText: String? diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 31178c8649..15f339e57d 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -30,6 +30,7 @@ enum TimelineViewModelAction { case tappedOnSenderDetails(userID: String) case displayMessageForwarding(forwardingItem: MessageForwardingItem) case displayLocation(body: String, geoURI: GeoURI, description: String?) + case displayResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) case composer(action: TimelineComposerAction) case hasScrolled(direction: ScrollDirection) case viewInRoomTimeline(eventID: String) @@ -60,7 +61,6 @@ enum TimelineViewAction { case displayTimelineItemMenu(itemID: TimelineItemIdentifier) case handleTimelineItemMenuAction(itemID: TimelineItemIdentifier, action: TimelineItemMenuAction) - case handleTimelineSendFailureAction(TimelineSendFailureAction) case tappedOnSenderDetails(userID: String) case displayReactionSummary(itemID: TimelineItemIdentifier, key: String) @@ -90,12 +90,6 @@ enum TimelineComposerAction { case clear } -enum TimelineSendFailureAction { - case resolveAndSend(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) - case retrySending(itemID: TimelineItemIdentifier) - case cancel -} - struct TimelineViewState: BindableState { let isPinnedEventsTimeline: Bool var roomID: String @@ -138,8 +132,6 @@ struct TimelineViewStateBindings { var actionMenuInfo: TimelineItemActionMenuInfo? - var sendFailureInfo: TimelineItemSendFailureInfo? - var reactionSummaryInfo: ReactionSummaryInfo? var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? @@ -157,11 +149,6 @@ struct TimelineItemActionMenuInfo: Equatable, Identifiable { } } -struct TimelineItemSendFailureInfo: Identifiable { - let id: TimelineItemIdentifier - let failure: TimelineItemSendFailure.VerifiedUser -} - struct ReactionSummaryInfo: Identifiable { let reactions: [AggregatedReaction] let selectedKey: String diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 4929ed828d..0138c7df95 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -152,8 +152,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID) case .handleTimelineItemMenuAction(let itemID, let action): timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID) - case .handleTimelineSendFailureAction(let action): - timelineInteractionHandler.handleTimelineSendFailureAction(action) case .tappedOnSenderDetails(userID: let userID): actionsSubject.send(.tappedOnSenderDetails(userID: userID)) case .displayEmojiPicker(let itemID): @@ -564,7 +562,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { if case .sendingFailed(.unknown) = eventTimelineItem.properties.deliveryStatus { displayAlert(.sendingFailed) } else if case let .sendingFailed(.verifiedUser(failure)) = eventTimelineItem.properties.deliveryStatus { - state.bindings.sendFailureInfo = .init(id: itemID, failure: failure) + actionsSubject.send(.displayResolveSendFailure(failure: failure, itemID: itemID)) } else if let authenticityMessage = eventTimelineItem.properties.encryptionAuthenticity?.message { displayAlert(.encryptionAuthenticity(authenticityMessage)) } diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/ResolveVerifiedUserSendFailureViewState.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/ResolveVerifiedUserSendFailureViewState.swift deleted file mode 100644 index 8d8367bd86..0000000000 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/ResolveVerifiedUserSendFailureViewState.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// Copyright 2024 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 - -@MainActor -class ResolveVerifiedUserSendFailureViewState: ObservableObject { - private let iterator: VerifiedUserSendFailureIterator - private let info: TimelineItemSendFailureInfo - private let context: TimelineViewModel.Context - - private var currentFailure: TimelineItemSendFailure.VerifiedUser - @Published private var currentMemberDisplayName: String - - init(info: TimelineItemSendFailureInfo, context: TimelineViewModel.Context) { - iterator = switch info.failure { - case .hasUnsignedDevice(let devices): UnsignedDeviceFailureIterator(devices: devices) - case .changedIdentity(let users): ChangedIdentityFailureIterator(users: users) - } - - self.info = info - self.context = context - - guard let (userID, failure) = iterator.next() else { fatalError() } - currentMemberDisplayName = context.viewState.members[userID]?.displayName ?? userID - currentFailure = failure - } - - var title: String { - switch info.failure { - case .hasUnsignedDevice: UntranslatedL10n.screenRoomSendFailureUnsignedDeviceResolveTitle(currentMemberDisplayName) - case .changedIdentity: UntranslatedL10n.screenRoomSendFailureIdentityChangedResolveTitle(currentMemberDisplayName) - } - } - - var subtitle: String { - switch info.failure { - case .hasUnsignedDevice: UntranslatedL10n.screenRoomSendFailureUnsignedDeviceResolveSubtitle(currentMemberDisplayName, - currentMemberDisplayName) - case .changedIdentity: UntranslatedL10n.screenRoomSendFailureIdentityChangedResolveSubtitle(currentMemberDisplayName) - } - } - - var primaryButtonTitle: String { - switch info.failure { - case .hasUnsignedDevice: UntranslatedL10n.screenRoomSendFailureUnsignedDeviceResolvePrimaryButtonTitle - case .changedIdentity: UntranslatedL10n.screenRoomSendFailureIdentityChangedResolvePrimaryButtonTitle - } - } - - func resolveAndSend() { - send(.resolveAndSend(failure: currentFailure, itemID: info.id), dismiss: false) - - if let (userID, failure) = iterator.next() { - currentMemberDisplayName = context.viewState.members[userID]?.displayName ?? userID - currentFailure = failure - } else { - context.sendFailureInfo = nil - } - } - - func retry() { - send(.retrySending(itemID: info.id)) - } - - func cancel() { - send(.cancel) - } - - private func send(_ action: TimelineSendFailureAction, dismiss: Bool = true) { - context.send(viewAction: .handleTimelineSendFailureAction(action)) - - if dismiss { - context.sendFailureInfo = nil - } - } -} - -@MainActor -protocol VerifiedUserSendFailureIterator { - func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? -} - -class UnsignedDeviceFailureIterator: VerifiedUserSendFailureIterator { - private var iterator: [String: [String]].Iterator - - init(devices: [String: [String]]) { - iterator = devices.makeIterator() - } - - func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? { - guard let nextUserDevices = iterator.next() else { return nil } - return (nextUserDevices.key, .hasUnsignedDevice(devices: [nextUserDevices.key: nextUserDevices.value])) - } -} - -class ChangedIdentityFailureIterator: VerifiedUserSendFailureIterator { - private var iterator: [String].Iterator - - init(users: [String]) { - iterator = users.makeIterator() - } - - func next() -> (userID: String, failure: TimelineItemSendFailure.VerifiedUser)? { - guard let nextUserID = iterator.next() else { return nil } - return (nextUserID, .changedIdentity(users: [nextUserID])) - } -} diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 42d13fd0bd..aa2e4213ab 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -397,7 +397,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } do { - try await room.trustDevicesAndResend(devices: devices, transactionId: transactionID) + try await room.ignoreDeviceTrustAndResend(devices: devices, transactionId: transactionID) return .success(()) } catch { MXLog.error("Failed trusting devices \(devices) and resending \(transactionID) with error: \(error)") diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureViewStateTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift similarity index 63% rename from UnitTests/Sources/ResolveVerifiedUserSendFailureViewStateTests.swift rename to UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift index 06d1ff2096..cc3186c9f0 100644 --- a/UnitTests/Sources/ResolveVerifiedUserSendFailureViewStateTests.swift +++ b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift @@ -1,5 +1,5 @@ // -// Copyright 2024 New Vector Ltd +// 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. @@ -19,21 +19,15 @@ import XCTest @testable import ElementX @MainActor -class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { - var viewModel: TimelineViewModel! - var context: TimelineViewModel.Context! - var viewState: ResolveVerifiedUserSendFailureViewState! - - override func setUp() async throws { - viewModel = .mock - context = viewModel.context - } +class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase { + let roomProxy = JoinedRoomProxyMock(.init()) + var viewModel: ResolveVerifiedUserSendFailureScreenViewModel! + var context: ResolveVerifiedUserSendFailureScreenViewModel.Context { viewModel.context } func testUnsignedDevice() async throws { // Given a failure where a single user has an unverified device let userID = "@alice:matrix.org" - viewState = makeViewState(with: .hasUnsignedDevice(devices: [userID: ["DEVICE1"]])) - XCTAssertNotNil(context.sendFailureInfo) + viewModel = makeViewModel(with: .hasUnsignedDevice(devices: [userID: ["DEVICE1"]])) try await verifyResolving(userIDs: [userID]) } @@ -42,8 +36,7 @@ class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { // Given a failure where a multiple users have unverified devices. let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"] let devices = Dictionary(uniqueKeysWithValues: userIDs.map { (key: $0, value: ["DEVICE1, DEVICE2"]) }) - viewState = makeViewState(with: .hasUnsignedDevice(devices: devices)) - XCTAssertNotNil(context.sendFailureInfo) + viewModel = makeViewModel(with: .hasUnsignedDevice(devices: devices)) try await verifyResolving(userIDs: userIDs, assertStrings: false) } @@ -51,8 +44,7 @@ class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { func testChangedIdentity() async throws { // Given a failure where a single user's identity has changed. let userID = "@alice:matrix.org" - viewState = makeViewState(with: .changedIdentity(users: [userID])) - XCTAssertNotNil(context.sendFailureInfo) + viewModel = makeViewModel(with: .changedIdentity(users: [userID])) try await verifyResolving(userIDs: [userID]) } @@ -60,18 +52,15 @@ class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { func testMultipleChangedIdentities() async throws { // Given a failure where a multiple users have unverified devices. let userIDs = ["@alice:matrix.org", "@bob:matrix.org", "@charlie:matrix.org"] - viewState = makeViewState(with: .changedIdentity(users: userIDs)) - XCTAssertNotNil(context.sendFailureInfo) + viewModel = makeViewModel(with: .changedIdentity(users: userIDs)) try await verifyResolving(userIDs: userIDs) } // MARK: Helpers - private func makeViewState(with failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureViewState { - let sendFailureInfo = TimelineItemSendFailureInfo(id: .random, failure: failure) - context.sendFailureInfo = sendFailureInfo - return ResolveVerifiedUserSendFailureViewState(info: sendFailureInfo, context: context) + private func makeViewModel(with failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { + ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, itemID: .random, roomProxy: roomProxy) } private func verifyResolving(userIDs: [String], assertStrings: Bool = true) async throws { @@ -84,12 +73,11 @@ class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { } // When resolving the first failure. - let deferredFailure = deferFailure(context.$viewState, timeout: 1) { $0.bindings.sendFailureInfo == nil } - viewState.resolveAndSend() - try await deferredFailure.fulfill() + let deferredFailure = deferFailure(viewModel.actionsPublisher, timeout: 1) { $0.isDismiss } + context.send(viewAction: .resolveAndResend) // Then the sheet should remain open for the next failure. - XCTAssertNotNil(context.sendFailureInfo) + try await deferredFailure.fulfill() remainingUserIDs.removeFirst() } @@ -100,12 +88,11 @@ class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { } // When resolving the final failure. - let deferred = deferFulfillment(context.$viewState) { $0.bindings.sendFailureInfo == nil } - viewState.resolveAndSend() - try await deferred.fulfill() + let deferred = deferFulfillment(viewModel.actionsPublisher) { $0.isDismiss } + context.send(viewAction: .resolveAndResend) // Then the sheet should be dismissed. - XCTAssertNil(context.sendFailureInfo) + try await deferred.fulfill() } private func verifyDisplayName(from remainingUserIDs: [String]) { @@ -114,12 +101,21 @@ class ResolveVerifiedUserSendFailureViewStateTests: XCTestCase { return } - guard let displayName = context.viewState.members[userID]?.displayName else { + guard let displayName = roomProxy.membersPublisher.value.first(where: { $0.userID == userID })?.displayName else { XCTFail("There should be a matching mock user") return } - XCTAssertTrue(viewState.title.contains(displayName)) - XCTAssertTrue(viewState.subtitle.contains(displayName)) + XCTAssertTrue(context.viewState.title.contains(displayName)) + XCTAssertTrue(context.viewState.subtitle.contains(displayName)) + } +} + +private extension ResolveVerifiedUserSendFailureScreenViewModelAction { + var isDismiss: Bool { + switch self { + case .dismiss: true + default: false + } } }