diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index f9c8df321..c81f294d1 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -948,6 +948,7 @@ B6DA66EFC13A90846B625836 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 91DE43B8815918E590912DDA /* InfoPlist.strings */; }; B6DF6B6FA8734B70F9BF261E /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */; }; B6EC2148FA5443C9289BEEBA /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; + B73E50AF1AB2EB5477E20710 /* RoomDetailsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 166D45E1861A73B232109843 /* RoomDetailsScreenViewModelTests.swift */; }; B773ACD8881DB18E876D950C /* WaveformSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94028A227645FA880B966211 /* WaveformSource.swift */; }; B7888FC1E1DEF816D175C8D6 /* SecureBackupKeyBackupScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD72A9B720D75DBE60AC299F /* SecureBackupKeyBackupScreenModels.swift */; }; B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B5EDCD05D50BA9B815C66C /* ImageRoomTimelineItemContent.swift */; }; @@ -976,6 +977,7 @@ BB9B800C6094E34860E89DC5 /* AppLockSetupBiometricsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */; }; BC1222EDFF0C240F14259315 /* BloomModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D152423EE6CF0ECCC84091A /* BloomModifier.swift */; }; BC7CA1379D7C24F47B1B8B7E /* PaginationIndicatorRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */; }; + BCA5E2157CE27AB6F1D043D3 /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 5A8EF1A5F9629FCA309D4B2A /* AsyncAlgorithms */; }; BCC864190651B3A3CF51E4DF /* MediaFileHandleProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */; }; BD0BE20DBCE31253AE4490A1 /* RoomListFiltersEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1DDB2293A51EA4C2739351 /* RoomListFiltersEmptyStateView.swift */; }; BD6685592716CA957D7BAAC4 /* RoomChangeRolesScreenSelectedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9B45D584D232CB9E5C7734 /* RoomChangeRolesScreenSelectedItem.swift */; }; @@ -1092,7 +1094,6 @@ D46C33F8B61B55F0C8C2D15F /* LocationRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B2AC540DE619B36832A5DB5 /* LocationRoomTimelineItem.swift */; }; D4CB979EB4FE26AAD9F9A72B /* UserProfileScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 604A69C081B935D6A38DE6D8 /* UserProfileScreenModels.swift */; }; D4D7CCECC6C0AAFC42E165BB /* NotificationPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9BBB18FB27F09032AD8769 /* NotificationPermissionsScreenViewModel.swift */; }; - D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */; }; D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */; }; D5681C80D8281560AACE0035 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = 045253F9967A535EE5B16691 /* Label.swift */; }; D5B1531A72387D432939D4E0 /* RoomDirectorySearchProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */; }; @@ -1190,7 +1191,6 @@ EA6613B29BA671F39CE1B1D2 /* ConfirmationDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */; }; EA78A7512AFB1E5451744EB1 /* AppRouteURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */; }; EA8D941771E762A5D3D7FA0D /* FileMediaEventsTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */; }; - EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */; }; EAB3C1F0BC7F671ED8BDF82D /* CompletionSuggestionServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECF11669EF253E98AA2977A /* CompletionSuggestionServiceProtocol.swift */; }; EAC6FE2CD4F50A43068ADCD8 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; }; EAF2B3E6C6AEC4AD3A8BD454 /* RoomMemberDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A87D0471D438A233C2CF4A /* RoomMemberDetailsScreenViewModel.swift */; }; @@ -1198,6 +1198,7 @@ EB88DBD77221E2CFE463018C /* NSE.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0D8F620C8B314840D8602E3F /* NSE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; EB9F4688006B52E69DF5358F /* BlankFormCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */; }; EBE13FAB4E29738AC41BD3E5 /* InfoPlistReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A580295A56B55A856CC4084 /* InfoPlistReader.swift */; }; + EC09E502A21E4EAA8B367AB8 /* ReportContentScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30EA681527A8FE65E4C8E9A9 /* ReportContentScreenViewModelTests.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 */; }; @@ -1430,7 +1431,6 @@ 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectoriesTests.swift; sourceTree = ""; }; 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = ""; }; 0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsFlowCoordinator.swift; sourceTree = ""; }; - 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentViewModelTests.swift; sourceTree = ""; }; 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = ""; }; 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = ""; }; 0A459AE4B6566B2FA99E86B2 /* TimelineItemBubbledStylerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemBubbledStylerView.swift; sourceTree = ""; }; @@ -1497,6 +1497,7 @@ 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = ""; }; 161CD412E75F4086F422AE39 /* SessionVerificationScreenStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenStateMachine.swift; sourceTree = ""; }; 1627F2D56477BD331F6D732C /* RoomHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomHeaderView.swift; sourceTree = ""; }; + 166D45E1861A73B232109843 /* RoomDetailsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelTests.swift; sourceTree = ""; }; 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = ""; }; 16D353E10A64172D863769BF /* TombstonedAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TombstonedAvatarImage.swift; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; @@ -1630,7 +1631,6 @@ 2DB0E533508094156D8024C3 /* TimelineStyler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyler.swift; sourceTree = ""; }; 2E11E7C396ED06A154CF6DF3 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/SAS.strings; sourceTree = ""; }; 2E88534A39781D76487D59DF /* SecureBackupKeyBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenViewModelTests.swift; sourceTree = ""; }; - 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelTests.swift; sourceTree = ""; }; 2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = ""; }; 2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = ""; }; 2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventContent.swift; sourceTree = ""; }; @@ -1640,6 +1640,7 @@ 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreen.swift; sourceTree = ""; }; 30856520F3263D0E195710D7 /* TimelineMediaPreviewFileExportPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewFileExportPicker.swift; sourceTree = ""; }; 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = ""; }; + 30EA681527A8FE65E4C8E9A9 /* ReportContentScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenViewModelTests.swift; sourceTree = ""; }; 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = ""; }; 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContextTests.swift; sourceTree = ""; }; @@ -2668,6 +2669,7 @@ buildActionMask = 2147483647; files = ( 7FF27DA70D833CFC5724EFC5 /* MatrixRustSDK in Frameworks */, + BCA5E2157CE27AB6F1D043D3 /* AsyncAlgorithms in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4363,14 +4365,14 @@ 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */, 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */, - 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, + 30EA681527A8FE65E4C8E9A9 /* ReportContentScreenViewModelTests.swift */, 0C3E9684DCE6B66BD0B5DF67 /* ReportRoomScreenViewModelTests.swift */, 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */, A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */, 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */, 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */, 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */, - 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */, + 166D45E1861A73B232109843 /* RoomDetailsScreenViewModelTests.swift */, EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */, 6AE5800184E93CD5E02C6543 /* RoomEventStringBuilderTests.swift */, 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */, @@ -6258,6 +6260,7 @@ name = UnitTests; packageProductDependencies = ( C07EA60CAB296D7726210F5B /* MatrixRustSDK */, + 5A8EF1A5F9629FCA309D4B2A /* AsyncAlgorithms */, ); productName = UnitTests; productReference = AAC9344689121887B74877AF /* UnitTests.xctest */; @@ -6479,6 +6482,7 @@ packageReferences = ( E025F19D013D9BA6C58B37F4 /* XCRemoteSwiftPackageReference "swift-algorithms" */, AC3475112CA40C2C6E78D1EB /* XCRemoteSwiftPackageReference "matrix-analytics-events" */, + 4A8D3ABF18EABB8066BBD46E /* XCRemoteSwiftPackageReference "swift-async-algorithms" */, F76A08D0EA29A07A54F4EB4D /* XCRemoteSwiftPackageReference "swift-collections" */, F71C70A4404CC6D9C4AF35F2 /* XCRemoteSwiftPackageReference "compound-ios" */, 4C34425923978C97409A3EF2 /* XCRemoteSwiftPackageReference "DSWaveformImage" */, @@ -6887,14 +6891,14 @@ FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */, E3EBC3BF7CE3960B41757BAA /* Publisher.swift in Sources */, BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */, - D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, + EC09E502A21E4EAA8B367AB8 /* ReportContentScreenViewModelTests.swift in Sources */, 513AF15E0E84711B80D04B1B /* ReportRoomScreenViewModelTests.swift in Sources */, 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */, C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */, 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */, D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */, 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */, - EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */, + B73E50AF1AB2EB5477E20710 /* RoomDetailsScreenViewModelTests.swift in Sources */, 5B7D24A318AFF75AD611A026 /* RoomDirectorySearchScreenScreenViewModelTests.swift in Sources */, E591742E509A2A009BF25F9D /* RoomEventStringBuilderTests.swift in Sources */, 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */, @@ -8718,6 +8722,14 @@ minimumVersion = 1.2.0; }; }; + 4A8D3ABF18EABB8066BBD46E /* XCRemoteSwiftPackageReference "swift-async-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-async-algorithms"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.0.0; + }; + }; 4BDA7F6042968E8422470F3F /* XCRemoteSwiftPackageReference "LoremSwiftum" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/lukaskubanek/LoremSwiftum"; @@ -9032,6 +9044,11 @@ package = C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */; productName = DTCoreText; }; + 5A8EF1A5F9629FCA309D4B2A /* AsyncAlgorithms */ = { + isa = XCSwiftPackageProductDependency; + package = 4A8D3ABF18EABB8066BBD46E /* XCRemoteSwiftPackageReference "swift-async-algorithms" */; + productName = AsyncAlgorithms; + }; 6647C55D93508C7CE9D954A5 /* MatrixRustSDK */ = { isa = XCSwiftPackageProductDependency; package = 6FC4820D8D4559CEECA064D7 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */; diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bf1dfdaa4..1c5defa64 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "cb4b4c28930e04929217c9570b7f82a1dc8e3dc51e3f956f11584dbae9bac079", + "originHash" : "f573b0754209479f46b77206f67033b0073bb2df38ede53b3289703615c5a48f", "pins" : [ { "identity" : "compound-design-tokens", @@ -216,6 +216,15 @@ "version" : "1.2.1" } }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms", + "state" : { + "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", + "version" : "1.0.4" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", diff --git a/ElementX/Sources/Other/Extensions/XCTestCase.swift b/ElementX/Sources/Other/Extensions/XCTestCase.swift index a822a8bd5..fd330f12d 100644 --- a/ElementX/Sources/Other/Extensions/XCTestCase.swift +++ b/ElementX/Sources/Other/Extensions/XCTestCase.swift @@ -158,6 +158,36 @@ extension XCTestCase { } } + /// XCTest utility that assists in subscribing to an async stream and deferring the failure for a particular value until some other actions have been performed. + /// - Parameters: + /// - asyncStream: The stream to wait on. + /// - timeout: A timeout after which we give up. + /// - message: An optional custom expectation message + /// - until: callback that evaluates outputs until some condition is reached + /// - Returns: The deferred fulfilment to be executed after some actions. The stream's result is not returned from this fulfilment. + func deferFailure(_ asyncStream: AsyncStream, + timeout: TimeInterval, + message: String? = nil, + until condition: @escaping (Value) -> Bool) -> DeferredFulfillment { + let expectation = expectation(description: message ?? "Awaiting stream") + expectation.isInverted = true + var hasFulfilled = false + + let task = Task { + for await value in asyncStream { + if condition(value), !hasFulfilled { + expectation.fulfill() + hasFulfilled = true + } + } + } + + return DeferredFulfillment { + await self.fulfillment(of: [expectation], timeout: timeout) + task.cancel() + } + } + struct DeferredFulfillment { let closure: () async throws -> T @discardableResult func fulfill() async throws -> T { diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift index 2207fd375..aea18216d 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias BlockedUsersScreenViewModelType = StateStoreViewModel +typealias BlockedUsersScreenViewModelType = StateStoreViewModelV2 class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsersScreenViewModelProtocol { let hideProfiles: Bool diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift index fec94a2dd..4ee0c476e 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct BlockedUsersScreen: View { - @ObservedObject var context: BlockedUsersScreenViewModel.Context + @Bindable var context: BlockedUsersScreenViewModel.Context var body: some View { content diff --git a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift index a8734ab67..1284804f7 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias PollFormScreenViewModelType = StateStoreViewModel +typealias PollFormScreenViewModelType = StateStoreViewModelV2 class PollFormScreenViewModel: PollFormScreenViewModelType, PollFormScreenViewModelProtocol { private var actionsSubject: PassthroughSubject = .init() diff --git a/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift index 2056d39b2..98a22672c 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/View/PollFormScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct PollFormScreen: View { - @ObservedObject var context: PollFormScreenViewModel.Context + @Bindable var context: PollFormScreenViewModel.Context @FocusState var focus: Focus? enum Focus: Hashable { diff --git a/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift b/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift index 63f2e2aca..fe9024733 100644 --- a/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift +++ b/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias DeclineAndBlockScreenViewModelType = StateStoreViewModel +typealias DeclineAndBlockScreenViewModelType = StateStoreViewModelV2 class DeclineAndBlockScreenViewModel: DeclineAndBlockScreenViewModelType, DeclineAndBlockScreenViewModelProtocol { let userID: String diff --git a/ElementX/Sources/Screens/DeclineAndBlockScreen/View/DeclineAndBlockScreen.swift b/ElementX/Sources/Screens/DeclineAndBlockScreen/View/DeclineAndBlockScreen.swift index d368ac3a5..8a5381834 100644 --- a/ElementX/Sources/Screens/DeclineAndBlockScreen/View/DeclineAndBlockScreen.swift +++ b/ElementX/Sources/Screens/DeclineAndBlockScreen/View/DeclineAndBlockScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct DeclineAndBlockScreen: View { - @ObservedObject var context: DeclineAndBlockScreenViewModel.Context + @Bindable var context: DeclineAndBlockScreenViewModel.Context var body: some View { Form { diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index 378b6b352..8a930ba2f 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias EmojiPickerScreenViewModelType = StateStoreViewModel +typealias EmojiPickerScreenViewModelType = StateStoreViewModelV2 class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScreenViewModelProtocol { private var actionsSubject: PassthroughSubject = .init() diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift index 96bb1d5b3..9492c3c30 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/View/EmojiPickerScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct EmojiPickerScreen: View { - @ObservedObject var context: EmojiPickerScreenViewModel.Context + let context: EmojiPickerScreenViewModel.Context var selectedEmojis = Set() @State var searchString = "" @@ -106,9 +106,7 @@ struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview { static var previews: some View { EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"]) .previewDisplayName("Screen") - .snapshotPreferences(expect: viewModel.context.$viewState.map { state in - !state.categories.isEmpty - }) + .snapshotPreferences(expect: viewModel.context.observe(\.viewState.categories).map { !$0.isEmpty }.eraseToStream()) } } diff --git a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift index dc9b96d96..fab413323 100644 --- a/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/StaticLocationScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import Foundation -typealias StaticLocationScreenViewModelType = StateStoreViewModel +typealias StaticLocationScreenViewModelType = StateStoreViewModelV2 class StaticLocationScreenViewModel: StaticLocationScreenViewModelType, StaticLocationScreenViewModelProtocol { private let actionsSubject: PassthroughSubject = .init() diff --git a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift index 3fb4373e5..3c94e55e4 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/StaticLocationScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct StaticLocationScreen: View { - @ObservedObject var context: StaticLocationScreenViewModel.Context + @Bindable var context: StaticLocationScreenViewModel.Context var body: some View { VStack(spacing: 0) { diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift index 3bafa0747..6c6898930 100644 --- a/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/ManageRoomMemberSheetViewModel.swift @@ -9,7 +9,7 @@ import Combine import Foundation import SwiftUI -typealias ManageRoomMemberSheetViewModelType = StateStoreViewModel +typealias ManageRoomMemberSheetViewModelType = StateStoreViewModelV2 class ManageRoomMemberSheetViewModel: ManageRoomMemberSheetViewModelType, ManageRoomMemberSheetViewModelProtocol { private let roomProxy: JoinedRoomProxyProtocol diff --git a/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift b/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift index 7cd366bff..19d69e6b4 100644 --- a/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift +++ b/ElementX/Sources/Screens/ManageRoomMemberSheet/View/ManageRoomMemberSheetView.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct ManageRoomMemberSheetView: View { - @ObservedObject var context: ManageRoomMemberSheetViewModelType.Context + @Bindable var context: ManageRoomMemberSheetViewModelType.Context var body: some View { Form { diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift index 45323deca..6ba3b163a 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift @@ -9,7 +9,7 @@ import Combine import MatrixRustSDK import SwiftUI -typealias MediaUploadPreviewScreenViewModelType = StateStoreViewModel +typealias MediaUploadPreviewScreenViewModelType = StateStoreViewModelV2 class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, MediaUploadPreviewScreenViewModelProtocol { private let userIndicatorController: UserIndicatorControllerProtocol diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift index 247dec6c3..06413fdb4 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift @@ -13,7 +13,7 @@ import SwiftUI struct MediaUploadPreviewScreen: View { @Environment(\.colorScheme) private var colorScheme - @ObservedObject var context: MediaUploadPreviewScreenViewModel.Context + @Bindable var context: MediaUploadPreviewScreenViewModel.Context @State private var captionWarningFrame: CGRect = .zero @FocusState private var isComposerFocussed diff --git a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift index 30f3c7bb3..c6dfd3a52 100644 --- a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift +++ b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias AnalyticsPromptScreenViewModelType = StateStoreViewModel +typealias AnalyticsPromptScreenViewModelType = StateStoreViewModelV2 class AnalyticsPromptScreenViewModel: AnalyticsPromptScreenViewModelType, AnalyticsPromptScreenViewModelProtocol { private var actionsSubject: PassthroughSubject = .init() diff --git a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift index 8d7a9f1e1..43dc839e9 100644 --- a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift @@ -10,7 +10,7 @@ import SwiftUI /// A prompt that asks the user whether they would like to enable Analytics or not. struct AnalyticsPromptScreen: View { - @ObservedObject var context: AnalyticsPromptScreenViewModel.Context + let context: AnalyticsPromptScreenViewModel.Context var body: some View { FullscreenDialog(topPadding: UIConstants.startScreenBreakerScreenTopPadding, background: .gradient) { diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift index 38d349872..fff15d2d9 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias ReportContentScreenViewModelType = StateStoreViewModel +typealias ReportContentScreenViewModelType = StateStoreViewModelV2 class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportContentScreenViewModelProtocol { private let eventID: String diff --git a/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift b/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift index 86c243247..bc2339625 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct ReportContentScreen: View { - @ObservedObject var context: ReportContentScreenViewModel.Context + @Bindable var context: ReportContentScreenViewModel.Context var body: some View { Form { diff --git a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift index dd680f633..e96c29fba 100644 --- a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias ReportRoomScreenViewModelType = StateStoreViewModel +typealias ReportRoomScreenViewModelType = StateStoreViewModelV2 class ReportRoomScreenViewModel: ReportRoomScreenViewModelType, ReportRoomScreenViewModelProtocol { let roomProxy: JoinedRoomProxyProtocol diff --git a/ElementX/Sources/Screens/ReportRoomScreen/View/ReportRoomScreen.swift b/ElementX/Sources/Screens/ReportRoomScreen/View/ReportRoomScreen.swift index 0010127a6..f614fee50 100644 --- a/ElementX/Sources/Screens/ReportRoomScreen/View/ReportRoomScreen.swift +++ b/ElementX/Sources/Screens/ReportRoomScreen/View/ReportRoomScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct ReportRoomScreen: View { - @ObservedObject var context: ReportRoomScreenViewModel.Context + @Bindable var context: ReportRoomScreenViewModel.Context var body: some View { Form { diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift index 37e75e264..256c14353 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias ResolveVerifiedUserSendFailureScreenViewModelType = StateStoreViewModel +typealias ResolveVerifiedUserSendFailureScreenViewModelType = StateStoreViewModelV2 class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFailureScreenViewModelType, ResolveVerifiedUserSendFailureScreenViewModelProtocol { private let iterator: VerifiedUserSendFailureIterator diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift index b299be5d2..c78c95084 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/View/ResolveVerifiedUserSendFailureScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct ResolveVerifiedUserSendFailureScreen: View { - @ObservedObject var context: ResolveVerifiedUserSendFailureScreenViewModel.Context + let context: ResolveVerifiedUserSendFailureScreenViewModel.Context @State private var sheetFrame: CGRect = .zero var body: some View { diff --git a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift index d5ae5ba31..d5d37a6ca 100644 --- a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenViewModel.swift @@ -9,7 +9,7 @@ import Combine import MatrixRustSDK import SwiftUI -typealias RoomChangePermissionsScreenViewModelType = StateStoreViewModel +typealias RoomChangePermissionsScreenViewModelType = StateStoreViewModelV2 class RoomChangePermissionsScreenViewModel: RoomChangePermissionsScreenViewModelType, RoomChangePermissionsScreenViewModelProtocol { private let roomProxy: JoinedRoomProxyProtocol diff --git a/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift b/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift index a97f38499..4fc9ab299 100644 --- a/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift +++ b/ElementX/Sources/Screens/RoomChangePermissionsScreen/View/RoomChangePermissionsScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct RoomChangePermissionsScreen: View { - @ObservedObject var context: RoomChangePermissionsScreenViewModel.Context + @Bindable var context: RoomChangePermissionsScreenViewModel.Context var body: some View { Form { diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift index 67297a505..3de89a37d 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias RoomChangeRolesScreenViewModelType = StateStoreViewModel +typealias RoomChangeRolesScreenViewModelType = StateStoreViewModelV2 class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomChangeRolesScreenViewModelProtocol { private let roomProxy: JoinedRoomProxyProtocol diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift index 9cf23e449..c48a06838 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct RoomChangeRolesScreen: View { - @ObservedObject var context: RoomChangeRolesScreenViewModel.Context + @Bindable var context: RoomChangeRolesScreenViewModel.Context var showTopSection: Bool { !context.viewState.membersWithRole.isEmpty } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift index d7d841ab0..f49b8adb9 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift @@ -13,7 +13,7 @@ struct RoomChangeRolesScreenSection: View { let title: String var isAdministratorsSection = false - @ObservedObject var context: RoomChangeRolesScreenViewModel.Context + let context: RoomChangeRolesScreenViewModel.Context var body: some View { if !members.isEmpty { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index c30ce7f27..6e7de350a 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias RoomDetailsScreenViewModelType = StateStoreViewModel +typealias RoomDetailsScreenViewModelType = StateStoreViewModelV2 class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol { private let roomProxy: JoinedRoomProxyProtocol diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index b6093d2fe..da031a952 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct RoomDetailsScreen: View { - @ObservedObject var context: RoomDetailsScreenViewModel.Context + @Bindable var context: RoomDetailsScreenViewModel.Context @State private var isTopicExpanded = false @@ -338,33 +338,23 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { static var previews: some View { RoomDetailsScreen(context: genericRoomViewModel.context) - .snapshotPreferences(expect: genericRoomViewModel.context.$viewState.map { state in - state.permalink != nil - }) + .snapshotPreferences(expect: genericRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil }.eraseToStream()) .previewDisplayName("Generic Room") RoomDetailsScreen(context: simpleRoomViewModel.context) - .snapshotPreferences(expect: simpleRoomViewModel.context.$viewState.map { state in - state.permalink != nil - }) + .snapshotPreferences(expect: simpleRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil }.eraseToStream()) .previewDisplayName("Simple Room") RoomDetailsScreen(context: dmRoomViewModel.context) - .snapshotPreferences(expect: dmRoomViewModel.context.$viewState.map { state in - state.accountOwner != nil - }) + .snapshotPreferences(expect: dmRoomViewModel.context.observe(\.viewState.accountOwner).map { $0 != nil }.eraseToStream()) .previewDisplayName("DM Room") RoomDetailsScreen(context: dmRoomVerifiedViewModel.context) - .snapshotPreferences(expect: dmRoomVerifiedViewModel.context.$viewState.map { state in - state.dmRecipientInfo?.verificationState == .verified - }) + .snapshotPreferences(expect: dmRoomVerifiedViewModel.context.observe(\.viewState.dmRecipientInfo?.verificationState).map { $0 == .verified }.eraseToStream()) .previewDisplayName("DM Room Verified") RoomDetailsScreen(context: dmRoomVerificationViolationViewModel.context) - .snapshotPreferences(expect: dmRoomVerificationViolationViewModel.context.$viewState.map { state in - state.accountOwner != nil - }) + .snapshotPreferences(expect: dmRoomVerificationViolationViewModel.context.observe(\.viewState.accountOwner).map { $0 != nil }.eraseToStream()) .previewDisplayName("DM Room Verification Violation") } diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift index 427494c04..0bc9277f2 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift @@ -8,7 +8,7 @@ import Combine import SwiftUI -typealias RoomRolesAndPermissionsScreenViewModelType = StateStoreViewModel +typealias RoomRolesAndPermissionsScreenViewModelType = StateStoreViewModelV2 class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewModelType, RoomRolesAndPermissionsScreenViewModelProtocol { private let roomProxy: JoinedRoomProxyProtocol diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift index fc18d9e38..e4a0cb14a 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct RoomRolesAndPermissionsScreen: View { - @ObservedObject var context: RoomRolesAndPermissionsScreenViewModel.Context + @Bindable var context: RoomRolesAndPermissionsScreenViewModel.Context var body: some View { Form { diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift index 4d97be85b..45f5b6283 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift @@ -9,7 +9,7 @@ import Combine import MatrixRustSDK import SwiftUI -typealias UserProfileScreenViewModelType = StateStoreViewModel +typealias UserProfileScreenViewModelType = StateStoreViewModelV2 class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScreenViewModelProtocol { private let clientProxy: ClientProxyProtocol diff --git a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift index 13cbf8876..1428560db 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift @@ -9,7 +9,7 @@ import Compound import SwiftUI struct UserProfileScreen: View { - @ObservedObject var context: UserProfileScreenViewModel.Context + @Bindable var context: UserProfileScreenViewModel.Context var body: some View { Form { @@ -103,21 +103,15 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview { static var previews: some View { UserProfileScreen(context: verifiedUserViewModel.context) - .snapshotPreferences(expect: verifiedUserViewModel.context.$viewState.map { state in - state.isVerified != nil - }) + .snapshotPreferences(expect: verifiedUserViewModel.context.observe(\.viewState.isVerified).map { $0 != nil }.eraseToStream()) .previewDisplayName("Verified User") UserProfileScreen(context: otherUserViewModel.context) - .snapshotPreferences(expect: otherUserViewModel.context.$viewState.map { state in - state.isVerified != nil - }) + .snapshotPreferences(expect: otherUserViewModel.context.observe(\.viewState.isVerified).map { $0 != nil }.eraseToStream()) .previewDisplayName("Other User") UserProfileScreen(context: accountOwnerViewModel.context) - .snapshotPreferences(expect: accountOwnerViewModel.context.$viewState.map { state in - state.isVerified != nil - }) + .snapshotPreferences(expect: accountOwnerViewModel.context.observe(\.viewState.isVerified).map { $0 != nil }.eraseToStream()) .previewDisplayName("Account Owner") } diff --git a/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist b/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist index 9e8b72c0d..c63eb0384 100644 --- a/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist +++ b/ElementX/SupportingFiles/Settings.bundle/Acknowledgements.plist @@ -170,6 +170,14 @@ Type PSChildPaneSpecifier + + File + Packages/swift-async-algorithms + Title + swift-async-algorithms + Type + PSChildPaneSpecifier + File Packages/swift-collections diff --git a/ElementX/SupportingFiles/Settings.bundle/Packages/swift-async-algorithms.plist b/ElementX/SupportingFiles/Settings.bundle/Packages/swift-async-algorithms.plist new file mode 100644 index 000000000..182faa6e4 --- /dev/null +++ b/ElementX/SupportingFiles/Settings.bundle/Packages/swift-async-algorithms.plist @@ -0,0 +1,226 @@ + + + + + PreferenceSpecifiers + + + FooterText + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. + + Type + PSGroupSpecifier + + + + diff --git a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift index 1ba121aea..5f9afa1c5 100644 --- a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift +++ b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift @@ -20,7 +20,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase { mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController) - let deferred = deferFailure(viewModel.context.$viewState, timeout: 1) { $0.blockedUsers.contains { $0.displayName != nil } } + let deferred = deferFailure(viewModel.context.observe(\.viewState.blockedUsers), timeout: 1) { $0.contains { $0.displayName != nil } } try await deferred.fulfill() XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) @@ -35,7 +35,7 @@ class BlockedUsersScreenViewModelTests: XCTestCase { mediaProvider: MediaProviderMock(configuration: .init()), userIndicatorController: ServiceLocator.shared.userIndicatorController) - let deferred = deferFulfillment(viewModel.context.$viewState) { $0.blockedUsers.contains { $0.displayName != nil } } + let deferred = deferFulfillment(viewModel.context.observe(\.viewState.blockedUsers)) { $0.contains { $0.displayName != nil } } try await deferred.fulfill() XCTAssertFalse(viewModel.context.viewState.blockedUsers.isEmpty) diff --git a/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift index bbb97de47..ee14d978e 100644 --- a/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift +++ b/UnitTests/Sources/ManageRoomMemberSheetViewModelTests.swift @@ -34,7 +34,7 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase { analyticsService: ServiceLocator.shared.analytics, mediaProvider: MediaProviderMock(configuration: .init())) - let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil } let deferredAction = deferFulfillment(viewModel.actions) { action in action == .dismiss(shouldShowDetails: false) } @@ -65,7 +65,7 @@ class ManageRoomMemberSheetViewModelTests: XCTestCase { analyticsService: ServiceLocator.shared.analytics, mediaProvider: MediaProviderMock(configuration: .init())) - let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + let deferred = deferFulfillment(context.observe(\.viewState.bindings.alertInfo)) { $0 != nil } context.send(viewAction: .ban) try await deferred.fulfill() diff --git a/UnitTests/Sources/ReportContentViewModelTests.swift b/UnitTests/Sources/ReportContentScreenViewModelTests.swift similarity index 100% rename from UnitTests/Sources/ReportContentViewModelTests.swift rename to UnitTests/Sources/ReportContentScreenViewModelTests.swift diff --git a/UnitTests/Sources/ReportRoomScreenViewModelTests.swift b/UnitTests/Sources/ReportRoomScreenViewModelTests.swift index 4807a2071..937f0ba37 100644 --- a/UnitTests/Sources/ReportRoomScreenViewModelTests.swift +++ b/UnitTests/Sources/ReportRoomScreenViewModelTests.swift @@ -90,9 +90,7 @@ class ReportRoomScreenViewModelTests: XCTestCase { return .failure(.eventNotFound) } - let deferred = deferFulfillment(context.$viewState) { state in - state.bindings.alert != nil - } + let deferred = deferFulfillment(context.observe(\.viewState.bindings.alert)) { $0 != nil } context.reason = reason context.shouldLeaveRoom = true diff --git a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift index d7834f7fc..1b6713f74 100644 --- a/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift +++ b/UnitTests/Sources/ResolveVerifiedUserSendFailureScreenViewModelTests.swift @@ -26,7 +26,7 @@ class ResolveVerifiedUserSendFailureScreenViewModelTests: XCTestCase { 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"]) }) + let devices = Dictionary(uniqueKeysWithValues: userIDs.map { ($0, ["DEVICE1, DEVICE2"]) }) viewModel = makeViewModel(with: .hasUnsignedDevice(devices: devices)) try await verifyResolving(userIDs: userIDs, assertStrings: false) diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift similarity index 90% rename from UnitTests/Sources/RoomDetailsViewModelTests.swift rename to UnitTests/Sources/RoomDetailsScreenViewModelTests.swift index 07e7ac845..5399334bc 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift @@ -5,6 +5,7 @@ // Please see LICENSE files in the repository root for full details. // +import AsyncAlgorithms import Combine import MatrixRustSDK import SwiftUI @@ -48,9 +49,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - let deferred = deferFulfillment(context.$viewState) { state in - state.bindings.leaveRoomAlertItem != nil - } + let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil } context.send(viewAction: .processTapLeave) try await deferred.fulfill() @@ -71,9 +70,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - let deferred = deferFulfillment(context.$viewState) { state in - state.bindings.leaveRoomAlertItem != nil - } + let deferred = deferFulfillment(context.observe(\.viewState.bindings.leaveRoomAlertItem)) { $0 != nil } context.send(viewAction: .processTapLeave) @@ -150,9 +147,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - let deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.dmRecipientInfo != nil - } + let deferred = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } try await deferred.fulfill() @@ -174,21 +169,18 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - var deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.dmRecipientInfo != nil - } + let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } - try await deferred.fulfill() + try await deferredRecipient.fulfill() XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient)) - - deferred = deferFulfillment(viewModel.context.$viewState, - keyPath: \.isProcessingIgnoreRequest, - transitionValues: [false, true, false]) + + let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest), + transitionValues: [false, true, false]) context.send(viewAction: .ignoreConfirmed) - try await deferred.fulfill() + try await deferredProcessing.fulfill() XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == true) } @@ -209,21 +201,18 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - var deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.dmRecipientInfo != nil - } + let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } - try await deferred.fulfill() + try await deferredRecipient.fulfill() XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient)) - deferred = deferFulfillment(viewModel.context.$viewState, - keyPath: \.isProcessingIgnoreRequest, - transitionValues: [false, true, false]) + let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest), + transitionValues: [false, true, false]) context.send(viewAction: .ignoreConfirmed) - try await deferred.fulfill() + try await deferredProcessing.fulfill() XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == false) XCTAssertNotNil(context.alertInfo) @@ -243,21 +232,18 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - var deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.dmRecipientInfo != nil - } + let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } - try await deferred.fulfill() + try await deferredRecipient.fulfill() XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient)) - deferred = deferFulfillment(viewModel.context.$viewState, - keyPath: \.isProcessingIgnoreRequest, - transitionValues: [false, true, false]) + let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest), + transitionValues: [false, true, false]) context.send(viewAction: .unignoreConfirmed) - try await deferred.fulfill() + try await deferredProcessing.fulfill() XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == false) } @@ -278,21 +264,18 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - var deferred = deferFulfillment(viewModel.context.$viewState) { state in - state.dmRecipientInfo != nil - } + let deferredRecipient = deferFulfillment(viewModel.context.observe(\.viewState.dmRecipientInfo)) { $0 != nil } - try await deferred.fulfill() + try await deferredRecipient.fulfill() XCTAssertEqual(context.viewState.dmRecipientInfo?.member, RoomMemberDetails(withProxy: recipient)) - deferred = deferFulfillment(viewModel.context.$viewState, - keyPath: \.isProcessingIgnoreRequest, - transitionValues: [false, true, false]) + let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest), + transitionValues: [false, true, false]) context.send(viewAction: .unignoreConfirmed) - try await deferred.fulfill() + try await deferredProcessing.fulfill() XCTAssert(context.viewState.dmRecipientInfo?.member.isIgnored == true) XCTAssertNotNil(context.alertInfo) @@ -314,7 +297,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertFalse(context.viewState.canInviteUsers) } @@ -332,7 +315,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertTrue(context.viewState.canInviteUsers) @@ -385,7 +368,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertTrue(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomName) @@ -425,7 +408,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertTrue(context.viewState.canEditRoomName) @@ -465,7 +448,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomName) @@ -486,7 +469,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomName) @@ -507,7 +490,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertFalse(context.viewState.canEdit) } @@ -526,17 +509,13 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - var deferred = deferFulfillment(context.$viewState) { state in - state.notificationSettingsState.isError - } + var deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isError } try await deferred.fulfill() notificationSettingsProxyMock.callbacks.send(.settingsDidChange) - deferred = deferFulfillment(context.$viewState) { state in - state.notificationSettingsState.isError - } + deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isError } try await deferred.fulfill() @@ -551,9 +530,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testNotificationDefaultMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: true)) - let deferred = deferFulfillment(context.$viewState) { state in - state.notificationSettingsState.isLoaded - } + let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isLoaded } notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() @@ -564,9 +541,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testNotificationCustomMode() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .allMessages, isDefault: false)) - let deferred = deferFulfillment(context.$viewState) { state in - state.notificationSettingsState.isCustom - } + let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isCustom } notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() @@ -577,14 +552,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testNotificationRoomMuted() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mute, isDefault: false)) - let deferred = deferFulfillment(context.$viewState) { state in - state.notificationSettingsState.isLoaded - } + let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isLoaded } notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first() + _ = await context.observe(\.viewState).debounce(for: .milliseconds(100)).first() XCTAssertEqual(context.viewState.notificationShortcutButtonTitle, L10n.commonUnmute) XCTAssertEqual(context.viewState.notificationShortcutButtonIcon, \.notificationsOff) @@ -593,9 +566,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testNotificationRoomNotMuted() async throws { notificationSettingsProxyMock.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: .mentionsAndKeywordsOnly, isDefault: false)) - let deferred = deferFulfillment(context.$viewState) { state in - state.notificationSettingsState.isLoaded - } + let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { $0.isLoaded } notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() @@ -667,12 +638,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertFalse(context.viewState.isProcessingMuteToggleAction) - let deferred = deferFulfillment(context.$viewState) { state in - switch state.notificationSettingsState { - case .loaded(settings: let settings): - return settings.mode == .mute - default: - return false + let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { state in + switch state { + case .loaded(settings: let settings): settings.mode == .mute + default: false } } @@ -700,12 +669,10 @@ class RoomDetailsScreenViewModelTests: XCTestCase { XCTAssertFalse(context.viewState.isProcessingMuteToggleAction) - let deferred = deferFulfillment(context.$viewState) { state in - switch state.notificationSettingsState { - case .loaded(settings: let settings): - return settings.mode == .allMessages - default: - return false + let deferred = deferFulfillment(context.observe(\.viewState.notificationSettingsState)) { state in + switch state { + case .loaded(settings: let settings): settings.mode == .allMessages + default: false } } @@ -736,7 +703,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - let deferred = deferFulfillment(context.$viewState) { state in + let deferred = deferFulfillment(context.observe(\.viewState)) { state in state.knockRequestsCount == 2 && state.canSeeKnockingRequests } try await deferred.fulfill() @@ -759,7 +726,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - let deferred = deferFulfillment(context.$viewState) { state in + let deferred = deferFulfillment(context.observe(\.viewState)) { state in state.knockRequestsCount == 0 && state.canSeeKnockingRequests } @@ -784,7 +751,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - let deferred = deferFulfillment(context.$viewState) { state in + let deferred = deferFulfillment(context.observe(\.viewState)) { state in state.knockRequestsCount == 2 && state.dmRecipientInfo == nil && !state.canSeeKnockingRequests && @@ -809,7 +776,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase { appMediator: AppMediatorMock.default, appSettings: ServiceLocator.shared.settings) - let deferred = deferFulfillment(context.$viewState) { state in + let deferred = deferFulfillment(context.observe(\.viewState)) { state in state.knockRequestsCount == 2 && !state.canSeeKnockingRequests && state.dmRecipientInfo != nil && diff --git a/UnitTests/Sources/UserProfileScreenViewModelTests.swift b/UnitTests/Sources/UserProfileScreenViewModelTests.swift index 18884f840..5fb053e80 100644 --- a/UnitTests/Sources/UserProfileScreenViewModelTests.swift +++ b/UnitTests/Sources/UserProfileScreenViewModelTests.swift @@ -26,7 +26,7 @@ class UserProfileScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) - let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.userProfile != nil } + let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil } try await waitForMemberToLoad.fulfill() XCTAssertFalse(context.viewState.isOwnUser) @@ -46,7 +46,7 @@ class UserProfileScreenViewModelTests: XCTestCase { userIndicatorController: ServiceLocator.shared.userIndicatorController, analytics: ServiceLocator.shared.analytics) - let waitForMemberToLoad = deferFulfillment(context.$viewState) { $0.userProfile != nil } + let waitForMemberToLoad = deferFulfillment(context.observe(\.viewState.userProfile)) { $0 != nil } try await waitForMemberToLoad.fulfill() XCTAssertTrue(context.viewState.isOwnUser) diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index 0e3301240..f1c788790 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -32,6 +32,7 @@ targets: dependencies: - target: ElementX - package: MatrixRustSDK + - package: AsyncAlgorithms info: path: ../SupportingFiles/Info.plist diff --git a/project.yml b/project.yml index 05632c78e..2fa2fa9ac 100644 --- a/project.yml +++ b/project.yml @@ -95,6 +95,9 @@ packages: Algorithms: url: https://github.com/apple/swift-algorithms minorVersion: 1.2.1 + AsyncAlgorithms: + url: https://github.com/apple/swift-async-algorithms + minorVersion: 1.0.0 Collections: url: https://github.com/apple/swift-collections minorVersion: 1.2.0