From cc4942fb78861ad2cfcfca1cd8db966331c6652e Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Tue, 10 Sep 2024 09:56:11 +0100 Subject: [PATCH] Require acknowledgement to send to verified users who have unsigned devices or have changed their identity. (#3215) * Refactor HeroImage style. * Add a screen to resolve (crypto-related) timeline item send failures. * Refactor send failures. * Update the SDK. --- ElementX.xcodeproj/project.pbxproj | 42 ++- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../en.lproj/Localizable.strings | 18 +- .../RoomFlowCoordinator.swift | 35 +++ ElementX/Sources/Generated/Strings.swift | 54 ++-- .../Mocks/Generated/GeneratedMocks.swift | 210 ++++++++++++++ .../Mocks/Generated/SDKGeneratedMocks.swift | 266 ++++++++++++++++-- .../Sources/Mocks/JoinedRoomProxyMock.swift | 4 + .../Other/Extensions/ClientBuilder.swift | 1 + .../Other/SwiftUI/Views/HeroImage.swift | 48 ++-- .../View/EncryptionResetScreen.swift | 2 +- .../View/IdentityConfirmedScreen.swift | 2 +- ...innedEventsTimelineScreenCoordinator.swift | 4 +- .../View/QRCodeLoginScreen.swift | 4 +- ...fiedUserSendFailureScreenCoordinator.swift | 65 +++++ ...eVerifiedUserSendFailureScreenModels.swift | 53 ++++ ...rifiedUserSendFailureScreenViewModel.swift | 132 +++++++++ ...erSendFailureScreenViewModelProtocol.swift | 23 ++ ...ResolveVerifiedUserSendFailureScreen.swift | 110 ++++++++ .../RoomScreen/RoomScreenCoordinator.swift | 3 + .../Screens/RoomScreen/View/RoomScreen.swift | 5 +- .../Screens/Timeline/TimelineModels.swift | 1 + .../Screens/Timeline/TimelineViewModel.swift | 5 +- .../View/ItemMenu/TimelineItemMenu.swift | 91 +++++- .../Services/Room/JoinedRoomProxy.swift | 52 +++- .../Services/Room/RoomProxyProtocol.swift | 7 + .../Services/Timeline/TimelineItemProxy.swift | 30 +- PreviewTests/Sources/PreviewTests.swift | 6 + .../test_heroImage-iPad-en-GB.1.png | 4 +- .../test_heroImage-iPad-pseudo.1.png | 4 +- .../test_heroImage-iPhone-15-en-GB.1.png | 4 +- .../test_heroImage-iPhone-15-pseudo.1.png | 4 +- ...lureScreen-iPad-en-GB.Identity-Changed.png | 3 + ...ilureScreen-iPad-en-GB.Unsigned-Device.png | 3 + ...ureScreen-iPad-pseudo.Identity-Changed.png | 3 + ...lureScreen-iPad-pseudo.Unsigned-Device.png | 3 + ...creen-iPhone-15-en-GB.Identity-Changed.png | 3 + ...Screen-iPhone-15-en-GB.Unsigned-Device.png | 3 + ...reen-iPhone-15-pseudo.Identity-Changed.png | 3 + ...creen-iPhone-15-pseudo.Unsigned-Device.png | 3 + ...iPad-en-GB.Authenticity-not-guaranteed.png | 3 - ...melineItemMenu-iPad-en-GB.Authenticity.png | 3 + ...elineItemMenu-iPad-en-GB.Button-shapes.png | 3 + ...neItemMenu-iPad-en-GB.Identity-Changed.png | 3 + ...est_timelineItemMenu-iPad-en-GB.Normal.png | 3 + ...imelineItemMenu-iPad-en-GB.Unencrypted.png | 4 +- ...ineItemMenu-iPad-en-GB.Unknown-failure.png | 3 + ...neItemMenu-iPad-en-GB.Unsigned-Devices.png | 3 + ...t_timelineItemMenu-iPad-en-GB.Unsigned.png | 4 +- ...Menu-iPad-en-GB.With-button-shapes-off.png | 3 - ...mMenu-iPad-en-GB.With-button-shapes-on.png | 3 - ...Pad-pseudo.Authenticity-not-guaranteed.png | 3 - ...elineItemMenu-iPad-pseudo.Authenticity.png | 3 + ...lineItemMenu-iPad-pseudo.Button-shapes.png | 3 + ...eItemMenu-iPad-pseudo.Identity-Changed.png | 3 + ...st_timelineItemMenu-iPad-pseudo.Normal.png | 3 + ...melineItemMenu-iPad-pseudo.Unencrypted.png | 4 +- ...neItemMenu-iPad-pseudo.Unknown-failure.png | 3 + ...eItemMenu-iPad-pseudo.Unsigned-Devices.png | 3 + ..._timelineItemMenu-iPad-pseudo.Unsigned.png | 4 +- ...enu-iPad-pseudo.With-button-shapes-off.png | 3 - ...Menu-iPad-pseudo.With-button-shapes-on.png | 3 - ...e-15-en-GB.Authenticity-not-guaranteed.png | 3 - ...eItemMenu-iPhone-15-en-GB.Authenticity.png | 3 + ...ItemMenu-iPhone-15-en-GB.Button-shapes.png | 3 + ...mMenu-iPhone-15-en-GB.Identity-Changed.png | 3 + ...imelineItemMenu-iPhone-15-en-GB.Normal.png | 3 + ...neItemMenu-iPhone-15-en-GB.Unencrypted.png | 4 +- ...emMenu-iPhone-15-en-GB.Unknown-failure.png | 3 + ...mMenu-iPhone-15-en-GB.Unsigned-Devices.png | 3 + ...elineItemMenu-iPhone-15-en-GB.Unsigned.png | 4 +- ...iPhone-15-en-GB.With-button-shapes-off.png | 3 - ...-iPhone-15-en-GB.With-button-shapes-on.png | 3 - ...-15-pseudo.Authenticity-not-guaranteed.png | 3 - ...ItemMenu-iPhone-15-pseudo.Authenticity.png | 3 + ...temMenu-iPhone-15-pseudo.Button-shapes.png | 3 + ...Menu-iPhone-15-pseudo.Identity-Changed.png | 3 + ...melineItemMenu-iPhone-15-pseudo.Normal.png | 3 + ...eItemMenu-iPhone-15-pseudo.Unencrypted.png | 4 +- ...mMenu-iPhone-15-pseudo.Unknown-failure.png | 3 + ...Menu-iPhone-15-pseudo.Unsigned-Devices.png | 3 + ...lineItemMenu-iPhone-15-pseudo.Unsigned.png | 4 +- ...Phone-15-pseudo.With-button-shapes-off.png | 3 - ...iPhone-15-pseudo.With-button-shapes-on.png | 3 - ...dUserSendFailureScreenViewModelTests.swift | 121 ++++++++ project.yml | 2 +- 86 files changed, 1422 insertions(+), 158 deletions(-) create mode 100644 ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift create mode 100644 ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity-not-guaranteed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Button-shapes.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Normal.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unknown-failure.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-off.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-on.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity-not-guaranteed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Button-shapes.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Normal.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unknown-failure.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-off.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-on.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity-not-guaranteed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Button-shapes.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Normal.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unknown-failure.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-off.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-on.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity-not-guaranteed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Button-shapes.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Identity-Changed.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Normal.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unknown-failure.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-off.png delete mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-on.png create mode 100644 UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index ed52e7b20a..52a671bbc2 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 */; }; @@ -310,6 +312,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 */; }; @@ -385,6 +388,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 */; }; @@ -683,6 +687,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 */; }; @@ -1030,6 +1035,7 @@ EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; EC280623A42904341363EAAF /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = A20EA00CCB9DBE0FFB17DD09 /* Collections */; }; EC3320639828BED8B3E5F2C6 /* EncryptionResetScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5875F7C0A2398E9F134B1284 /* EncryptionResetScreenViewModel.swift */; }; + ED3E91E6166E4923791ACA84 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56852036214ABA9D7D305768 /* ResolveVerifiedUserSendFailureScreenViewModelProtocol.swift */; }; ED564C8C7C43CF5F67000368 /* PlatformViewVersionPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26813CCE39221FE30BF22CD /* PlatformViewVersionPredicate.swift */; }; ED635D7F00FA07E94D3CE1E8 /* PreviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B796D347E53631576F631C /* PreviewTests.swift */; }; ED90A59F068FD0CA27E602ED /* UserProfileListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10DA51DBC8C7E1460DBCCBD /* UserProfileListRow.swift */; }; @@ -1555,7 +1561,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 = ""; }; @@ -1589,6 +1598,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 = ""; }; @@ -1807,6 +1817,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 = ""; }; @@ -2013,6 +2024,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 = ""; }; @@ -3763,6 +3775,7 @@ 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, + 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */, A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */, 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */, 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */, @@ -4059,6 +4072,14 @@ path = Tools; sourceTree = ""; }; + 829DDE5AE36ADD18677C150C /* View */ = { + isa = PBXGroup; + children = ( + 97287090CA64DAA95386ECED /* ResolveVerifiedUserSendFailureScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 82D5AD3EAE3A5C1068A44A88 /* Session */ = { isa = PBXGroup; children = ( @@ -4435,6 +4456,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 = ( @@ -5116,6 +5149,7 @@ 3E535010B850B53DDD3CFF2A /* PinnedEventsTimelineScreen */, 3D733E8352DD4C461CFD8B8A /* QRCodeLoginScreen */, 5970F275D6014548DCED6106 /* ReportContentScreen */, + A040ACE4D778FFCD65DDF5F8 /* ResolveVerifiedUserSendFailureScreen */, DAB7DC51866A6D1B51BDC3A2 /* RoomChangePermissionsScreen */, D8388454B5909D862CAC78F7 /* RoomChangeRolesScreen */, E71742A824A7192C8D378875 /* RoomDetailsEditScreen */, @@ -6016,6 +6050,7 @@ D415764645491F10344FC6AC /* Publisher.swift in Sources */, BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, + 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */, C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */, 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */, D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */, @@ -6557,6 +6592,11 @@ 46A261AA898344A1F3C406B1 /* ReportContentScreenModels.swift in Sources */, 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */, 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.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 */, @@ -7647,7 +7687,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.44; + version = 1.0.45; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1ec476fabc..8dc5864120 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "343d7045a0ad6fe508728f624a02698e679327fb", - "version" : "1.0.44" + "revision" : "103b7000e5191485873a81386d0134d71bd9fc36", + "version" : "1.0.45" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 7840094e27..fa28d3093b 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -27,6 +27,7 @@ "action_back" = "Back"; "action_call" = "Call"; "action_cancel" = "Cancel"; +"action_cancel_for_now" = "Cancel for now"; "action_choose_photo" = "Choose photo"; "action_clear" = "Clear"; "action_close" = "Close"; @@ -106,6 +107,10 @@ "action_view_source" = "View source"; "action_yes" = "Yes"; "action.load_more" = "Load more"; +"banner_migrate_to_native_sliding_sync_action" = "Log Out & Upgrade"; +"banner_migrate_to_native_sliding_sync_description" = "Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later."; +"banner_migrate_to_native_sliding_sync_force_logout_title" = "Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app."; +"banner_migrate_to_native_sliding_sync_title" = "Upgrade available"; "banner.set_up_recovery.content" = "Generate a new recovery key that can be used to restore your encrypted message history in case you lose access to your devices."; "banner.set_up_recovery.title" = "Set up recovery"; "common_about" = "About"; @@ -325,12 +330,20 @@ "screen_pinned_timeline_empty_state_headline" = "Pin important messages so that they can be easily discovered"; "screen_pinned_timeline_screen_title_empty" = "Pinned messages"; "screen_reset_encryption_password_error" = "An unknown error happened. Please check your account password is correct and try again."; +"screen_resolve_send_failure_changed_identity_primary_button_title" = "Withdraw verification and send"; +"screen_resolve_send_failure_changed_identity_subtitle" = "You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@."; +"screen_resolve_send_failure_changed_identity_title" = "Your message was not sent because %1$@’s verified identity has changed"; +"screen_resolve_send_failure_unsigned_device_primary_button_title" = "Send message anyway"; +"screen_resolve_send_failure_unsigned_device_subtitle" = "%1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices."; +"screen_resolve_send_failure_unsigned_device_title" = "Your message was not sent because %1$@ has not verified one or more devices"; "screen_room_mentions_at_room_subtitle" = "Notify the whole room"; "screen_room_pinned_banner_indicator" = "%1$@ of %2$@"; "screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages"; "screen_room_pinned_banner_loading_description" = "Loading message…"; "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; +"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed."; +"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified one or more devices."; "screen_account_provider_change" = "Change account provider"; "screen_account_provider_form_hint" = "Homeserver address"; "screen_account_provider_form_notice" = "Enter a search term or a domain address."; @@ -579,7 +592,6 @@ "screen_recovery_key_confirm_error_content" = "Please try again to confirm access to your chat backup."; "screen_recovery_key_confirm_error_title" = "Incorrect recovery key"; "screen_recovery_key_confirm_key_description" = "If you have a security key or security phrase, this will work too."; -"screen_recovery_key_confirm_key_label" = "Recovery key or passcode"; "screen_recovery_key_confirm_key_placeholder" = "Enter…"; "screen_recovery_key_confirm_lost_recovery_key" = "Lost your recovery key?"; "screen_recovery_key_confirm_success" = "Recovery key confirmed"; @@ -806,9 +818,6 @@ "screen_signout_save_recovery_key_title" = "Have you saved your recovery key?"; "screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; "screen_view_location_title" = "Location"; -"screen_waitlist_message" = "There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again.\n\nThanks for your patience!"; -"screen_waitlist_title" = "You’re almost there."; -"screen_waitlist_title_success" = "You're in."; "screen_welcome_bullet_1" = "Calls, polls, search and more will be added later this year."; "screen_welcome_bullet_2" = "Message history for encrypted rooms isn’t available yet."; "screen_welcome_bullet_3" = "We’d love to hear from you, let us know what you think via the settings page."; @@ -983,4 +992,3 @@ "screen_signout_confirmation_dialog_submit" = "Sign out"; "screen_signout_confirmation_dialog_title" = "Sign out"; "screen_signout_preference_item" = "Sign out"; -"screen_waitlist_message_success" = "Welcome to %1$@!"; diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index f91553e3fa..918fc155a0 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -349,6 +349,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, _, _)): @@ -499,6 +504,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) } @@ -623,6 +633,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) @@ -1351,6 +1363,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 { @@ -1425,6 +1456,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) @@ -1497,6 +1529,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/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 7d7b18a799..e0508015c7 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -84,6 +84,8 @@ internal enum L10n { internal static var actionCall: String { return L10n.tr("Localizable", "action_call") } /// Cancel internal static var actionCancel: String { return L10n.tr("Localizable", "action_cancel") } + /// Cancel for now + internal static var actionCancelForNow: String { return L10n.tr("Localizable", "action_cancel_for_now") } /// Choose photo internal static var actionChoosePhoto: String { return L10n.tr("Localizable", "action_choose_photo") } /// Clear @@ -244,6 +246,14 @@ internal enum L10n { internal static var actionViewSource: String { return L10n.tr("Localizable", "action_view_source") } /// Yes internal static var actionYes: String { return L10n.tr("Localizable", "action_yes") } + /// Log Out & Upgrade + internal static var bannerMigrateToNativeSlidingSyncAction: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_action") } + /// Your server now supports a new, faster protocol. Log out and log back in to upgrade now. Doing this now will help you avoid a forced logout when the old protocol is removed later. + internal static var bannerMigrateToNativeSlidingSyncDescription: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_description") } + /// Your homeserver no longer supports the old protocol. Please log out and log back in to continue using the app. + internal static var bannerMigrateToNativeSlidingSyncForceLogoutTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_force_logout_title") } + /// Upgrade available + internal static var bannerMigrateToNativeSlidingSyncTitle: String { return L10n.tr("Localizable", "banner_migrate_to_native_sliding_sync_title") } /// About internal static var commonAbout: String { return L10n.tr("Localizable", "common_about") } /// Acceptable use policy @@ -1383,8 +1393,6 @@ internal enum L10n { internal static var screenRecoveryKeyConfirmErrorTitle: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_error_title") } /// If you have a security key or security phrase, this will work too. internal static var screenRecoveryKeyConfirmKeyDescription: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_description") } - /// Recovery key or passcode - internal static var screenRecoveryKeyConfirmKeyLabel: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_label") } /// Enter… internal static var screenRecoveryKeyConfirmKeyPlaceholder: String { return L10n.tr("Localizable", "screen_recovery_key_confirm_key_placeholder") } /// Lost your recovery key? @@ -1447,6 +1455,26 @@ internal enum L10n { } /// Can't confirm? Go to your account to reset your identity. internal static var screenResetIdentityConfirmationTitle: String { return L10n.tr("Localizable", "screen_reset_identity_confirmation_title") } + /// Withdraw verification and send + internal static var screenResolveSendFailureChangedIdentityPrimaryButtonTitle: String { return L10n.tr("Localizable", "screen_resolve_send_failure_changed_identity_primary_button_title") } + /// You can withdraw your verification and send this message anyway, or you can cancel for now and try again later after reverifying %1$@. + internal static func screenResolveSendFailureChangedIdentitySubtitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_resolve_send_failure_changed_identity_subtitle", String(describing: p1)) + } + /// Your message was not sent because %1$@’s verified identity has changed + internal static func screenResolveSendFailureChangedIdentityTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_resolve_send_failure_changed_identity_title", String(describing: p1)) + } + /// Send message anyway + internal static var screenResolveSendFailureUnsignedDevicePrimaryButtonTitle: String { return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_primary_button_title") } + /// %1$@ is using one or more unverified devices. You can send the message anyway, or you can cancel for now and try again later after %2$@ has verified all their devices. + internal static func screenResolveSendFailureUnsignedDeviceSubtitle(_ p1: Any, _ p2: Any) -> String { + return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_subtitle", String(describing: p1), String(describing: p2)) + } + /// Your message was not sent because %1$@ has not verified one or more devices + internal static func screenResolveSendFailureUnsignedDeviceTitle(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_resolve_send_failure_unsigned_device_title", String(describing: p1)) + } /// Failed to resolve room alias. internal static var screenRoomAliasResolverResolveAliasFailure: String { return L10n.tr("Localizable", "screen_room_alias_resolver_resolve_alias_failure") } /// Camera @@ -1945,22 +1973,16 @@ internal enum L10n { internal static var screenSignoutSaveRecoveryKeyTitle: String { return L10n.tr("Localizable", "screen_signout_save_recovery_key_title") } /// An error occurred when trying to start a chat internal static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") } - /// Location - internal static var screenViewLocationTitle: String { return L10n.tr("Localizable", "screen_view_location_title") } - /// There's a high demand for %1$@ on %2$@ at the moment. Come back to the app in a few days and try again. - /// - /// Thanks for your patience! - internal static func screenWaitlistMessage(_ p1: Any, _ p2: Any) -> String { - return L10n.tr("Localizable", "screen_waitlist_message", String(describing: p1), String(describing: p2)) + /// Message not sent because %1$@’s verified identity has changed. + internal static func screenTimelineItemMenuSendFailureChangedIdentity(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_timeline_item_menu_send_failure_changed_identity", String(describing: p1)) } - /// Welcome to %1$@! - internal static func screenWaitlistMessageSuccess(_ p1: Any) -> String { - return L10n.tr("Localizable", "screen_waitlist_message_success", String(describing: p1)) + /// Message not sent because %1$@ has not verified one or more devices. + internal static func screenTimelineItemMenuSendFailureUnsignedDevice(_ p1: Any) -> String { + return L10n.tr("Localizable", "screen_timeline_item_menu_send_failure_unsigned_device", String(describing: p1)) } - /// You’re almost there. - internal static var screenWaitlistTitle: String { return L10n.tr("Localizable", "screen_waitlist_title") } - /// You're in. - internal static var screenWaitlistTitleSuccess: String { return L10n.tr("Localizable", "screen_waitlist_title_success") } + /// Location + internal static var screenViewLocationTitle: String { return L10n.tr("Localizable", "screen_view_location_title") } /// Calls, polls, search and more will be added later this year. internal static var screenWelcomeBullet1: String { return L10n.tr("Localizable", "screen_welcome_bullet_1") } /// Message history for encrypted rooms isn’t available yet. diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 064d992c0a..1b788c023c 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -6563,6 +6563,216 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol { return sendTypingNotificationIsTypingReturnValue } } + //MARK: - resend + + var resendItemIDUnderlyingCallsCount = 0 + var resendItemIDCallsCount: Int { + get { + if Thread.isMainThread { + return resendItemIDUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = resendItemIDUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + resendItemIDUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + resendItemIDUnderlyingCallsCount = newValue + } + } + } + } + var resendItemIDCalled: Bool { + return resendItemIDCallsCount > 0 + } + var resendItemIDReceivedItemID: TimelineItemIdentifier? + var resendItemIDReceivedInvocations: [TimelineItemIdentifier] = [] + + var resendItemIDUnderlyingReturnValue: Result! + var resendItemIDReturnValue: Result! { + get { + if Thread.isMainThread { + return resendItemIDUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = resendItemIDUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + resendItemIDUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + resendItemIDUnderlyingReturnValue = newValue + } + } + } + } + var resendItemIDClosure: ((TimelineItemIdentifier) async -> Result)? + + func resend(itemID: TimelineItemIdentifier) async -> Result { + resendItemIDCallsCount += 1 + resendItemIDReceivedItemID = itemID + DispatchQueue.main.async { + self.resendItemIDReceivedInvocations.append(itemID) + } + if let resendItemIDClosure = resendItemIDClosure { + return await resendItemIDClosure(itemID) + } else { + return resendItemIDReturnValue + } + } + //MARK: - ignoreDeviceTrustAndResend + + var ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = 0 + var ignoreDeviceTrustAndResendDevicesItemIDCallsCount: Int { + get { + if Thread.isMainThread { + return ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + ignoreDeviceTrustAndResendDevicesItemIDUnderlyingCallsCount = newValue + } + } + } + } + var ignoreDeviceTrustAndResendDevicesItemIDCalled: Bool { + return ignoreDeviceTrustAndResendDevicesItemIDCallsCount > 0 + } + var ignoreDeviceTrustAndResendDevicesItemIDReceivedArguments: (devices: [String: [String]], itemID: TimelineItemIdentifier)? + var ignoreDeviceTrustAndResendDevicesItemIDReceivedInvocations: [(devices: [String: [String]], itemID: TimelineItemIdentifier)] = [] + + var ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue: Result! + var ignoreDeviceTrustAndResendDevicesItemIDReturnValue: Result! { + get { + if Thread.isMainThread { + return ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + ignoreDeviceTrustAndResendDevicesItemIDUnderlyingReturnValue = newValue + } + } + } + } + var ignoreDeviceTrustAndResendDevicesItemIDClosure: (([String: [String]], TimelineItemIdentifier) async -> Result)? + + func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result { + ignoreDeviceTrustAndResendDevicesItemIDCallsCount += 1 + ignoreDeviceTrustAndResendDevicesItemIDReceivedArguments = (devices: devices, itemID: itemID) + DispatchQueue.main.async { + self.ignoreDeviceTrustAndResendDevicesItemIDReceivedInvocations.append((devices: devices, itemID: itemID)) + } + if let ignoreDeviceTrustAndResendDevicesItemIDClosure = ignoreDeviceTrustAndResendDevicesItemIDClosure { + return await ignoreDeviceTrustAndResendDevicesItemIDClosure(devices, itemID) + } else { + return ignoreDeviceTrustAndResendDevicesItemIDReturnValue + } + } + //MARK: - withdrawVerificationAndResend + + var withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = 0 + var withdrawVerificationAndResendUserIDsItemIDCallsCount: Int { + get { + if Thread.isMainThread { + return withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + withdrawVerificationAndResendUserIDsItemIDUnderlyingCallsCount = newValue + } + } + } + } + var withdrawVerificationAndResendUserIDsItemIDCalled: Bool { + return withdrawVerificationAndResendUserIDsItemIDCallsCount > 0 + } + var withdrawVerificationAndResendUserIDsItemIDReceivedArguments: (userIDs: [String], itemID: TimelineItemIdentifier)? + var withdrawVerificationAndResendUserIDsItemIDReceivedInvocations: [(userIDs: [String], itemID: TimelineItemIdentifier)] = [] + + var withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue: Result! + var withdrawVerificationAndResendUserIDsItemIDReturnValue: Result! { + get { + if Thread.isMainThread { + return withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + withdrawVerificationAndResendUserIDsItemIDUnderlyingReturnValue = newValue + } + } + } + } + var withdrawVerificationAndResendUserIDsItemIDClosure: (([String], TimelineItemIdentifier) async -> Result)? + + func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result { + withdrawVerificationAndResendUserIDsItemIDCallsCount += 1 + withdrawVerificationAndResendUserIDsItemIDReceivedArguments = (userIDs: userIDs, itemID: itemID) + DispatchQueue.main.async { + self.withdrawVerificationAndResendUserIDsItemIDReceivedInvocations.append((userIDs: userIDs, itemID: itemID)) + } + if let withdrawVerificationAndResendUserIDsItemIDClosure = withdrawVerificationAndResendUserIDsItemIDClosure { + return await withdrawVerificationAndResendUserIDsItemIDClosure(userIDs, itemID) + } else { + return withdrawVerificationAndResendUserIDsItemIDReturnValue + } + } //MARK: - flagAsUnread var flagAsUnreadUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 80c231d0f7..2f5ec419dc 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -9974,6 +9974,82 @@ open class NotificationSettingsSDKMock: MatrixRustSDK.NotificationSettings { try await unmuteRoomRoomIdIsEncryptedIsOneToOneClosure?(roomId, isEncrypted, isOneToOne) } } +open class OidcAuthorizationDataSDKMock: MatrixRustSDK.OidcAuthorizationData { + init() { + super.init(noPointer: .init()) + } + + public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + fatalError("init(unsafeFromRawPointer:) has not been implemented") + } + + fileprivate var pointer: UnsafeMutableRawPointer! + + //MARK: - loginUrl + + var loginUrlUnderlyingCallsCount = 0 + open var loginUrlCallsCount: Int { + get { + if Thread.isMainThread { + return loginUrlUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = loginUrlUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + loginUrlUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + loginUrlUnderlyingCallsCount = newValue + } + } + } + } + open var loginUrlCalled: Bool { + return loginUrlCallsCount > 0 + } + + var loginUrlUnderlyingReturnValue: String! + open var loginUrlReturnValue: String! { + get { + if Thread.isMainThread { + return loginUrlUnderlyingReturnValue + } else { + var returnValue: String? = nil + DispatchQueue.main.sync { + returnValue = loginUrlUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + loginUrlUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + loginUrlUnderlyingReturnValue = newValue + } + } + } + } + open var loginUrlClosure: (() -> String)? + + open override func loginUrl() -> String { + loginUrlCallsCount += 1 + if let loginUrlClosure = loginUrlClosure { + return loginUrlClosure() + } else { + return loginUrlReturnValue + } + } +} open class QrCodeDataSDKMock: MatrixRustSDK.QrCodeData { init() { super.init(noPointer: .init()) @@ -11588,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? @@ -13149,16 +13271,16 @@ open class RoomSDKMock: MatrixRustSDK.Room { //MARK: - pinnedEventsTimeline - open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError: Error? - var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = 0 - open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount: Int { + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsThrowableError: Error? + var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount = 0 + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCallsCount: Int { get { if Thread.isMainThread { - return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount + returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount } return returnValue! @@ -13166,29 +13288,29 @@ open class RoomSDKMock: MatrixRustSDK.Room { } set { if Thread.isMainThread { - pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingCallsCount = newValue } } } } - open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCalled: Bool { - return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount > 0 + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCalled: Bool { + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCallsCount > 0 } - open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments: (internalIdPrefix: String?, maxEventsToLoad: UInt16)? - open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations: [(internalIdPrefix: String?, maxEventsToLoad: UInt16)] = [] + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedArguments: (internalIdPrefix: String?, maxEventsToLoad: UInt16, maxConcurrentRequests: UInt16)? + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedInvocations: [(internalIdPrefix: String?, maxEventsToLoad: UInt16, maxConcurrentRequests: UInt16)] = [] - var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue: Timeline! - open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue: Timeline! { + var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue: Timeline! + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReturnValue: Timeline! { get { if Thread.isMainThread { - return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue } else { var returnValue: Timeline? = nil DispatchQueue.main.sync { - returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue + returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue } return returnValue! @@ -13196,29 +13318,29 @@ open class RoomSDKMock: MatrixRustSDK.Room { } set { if Thread.isMainThread { - pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsUnderlyingReturnValue = newValue } } } } - open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure: ((String?, UInt16) async throws -> Timeline)? + open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure: ((String?, UInt16, UInt16) async throws -> Timeline)? - open override func pinnedEventsTimeline(internalIdPrefix: String?, maxEventsToLoad: UInt16) async throws -> Timeline { - if let error = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError { + open override func pinnedEventsTimeline(internalIdPrefix: String?, maxEventsToLoad: UInt16, maxConcurrentRequests: UInt16) async throws -> Timeline { + if let error = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsThrowableError { throw error } - pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount += 1 - pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments = (internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad) + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsCallsCount += 1 + pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedArguments = (internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad, maxConcurrentRequests: maxConcurrentRequests) DispatchQueue.main.async { - self.pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations.append((internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad)) + self.pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReceivedInvocations.append((internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad, maxConcurrentRequests: maxConcurrentRequests)) } - if let pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure { - return try await pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure(internalIdPrefix, maxEventsToLoad) + if let pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure { + return try await pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsClosure(internalIdPrefix, maxEventsToLoad, maxConcurrentRequests) } else { - return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue + return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadMaxConcurrentRequestsReturnValue } } @@ -14345,6 +14467,52 @@ open class RoomSDKMock: MatrixRustSDK.Room { } } + //MARK: - tryResend + + open var tryResendTransactionIdThrowableError: Error? + var tryResendTransactionIdUnderlyingCallsCount = 0 + open var tryResendTransactionIdCallsCount: Int { + get { + if Thread.isMainThread { + return tryResendTransactionIdUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = tryResendTransactionIdUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + tryResendTransactionIdUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + tryResendTransactionIdUnderlyingCallsCount = newValue + } + } + } + } + open var tryResendTransactionIdCalled: Bool { + return tryResendTransactionIdCallsCount > 0 + } + open var tryResendTransactionIdReceivedTransactionId: String? + open var tryResendTransactionIdReceivedInvocations: [String] = [] + open var tryResendTransactionIdClosure: ((String) async throws -> Void)? + + open override func tryResend(transactionId: String) async throws { + if let error = tryResendTransactionIdThrowableError { + throw error + } + tryResendTransactionIdCallsCount += 1 + tryResendTransactionIdReceivedTransactionId = transactionId + DispatchQueue.main.async { + self.tryResendTransactionIdReceivedInvocations.append(transactionId) + } + try await tryResendTransactionIdClosure?(transactionId) + } + //MARK: - typingNotice open var typingNoticeIsTypingThrowableError: Error? @@ -14528,6 +14696,52 @@ open class RoomSDKMock: MatrixRustSDK.Room { } try await uploadAvatarMimeTypeDataMediaInfoClosure?(mimeType, data, mediaInfo) } + + //MARK: - withdrawVerificationAndResend + + open var withdrawVerificationAndResendUserIdsTransactionIdThrowableError: Error? + var withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = 0 + open var withdrawVerificationAndResendUserIdsTransactionIdCallsCount: Int { + get { + if Thread.isMainThread { + return withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + withdrawVerificationAndResendUserIdsTransactionIdUnderlyingCallsCount = newValue + } + } + } + } + open var withdrawVerificationAndResendUserIdsTransactionIdCalled: Bool { + return withdrawVerificationAndResendUserIdsTransactionIdCallsCount > 0 + } + open var withdrawVerificationAndResendUserIdsTransactionIdReceivedArguments: (userIds: [String], transactionId: String)? + open var withdrawVerificationAndResendUserIdsTransactionIdReceivedInvocations: [(userIds: [String], transactionId: String)] = [] + open var withdrawVerificationAndResendUserIdsTransactionIdClosure: (([String], String) async throws -> Void)? + + open override func withdrawVerificationAndResend(userIds: [String], transactionId: String) async throws { + if let error = withdrawVerificationAndResendUserIdsTransactionIdThrowableError { + throw error + } + withdrawVerificationAndResendUserIdsTransactionIdCallsCount += 1 + withdrawVerificationAndResendUserIdsTransactionIdReceivedArguments = (userIds: userIds, transactionId: transactionId) + DispatchQueue.main.async { + self.withdrawVerificationAndResendUserIdsTransactionIdReceivedInvocations.append((userIds: userIds, transactionId: transactionId)) + } + try await withdrawVerificationAndResendUserIdsTransactionIdClosure?(userIds, transactionId) + } } open class RoomDirectorySearchSDKMock: MatrixRustSDK.RoomDirectorySearch { init() { diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index ac3ac56d9a..86e33a7a48 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -94,6 +94,10 @@ extension JoinedRoomProxyMock { } return .success(member) } + + resendItemIDReturnValue = .success(()) + ignoreDeviceTrustAndResendDevicesItemIDReturnValue = .success(()) + withdrawVerificationAndResendUserIDsItemIDReturnValue = .success(()) flagAsUnreadReturnValue = .success(()) markAsReadReceiptTypeReturnValue = .success(()) diff --git a/ElementX/Sources/Other/Extensions/ClientBuilder.swift b/ElementX/Sources/Other/Extensions/ClientBuilder.swift index cece99632a..23e835f770 100644 --- a/ElementX/Sources/Other/Extensions/ClientBuilder.swift +++ b/ElementX/Sources/Other/Extensions/ClientBuilder.swift @@ -19,6 +19,7 @@ extension ClientBuilder { .enableCrossProcessRefreshLock(processId: InfoPlistReader.main.bundleIdentifier, sessionDelegate: sessionDelegate) .userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent()) .requestConfig(config: .init(retryLimit: 0, timeout: 30000, maxConcurrentRequests: nil, retryTimeout: nil)) + .roomKeyRecipientStrategy(strategy: .deviceBasedStrategy(onlyAllowTrustedDevices: false, errorOnVerifiedUserProblem: true)) builder = switch slidingSync { case .restored: builder diff --git a/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift b/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift index 586b2262de..f50ecb11e3 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/HeroImage.swift @@ -14,33 +14,36 @@ import SwiftUI struct HeroImage: View { enum Style { case normal - case positive case subtle + case success case critical + case criticalOnSecondary var foregroundColor: Color { switch self { case .normal: - return .compound.iconPrimary - case .positive: - return .compound.iconSuccessPrimary + .compound.iconPrimary case .subtle: - return .compound.iconSecondary - case .critical: - return .compound.iconCriticalPrimary + .compound.iconSecondary + case .success: + .compound.iconSuccessPrimary + case .critical, .criticalOnSecondary: + .compound.iconCriticalPrimary } } var backgroundFillColor: Color { switch self { case .normal: - return .compound.bgSubtleSecondary - case .positive: - return .compound.bgSuccessSubtle + .compound.bgSubtleSecondary case .subtle: - return .compound.bgSubtlePrimary + .compound.bgSubtlePrimary + case .success: + .compound.bgSuccessSubtle case .critical: - return .compound.bgCanvasDefault + .compound.bgCriticalSubtle + case .criticalOnSecondary: + .compound.bgCanvasDefault } } } @@ -86,12 +89,21 @@ private struct HeroImageModifier: ViewModifier { struct HeroImage_Previews: PreviewProvider, TestablePreview { static var previews: some View { - HStack(spacing: 20) { - HeroImage(icon: \.lockSolid) - Image(systemName: "hourglass") - .heroImage() - Image(asset: Asset.Images.serverSelectionIcon) - .heroImage(insets: 19) + VStack(spacing: 20) { + HStack(spacing: 20) { + HeroImage(icon: \.lockSolid) + Image(systemName: "hourglass") + .heroImage() + Image(asset: Asset.Images.serverSelectionIcon) + .heroImage(insets: 19) + } + + HStack(spacing: 20) { + HeroImage(icon: \.helpSolid, style: .subtle) + HeroImage(icon: \.checkCircleSolid, style: .success) + HeroImage(icon: \.error, style: .critical) + HeroImage(icon: \.error, style: .criticalOnSecondary) + } } } } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift index 7e2555b018..ab03039bcc 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift @@ -39,7 +39,7 @@ struct EncryptionResetScreen: View { private var header: some View { VStack(spacing: 8) { - HeroImage(icon: \.error, style: .critical) + HeroImage(icon: \.error, style: .criticalOnSecondary) .padding(.bottom, 8) Text(L10n.screenEncryptionResetTitle) diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift index 27726d3eb1..fc8b705727 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift @@ -29,7 +29,7 @@ struct IdentityConfirmedScreen: View { @ViewBuilder private var screenHeader: some View { VStack(spacing: 0) { - HeroImage(icon: \.checkCircle, style: .positive) + HeroImage(icon: \.checkCircle, style: .success) .padding(.bottom, 16) Text(L10n.screenIdentityConfirmedTitle) diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 20f415ecee..dc3b7820da 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -78,7 +78,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/QRCodeLoginScreen/View/QRCodeLoginScreen.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift index f9fa677c22..a0a9a0b664 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeLoginScreen.swift @@ -266,7 +266,7 @@ struct QRCodeLoginScreen: View { case .connectionNotSecure: VStack(spacing: 40) { VStack(spacing: 16) { - HeroImage(icon: \.error, style: .critical) + HeroImage(icon: \.error, style: .criticalOnSecondary) VStack(spacing: 8) { Text(L10n.screenQrCodeLoginConnectionNoteSecureStateTitle) @@ -332,7 +332,7 @@ struct QRCodeLoginScreen: View { } VStack(spacing: 16) { - HeroImage(icon: \.error, style: .critical) + HeroImage(icon: \.error, style: .criticalOnSecondary) VStack(spacing: 8) { Text(title) 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..f78418d060 --- /dev/null +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenModels.swift @@ -0,0 +1,53 @@ +// +// 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: L10n.screenResolveSendFailureUnsignedDeviceTitle(currentMemberDisplayName) + case .changedIdentity: L10n.screenResolveSendFailureChangedIdentityTitle(currentMemberDisplayName) + } + } + + var subtitle: String { + switch currentFailure { + case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDeviceSubtitle(currentMemberDisplayName, currentMemberDisplayName) + case .changedIdentity: L10n.screenResolveSendFailureChangedIdentitySubtitle(currentMemberDisplayName) + } + } + + var primaryButtonTitle: String { + switch currentFailure { + case .hasUnsignedDevice: L10n.screenResolveSendFailureUnsignedDevicePrimaryButtonTitle + case .changedIdentity: L10n.screenResolveSendFailureChangedIdentityPrimaryButtonTitle + } + } +} + +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..99abf24686 --- /dev/null +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift @@ -0,0 +1,132 @@ +// +// 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 { + switch await roomProxy.resend(itemID: itemID) { + case .success: + actionsSubject.send(.dismiss) + case .failure(let failure): + #warning("Show the error?") + } + } +} + +// 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/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift new file mode 100644 index 0000000000..eaa2b0ddd3 --- /dev/null +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift @@ -0,0 +1,110 @@ +// +// Copyright 2022 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Compound +import SwiftUI + +struct ResolveVerifiedUserSendFailureScreen: View { + @ObservedObject var context: ResolveVerifiedUserSendFailureScreenViewModel.Context + @State private var sheetFrame: CGRect = .zero + + var body: some View { + ScrollView { + VStack(spacing: 40) { + header + buttons + } + .padding(.top, 24) + .padding(.bottom, 16) + .padding(.horizontal, 16) + .readFrame($sheetFrame) + } + .scrollBounceBehavior(.basedOnSize) + .presentationDetents([.height(sheetFrame.height)]) + } + + var header: some View { + VStack(spacing: 8) { + HeroImage(icon: \.error, style: .critical) + .padding(.bottom, 8) + + Text(context.viewState.title) + .font(.compound.headingMDBold) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textPrimary) + + Text(context.viewState.subtitle) + .font(.compound.bodyMD) + .multilineTextAlignment(.center) + .foregroundColor(.compound.textSecondary) + } + } + + var buttons: some View { + VStack(spacing: 16) { + Button(context.viewState.primaryButtonTitle) { + context.send(viewAction: .resolveAndResend) + } + .buttonStyle(.compound(.primary)) + + Button(L10n.actionRetry) { + context.send(viewAction: .resend) + } + .buttonStyle(.compound(.secondary)) + + Button { context.send(viewAction: .cancel) } label: { + Text(L10n.actionCancelForNow) + .padding(.vertical, 14) + } + .buttonStyle(.compound(.plain)) + } + } +} + +// MARK: - Previews + +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 { + ResolveVerifiedUserSendFailureScreen(context: unsignedDeviceViewModel.context) + .previewDisplayName("Unsigned Device") + + ResolveVerifiedUserSendFailureScreen(context: changedIdentityViewModel.context) + .previewDisplayName("Identity Changed") + } + + static func makeViewModel(failure: TimelineItemSendFailure.VerifiedUser) -> ResolveVerifiedUserSendFailureScreenViewModel { + ResolveVerifiedUserSendFailureScreenViewModel(failure: failure, + itemID: .random, + roomProxy: JoinedRoomProxyMock(.init())) + } +} + +struct ResolveVerifiedUserSendFailureScreenSheet_Previews: PreviewProvider { + 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)) { + ResolveVerifiedUserSendFailureScreen(context: viewModel.context) + } + .previewDisplayName("Sheet") + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 2ee11691be..05249aab20 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -38,6 +38,7 @@ enum RoomScreenCoordinatorAction { case presentMessageForwarding(forwardingItem: MessageForwardingItem) case presentCallScreen case presentPinnedEventsTimeline + case presentResolveSendFailure(failure: TimelineItemSendFailure.VerifiedUser, itemID: TimelineItemIdentifier) } final class RoomScreenCoordinator: CoordinatorProtocol { @@ -126,6 +127,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 87ac181163..e1f1b81ea0 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -75,7 +75,10 @@ struct RoomScreen: View { } } .sheet(item: $timelineContext.reactionSummaryInfo) { - ReactionsSummaryView(reactions: $0.reactions, members: timelineContext.viewState.members, mediaProvider: timelineContext.mediaProvider, selectedReactionKey: $0.selectedKey) + ReactionsSummaryView(reactions: $0.reactions, + members: timelineContext.viewState.members, + mediaProvider: timelineContext.mediaProvider, + selectedReactionKey: $0.selectedKey) .edgesIgnoringSafeArea([.bottom]) } .sheet(item: $timelineContext.readReceiptsSummaryInfo) { diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 5992eb0387..6f47c96625 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -21,6 +21,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) diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index a2d3be83aa..03df560cee 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -550,9 +550,10 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { fatalError("Only events can have send info.") } - if case .sendingFailed = eventTimelineItem.properties.deliveryStatus { - // In the future we will show different errors for the various failure reasons. + if case .sendingFailed(.unknown) = eventTimelineItem.properties.deliveryStatus { displayAlert(.sendingFailed) + } else if case let .sendingFailed(.verifiedUser(failure)) = eventTimelineItem.properties.deliveryStatus { + 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/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index 3dccf5bd78..4e3339b66f 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -21,6 +21,9 @@ struct TimelineItemMenu: View { var body: some View { VStack(spacing: 8) { messagePreview + .padding(.horizontal, 16) + .padding(.top, 32.0) + .padding(.bottom, 4.0) .frame(idealWidth: 300.0) Divider() @@ -30,7 +33,6 @@ struct TimelineItemMenu: View { VStack(alignment: .leading, spacing: 0.0) { if !actions.reactions.isEmpty { reactionsSection - .padding(.top, 4.0) .padding(.bottom, 8.0) Divider() @@ -87,15 +89,20 @@ struct TimelineItemMenu: View { } .accessibilityElement(children: .combine) - if let authenticity = item.properties.encryptionAuthenticity { + if case let .sendingFailed(.verifiedUser(failure)) = item.properties.deliveryStatus { + Divider() + .padding(.horizontal, -16) + + VerifiedUserSendFailureView(failure: failure, members: context.viewState.members) { + send(.itemSendInfoTapped(itemID: item.id)) + } + .padding(.bottom, 8) + } else if let authenticity = item.properties.encryptionAuthenticity { Label(authenticity.message, icon: authenticity.icon, iconSize: .small, relativeTo: .compound.bodySMSemibold) .font(.compound.bodySMSemibold) .foregroundStyle(authenticity.foregroundStyle) } } - .padding(.horizontal) - .padding(.top, 32.0) - .padding(.bottom, 4.0) } private var reactionsSection: some View { @@ -162,10 +169,50 @@ struct TimelineItemMenu: View { } private func send(_ action: TimelineItemMenuAction) { + send(.handleTimelineItemMenuAction(itemID: item.id, action: action)) + } + + private func send(_ action: TimelineViewAction) { dismiss() // Otherwise we might get errors that a sheet is already presented DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - context.send(viewAction: .handleTimelineItemMenuAction(itemID: item.id, action: action)) + context.send(viewAction: action) + } + } +} + +private struct VerifiedUserSendFailureView: View { + let failure: TimelineItemSendFailure.VerifiedUser + let action: () -> Void + + private let memberDisplayName: String + + init(failure: TimelineItemSendFailure.VerifiedUser, + members: [String: RoomMemberState], + action: @escaping () -> Void) { + self.failure = failure + self.action = action + + let userIDs = failure.affectedUserIDs + memberDisplayName = userIDs.first.map { members[$0]?.displayName ?? $0 } ?? "" + } + + var title: String { + switch failure { + case .hasUnsignedDevice: L10n.screenTimelineItemMenuSendFailureUnsignedDevice(memberDisplayName) + case .changedIdentity: L10n.screenTimelineItemMenuSendFailureChangedIdentity(memberDisplayName) + } + } + + var body: some View { + Button(action: action) { + HStack(spacing: 8) { + Label(title, icon: \.error, iconSize: .small, relativeTo: .compound.bodySMSemibold) + .font(.compound.bodySMSemibold) + .foregroundStyle(.compound.textCriticalPrimary) + .frame(maxWidth: .infinity, alignment: .leading) + ListRowAccessory.navigationLink + } } } } @@ -187,20 +234,27 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { static let (backupItem, _) = makeItem(authenticity: .notGuaranteed(color: .gray)) static let (unsignedItem, _) = makeItem(authenticity: .unsignedDevice(color: .red)) static let (unencryptedItem, _) = makeItem(authenticity: .sentInClear(color: .red)) + static let (unknownFailureItem, _) = makeItem(deliveryStatus: .sendingFailed(.unknown)) + static let (identityChangedItem, _) = makeItem(deliveryStatus: .sendingFailed(.verifiedUser(.changedIdentity(users: [ + "@alice:matrix.org" + ])))) + static let (unsignedDevicesItem, _) = makeItem(deliveryStatus: .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: [ + "@alice:matrix.org": ["DEVICE1", "DEVICE2"] + ])))) static var previews: some View { TimelineItemMenu(item: item, actions: actions) .environmentObject(viewModel.context) - .previewDisplayName("With button shapes off") + .previewDisplayName("Normal") TimelineItemMenu(item: item, actions: actions) .environmentObject(viewModel.context) .environment(\._accessibilityShowButtonShapes, true) - .previewDisplayName("With button shapes on") + .previewDisplayName("Button shapes") TimelineItemMenu(item: backupItem, actions: actions) .environmentObject(viewModel.context) - .previewDisplayName("Authenticity not guaranteed") + .previewDisplayName("Authenticity") TimelineItemMenu(item: unsignedItem, actions: actions) .environmentObject(viewModel.context) @@ -209,9 +263,22 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { TimelineItemMenu(item: unencryptedItem, actions: actions) .environmentObject(viewModel.context) .previewDisplayName("Unencrypted") + + TimelineItemMenu(item: unknownFailureItem, actions: actions) + .environmentObject(viewModel.context) + .previewDisplayName("Unknown failure") + + TimelineItemMenu(item: unsignedDevicesItem, actions: actions) + .environmentObject(viewModel.context) + .previewDisplayName("Unsigned Devices") + + TimelineItemMenu(item: identityChangedItem, actions: actions) + .environmentObject(viewModel.context) + .previewDisplayName("Identity Changed") } - static func makeItem(authenticity: EncryptionAuthenticity? = nil) -> (TextRoomTimelineItem, TimelineItemMenuActions)! { + static func makeItem(authenticity: EncryptionAuthenticity? = nil, + deliveryStatus: TimelineItemDeliveryStatus? = nil) -> (TextRoomTimelineItem, TimelineItemMenuActions)! { guard var item = RoomTimelineItemFixtures.singleMessageChunk.first as? TextRoomTimelineItem, let actions = TimelineItemMenuActions(isReactable: true, actions: [.copy, .edit, .reply(isThread: false), .pin, .redact], @@ -223,6 +290,10 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { item.properties.encryptionAuthenticity = authenticity } + if let deliveryStatus { + item.properties.deliveryStatus = deliveryStatus + } + return (item, actions) } } diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 2102922bdf..e23cd573a1 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -20,7 +20,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { private var innerPinnedEventsTimelineTask: Task? var pinnedEventsTimeline: TimelineProxyProtocol? { get async { - // Check if is alrrady available. + // Check if is already available. if let innerPinnedEventsTimeline { return innerPinnedEventsTimeline // Otherwise check if there is already a task loading it, and wait for it. @@ -35,7 +35,10 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } do { - let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), kind: .pinned) + let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, + maxEventsToLoad: 100, + maxConcurrentRequests: 10), + kind: .pinned) await timeline.subscribeForUpdates() innerPinnedEventsTimeline = timeline return timeline @@ -377,6 +380,51 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } } + func resend(itemID: TimelineItemIdentifier) async -> Result { + guard let transactionID = itemID.transactionID else { + MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)") + return .failure(.missingTransactionID) + } + + do { + try await room.tryResend(transactionId: transactionID) + return .success(()) + } catch { + MXLog.error("Failed resending \(transactionID) with error: \(error)") + return .failure(.sdkError(error)) + } + } + + func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result { + guard let transactionID = itemID.transactionID else { + MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)") + return .failure(.missingTransactionID) + } + + do { + try await room.ignoreDeviceTrustAndResend(devices: devices, transactionId: transactionID) + return .success(()) + } catch { + MXLog.error("Failed trusting devices \(devices) and resending \(transactionID) with error: \(error)") + return .failure(.sdkError(error)) + } + } + + func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result { + guard let transactionID = itemID.transactionID else { + MXLog.error("Attempting to resend an item that has no transaction ID: \(itemID)") + return .failure(.missingTransactionID) + } + + do { + try await room.withdrawVerificationAndResend(userIds: userIDs, transactionId: transactionID) + return .success(()) + } catch { + MXLog.error("Failed withdrawing verification of \(userIDs) and resending \(transactionID) with error: \(error)") + return .failure(.sdkError(error)) + } + } + // MARK: - Room flags func flagAsUnread(_ isUnread: Bool) async -> Result { diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 92dae68b17..d2d99406cb 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -15,6 +15,7 @@ enum RoomProxyError: Error { case invalidURL case invalidMedia case eventNotFound + case missingTransactionID } enum RoomProxyType { @@ -111,6 +112,12 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { /// https://spec.matrix.org/v1.9/client-server-api/#typing-notifications @discardableResult func sendTypingNotification(isTyping: Bool) async -> Result + func resend(itemID: TimelineItemIdentifier) async -> Result + + func ignoreDeviceTrustAndResend(devices: [String: [String]], itemID: TimelineItemIdentifier) async -> Result + + func withdrawVerificationAndResend(userIDs: [String], itemID: TimelineItemIdentifier) async -> Result + // MARK: - Room Flags func flagAsUnread(_ isUnread: Bool) async -> Result diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 5dd21663d0..6a94a3614b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -59,13 +59,7 @@ enum TimelineItemProxy { enum TimelineItemDeliveryStatus: Hashable { case sending case sent - case sendingFailed(SendFailureReason) - - enum SendFailureReason: Hashable { - case verifiedUserHasUnsignedDevice(devices: [String: [String]]) - case verifiedUserChangedIdentity(users: [String]) - case unknown - } + case sendingFailed(TimelineItemSendFailure) var isSendingFailed: Bool { switch self { @@ -75,6 +69,24 @@ enum TimelineItemDeliveryStatus: Hashable { } } +/// The reason a timeline item failed to send. +enum TimelineItemSendFailure: Hashable { + enum VerifiedUser: Hashable { + case hasUnsignedDevice(devices: [String: [String]]) + case changedIdentity(users: [String]) + + var affectedUserIDs: [String] { + switch self { + case .hasUnsignedDevice(let devices): Array(devices.keys) + case .changedIdentity(let users): users + } + } + } + + case verifiedUser(VerifiedUser) + case unknown +} + /// A light wrapper around event timeline items returned from Rust. class EventTimelineItemProxy { let item: MatrixRustSDK.EventTimelineItem @@ -98,9 +110,9 @@ class EventTimelineItemProxy { case .sent: return .sent case .verifiedUserHasUnsignedDevice(devices: let devices): - return .sendingFailed(.verifiedUserHasUnsignedDevice(devices: devices)) + return .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: devices))) case .verifiedUserChangedIdentity(users: let users): - return .sendingFailed(.verifiedUserChangedIdentity(users: users)) + return .sendingFailed(.verifiedUser(.changedIdentity(users: users))) case .crossSigningNotSetup, .sendingFromUnverifiedDevice: return .sendingFailed(.unknown) } diff --git a/PreviewTests/Sources/PreviewTests.swift b/PreviewTests/Sources/PreviewTests.swift index 3d6263183a..f550c02341 100644 --- a/PreviewTests/Sources/PreviewTests.swift +++ b/PreviewTests/Sources/PreviewTests.swift @@ -531,6 +531,12 @@ class PreviewTests: XCTestCase { } } + func test_resolveVerifiedUserSendFailureScreen() { + for preview in ResolveVerifiedUserSendFailureScreen_Previews._allPreviews { + assertSnapshots(matching: preview) + } + } + func test_roomAttachmentPicker() { for preview in RoomAttachmentPicker_Previews._allPreviews { assertSnapshots(matching: preview) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-en-GB.1.png index 07973ed32a..8dca987f82 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5aff3c2bdc10d469661d8d156619b7a638aa5db4bff2525ea1a7091753e13194 -size 75111 +oid sha256:fc0197e073dc65810abea718dc3e01c0828268d1f66a8b28b907d8ce19fcd836 +size 86873 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-pseudo.1.png index 07973ed32a..8dca987f82 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPad-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5aff3c2bdc10d469661d8d156619b7a638aa5db4bff2525ea1a7091753e13194 -size 75111 +oid sha256:fc0197e073dc65810abea718dc3e01c0828268d1f66a8b28b907d8ce19fcd836 +size 86873 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-en-GB.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-en-GB.1.png index 9e910808da..61a05ca7ab 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-en-GB.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-en-GB.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b44ce499b27688bbf5dee93b8be232bc1956f764b6056bb9d018f4642ba802b9 -size 33871 +oid sha256:768e676b260ee17b868e77ee968df0017c61df374095a9504b12518b6eb511df +size 45549 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-pseudo.1.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-pseudo.1.png index 9e910808da..61a05ca7ab 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-pseudo.1.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_heroImage-iPhone-15-pseudo.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b44ce499b27688bbf5dee93b8be232bc1956f764b6056bb9d018f4642ba802b9 -size 33871 +oid sha256:768e676b260ee17b868e77ee968df0017c61df374095a9504b12518b6eb511df +size 45549 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Identity-Changed.png new file mode 100644 index 0000000000..2063294306 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e4f2a9579f5e64da486ac22e56f1cea10f3450687641900790408f9f3004311 +size 121955 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png new file mode 100644 index 0000000000..ff34cc5399 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-en-GB.Unsigned-Device.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4555eb5aab386a6dc73622b063a871f3d2c103e9078d14e0f65ec8d072d6cd0a +size 124524 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Identity-Changed.png new file mode 100644 index 0000000000..343d5fa491 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec75d1ba7375877c82ea1b71947607558762aa00ae4ca575bcc81a8434e9b76b +size 155316 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png new file mode 100644 index 0000000000..938f9e81e4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPad-pseudo.Unsigned-Device.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:197d010abc66e31f74620898772fccdbe9b15e77ca72c764be65fca3e96ef5bd +size 160426 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Identity-Changed.png new file mode 100644 index 0000000000..3108ccc31f --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ca3e0c570345f63fe2d0e915b9cb1c68faa5758acf87901b21b674a9838b380 +size 85398 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png new file mode 100644 index 0000000000..df5833dbc0 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-en-GB.Unsigned-Device.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6651a6be9527f47ede782f8a7f3a0c630884b4d2f14c34d67e3a7262bb39908 +size 89346 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Identity-Changed.png new file mode 100644 index 0000000000..947b01f288 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb7f1eb81810906391eb771ffcec9f37357d2f340677e151edbe1b7977477137 +size 128891 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png new file mode 100644 index 0000000000..c6d6dad5a3 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_resolveVerifiedUserSendFailureScreen-iPhone-15-pseudo.Unsigned-Device.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90f15401000e28a427e45f5a3d2e4abaa6dd6074bdd541a04ad1ec5fc062b891 +size 134865 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity-not-guaranteed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity-not-guaranteed.png deleted file mode 100644 index 91ab0bee36..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity-not-guaranteed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a814e87ba746f5d1590bc620afae3d473eab3497875da9f56fa1f5aceb6b593 -size 139034 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity.png new file mode 100644 index 0000000000..e7ecf795e9 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Authenticity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03297b75fc2e171ca2308ef9f7282ed16de2325c6a3a150149e7bb2a7ce91cd7 +size 139166 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Button-shapes.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Button-shapes.png new file mode 100644 index 0000000000..2f53b18a29 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Button-shapes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4c791d4456c8599bb42d53ec3ff5f20fd07f38fec94a22befe7366e63d0fb6c +size 146941 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Identity-Changed.png new file mode 100644 index 0000000000..1dd1c9391c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b6a081183c1dfddbe45ac2916e94a67487279dff50f9d6145a34c9d2b68de06c +size 137752 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Normal.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Normal.png new file mode 100644 index 0000000000..8d37ae1712 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:175ac6aeee353f9449221ee3a577b9ff68d4091246995a53a5fdbc6eb4a84f0c +size 128129 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unencrypted.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unencrypted.png index 4d52e876c5..b5a7a51e88 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unencrypted.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unencrypted.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:856395860c1c673ada6ecd4bc1e279fe38d37d6038c4e7cffe6aa885433960d7 -size 132971 +oid sha256:dce83b00452e17c6092bbf333dc5bb5538c5b2860eb438086ee2006820455115 +size 132996 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unknown-failure.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unknown-failure.png new file mode 100644 index 0000000000..8d37ae1712 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unknown-failure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:175ac6aeee353f9449221ee3a577b9ff68d4091246995a53a5fdbc6eb4a84f0c +size 128129 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png new file mode 100644 index 0000000000..bccb726820 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned-Devices.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:268ca2367cd6fae133b5aa193bac1516a4a5ed56268bb5b061453bd607501398 +size 138558 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned.png index f8699c61c0..35e5f931cc 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.Unsigned.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16b0210687ba4e03042cdf79b50bc06e2a866699561228007a6758f03a02a79f -size 136665 +oid sha256:1ace2dc3b0c346443590b893332d8ff36689490c2a16e3a69f1514f04b20205c +size 136768 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-off.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-off.png deleted file mode 100644 index 32b2a52828..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-off.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b9ef697532f30ecb4ad0c1ae42cec52c495999da9e2ff238e486be621626692 -size 128275 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-on.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-on.png deleted file mode 100644 index 7a822bdd67..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-en-GB.With-button-shapes-on.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b98860688a9a13f02ed226c6db06b208381c0938242b923b24669826ea30ad5c -size 146954 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity-not-guaranteed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity-not-guaranteed.png deleted file mode 100644 index 736724f173..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity-not-guaranteed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dc5eb6e01de2c2e7b57683ffa0881a300b073c1b0517d96dbc477689e2e4f2e7 -size 146246 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity.png new file mode 100644 index 0000000000..a1af885e14 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Authenticity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2a3123d25db61ba232747dd1d7eb5e20cc0395923b63d7368d19ef273ae2165 +size 146218 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Button-shapes.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Button-shapes.png new file mode 100644 index 0000000000..d2ba529812 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Button-shapes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5ba0459b444324a63dd0197bfc8bc6c7624ca56b14e09aa39ea402cbb84bc03 +size 148850 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Identity-Changed.png new file mode 100644 index 0000000000..6ce957b036 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:665d19ddbfdb19d79fc3afdb131bbdc2610ab92950f99ef5a71a851407de7540 +size 145728 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Normal.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Normal.png new file mode 100644 index 0000000000..788fc4eaf4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93502300833314fc2cbbc60c2697eba8ad6a97fe65eb16b388b0c8242fe4cf96 +size 129926 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unencrypted.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unencrypted.png index 3ef3468980..48b79dfdcb 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unencrypted.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unencrypted.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4b4d87745ede48f63c635a517b0b53333248093a12b007f9cb63103eec708f6 -size 137003 +oid sha256:88b615bbd24548c09f51fd44f8d5e84e7a2a3f6719248f570a900336bb49209e +size 137067 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unknown-failure.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unknown-failure.png new file mode 100644 index 0000000000..788fc4eaf4 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unknown-failure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93502300833314fc2cbbc60c2697eba8ad6a97fe65eb16b388b0c8242fe4cf96 +size 129926 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png new file mode 100644 index 0000000000..925b2e73c3 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned-Devices.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25c106563019a4e83c4537f3063b8a18c23bde138ba724dc03d54f289ae8efbb +size 145432 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned.png index d9e0cc4ee1..f9d13af4e5 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.Unsigned.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d12e1350b1e5a6ec8477feb15fb74e3492fcda98e5802d368110b9289f135475 -size 142260 +oid sha256:e0bb5d00d02fc9a0174a49ece0bf0e9c5944eee89887813cffede3ad79327c3f +size 142433 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-off.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-off.png deleted file mode 100644 index 2daa634868..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-off.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5438bd086e097841f104bd6d6b43b308a2116a8be81253fbef9666267403c26f -size 130069 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-on.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-on.png deleted file mode 100644 index 743de05282..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPad-pseudo.With-button-shapes-on.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d39ea3ad6fe5325875e6449b08c6d5f4c8a56ded6d8b52a2a71aba606af01302 -size 148904 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity-not-guaranteed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity-not-guaranteed.png deleted file mode 100644 index 455ec6afbb..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity-not-guaranteed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:32ef4ab70c1c611b8022a85855cbc3f0483aeb8b794bc31403dbdeeb417dca3f -size 91903 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity.png new file mode 100644 index 0000000000..fc4d44db59 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Authenticity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3012f9c2833a903b90e0ab8e3976ccb15d5e9b99629b449c15416aec678422b4 +size 91831 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Button-shapes.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Button-shapes.png new file mode 100644 index 0000000000..f966921279 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Button-shapes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:087068712179f7fedcf0fe1b984ece43378b6528312873b363b128aae9785e68 +size 91908 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Identity-Changed.png new file mode 100644 index 0000000000..6c264bfb7d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8053e7b49f3371b57eee1fb2bbf612e83b70c199f97b82b7610823c98a11fc95 +size 90802 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Normal.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Normal.png new file mode 100644 index 0000000000..305fa6ffaf --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ff69118ef7182e5d9cdd78531a394dd9344bc34fc6dd090c9595308568bdd8c +size 81637 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unencrypted.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unencrypted.png index 1fa9b5b079..464df77eba 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unencrypted.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unencrypted.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c14641b3b0bc6d14fefe09eb2e94c273beadd4fb638fd91e13839cb45291dc7 -size 84586 +oid sha256:ebd53df8d28fa0c1b48a9b23b04153f8f9f53b67de9c771eed6dee9a902b72e8 +size 84476 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unknown-failure.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unknown-failure.png new file mode 100644 index 0000000000..305fa6ffaf --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unknown-failure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ff69118ef7182e5d9cdd78531a394dd9344bc34fc6dd090c9595308568bdd8c +size 81637 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png new file mode 100644 index 0000000000..3da17ab0c3 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned-Devices.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74260e444cf6dffe7302066e81901b4e235d967a71586528e1a5a74691487f24 +size 91442 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned.png index 2aaedd1c47..9944bab470 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.Unsigned.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:616fc70da47bf61a23cfcaf22928139913dbddab00769dcd2bb3da01a7afc30a -size 87485 +oid sha256:268ee1dd299033cf76cf8fe1ffcb0087b5180d4b15b57b189a221f2bd3d2d289 +size 87437 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-off.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-off.png deleted file mode 100644 index d788ffea9b..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-off.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:babe7ac92847207fe9c42db76a25c443fc056d0cb5236bb74123df1eb1c75fc7 -size 81558 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-on.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-on.png deleted file mode 100644 index 71e6dfbab2..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-en-GB.With-button-shapes-on.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b92834dc9f0ea920743fd9336de180eafdc87071209a27e8bd880d21e8e6a39f -size 91943 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity-not-guaranteed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity-not-guaranteed.png deleted file mode 100644 index 89961f7daf..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity-not-guaranteed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:011c4ce8721d2d12fd159b9451e06f03419543ad6744c7c2ec2f977f46803687 -size 107558 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity.png new file mode 100644 index 0000000000..bf4af041b8 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Authenticity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57db8ed1a18fa300f813e3796a31449a0f295d2845dd14bd87f339ea82c8cacb +size 107529 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Button-shapes.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Button-shapes.png new file mode 100644 index 0000000000..76533f78ed --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Button-shapes.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c828ff373382a4d6c9f7d2d9577c4578f0c874e5fb08f8886bd8d48caee286c +size 97553 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Identity-Changed.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Identity-Changed.png new file mode 100644 index 0000000000..691ef62342 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Identity-Changed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ba91ee1295d59e6f17b0178659423f0c628e6189535e73226607589c0d5eee4 +size 105819 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Normal.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Normal.png new file mode 100644 index 0000000000..f774b0d342 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Normal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b00ba9ac752baea6f31925bb23a5db5834402e8daba7bb072dc15ae6e085945d +size 87286 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unencrypted.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unencrypted.png index 844b5fcbde..46be2b70a8 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unencrypted.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unencrypted.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f77a276e429a2d601cd474e547e3a0923b8b432121b1bb7e12a1eaa593d0d4b6 -size 92034 +oid sha256:7e2917406331c37dc9d1e369fb034d29223d5af5dc9f7069ca9ac82827cf4b9a +size 91918 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unknown-failure.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unknown-failure.png new file mode 100644 index 0000000000..f774b0d342 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unknown-failure.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b00ba9ac752baea6f31925bb23a5db5834402e8daba7bb072dc15ae6e085945d +size 87286 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png new file mode 100644 index 0000000000..852eaf1d3a --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned-Devices.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a212b2a93a757524ffc9085ac15e0c115add5b20f353507632d6eb7c794701b0 +size 105467 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned.png index 80b26b3d45..774a444f6b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.Unsigned.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d3cddc2a7fc71a390be380caf341c638ec43950cb7196ecda6ebd58069de750 -size 99561 +oid sha256:c4a3151ffb3f555fd99747cf211236f01be15c5f13373c684021e9ee7a4bd788 +size 99441 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-off.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-off.png deleted file mode 100644 index 61c99b66fa..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-off.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8bdb7a577de87f2e99570feda71222da953c4321e3a681c62d0d80b6079cdf81 -size 87175 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-on.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-on.png deleted file mode 100644 index 01685ac153..0000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/test_timelineItemMenu-iPhone-15-pseudo.With-button-shapes-on.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d2ba9cfa065c63a2e62df7ed84bf4838cb01b29124b70a6c9a220d8c037cc50 -size 97569 diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift new file mode 100644 index 0000000000..cc3186c9f0 --- /dev/null +++ b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift @@ -0,0 +1,121 @@ +// +// 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 XCTest + +@testable import ElementX + +@MainActor +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" + viewModel = makeViewModel(with: .hasUnsignedDevice(devices: [userID: ["DEVICE1"]])) + + try await verifyResolving(userIDs: [userID]) + } + + func testMultipleUnsignedDevices() async throws { + // 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"]) }) + viewModel = makeViewModel(with: .hasUnsignedDevice(devices: devices)) + + try await verifyResolving(userIDs: userIDs, assertStrings: false) + } + + func testChangedIdentity() async throws { + // Given a failure where a single user's identity has changed. + let userID = "@alice:matrix.org" + viewModel = makeViewModel(with: .changedIdentity(users: [userID])) + + try await verifyResolving(userIDs: [userID]) + } + + 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"] + viewModel = makeViewModel(with: .changedIdentity(users: userIDs)) + + try await verifyResolving(userIDs: userIDs) + } + + // MARK: Helpers + + 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 { + var remainingUserIDs = userIDs + + while remainingUserIDs.count > 1 { + // Verify that the strings are being updated. + if assertStrings { + verifyDisplayName(from: remainingUserIDs) + } + + // When resolving the first failure. + let deferredFailure = deferFailure(viewModel.actionsPublisher, timeout: 1) { $0.isDismiss } + context.send(viewAction: .resolveAndResend) + + // Then the sheet should remain open for the next failure. + try await deferredFailure.fulfill() + + remainingUserIDs.removeFirst() + } + + // Verify the final string. + if assertStrings { + verifyDisplayName(from: remainingUserIDs) + } + + // When resolving the final failure. + let deferred = deferFulfillment(viewModel.actionsPublisher) { $0.isDismiss } + context.send(viewAction: .resolveAndResend) + + // Then the sheet should be dismissed. + try await deferred.fulfill() + } + + private func verifyDisplayName(from remainingUserIDs: [String]) { + guard let userID = remainingUserIDs.first else { + XCTFail("There should be a user ID to check.") + return + } + + guard let displayName = roomProxy.membersPublisher.value.first(where: { $0.userID == userID })?.displayName else { + XCTFail("There should be a matching mock user") + return + } + + 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 + } + } +} diff --git a/project.yml b/project.yml index 09ae22e669..7b858fe846 100644 --- a/project.yml +++ b/project.yml @@ -60,7 +60,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.44 + exactVersion: 1.0.45 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios