diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index d5d287079..ae2774f42 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -179,14 +179,6 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "FullscreenDialog_Previews") } - func testGlobalSearchScreenListRow() async throws { - try await performAccessibilityAudit(named: "GlobalSearchScreenListRow_Previews") - } - - func testGlobalSearchScreen() async throws { - try await performAccessibilityAudit(named: "GlobalSearchScreen_Previews") - } - func testHighlightedTimelineItemModifier() async throws { try await performAccessibilityAudit(named: "HighlightedTimelineItemModifier_Previews") } @@ -611,6 +603,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "SFNumberedListView_Previews") } + func testSearchScreen() async throws { + try await performAccessibilityAudit(named: "SearchScreen_Previews") + } + func testSecureBackupKeyBackupScreen() async throws { try await performAccessibilityAudit(named: "SecureBackupKeyBackupScreen_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index a6ae2f16d..8a4318286 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -263,6 +263,7 @@ 298F9EC30E918F12AB7F1EE8 /* TypingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81F0325E252B057FAEEE1B2D /* TypingIndicatorView.swift */; }; 29E411E37D3FBF3899B3ED69 /* UserDefaultsProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCA4547BE58E17899E95CDE /* UserDefaultsProtocol.swift */; }; 29EE1791E0AFA1ABB7F23D2F /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 1BCD21310B997A6837B854D6 /* GZIP */; }; + 2A4B7856611589DD7B1B36CA /* SearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79A78330B9329E3A99E4105 /* SearchScreen.swift */; }; 2A4F8B76E1F99DE95D5867E6 /* ClassicAppManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 633BAD3D9BB44B2AED7CBB93 /* ClassicAppManagerMock.swift */; }; 2A56B00B070F83E0FE571193 /* TimelineMediaPreviewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = B18A454132A5A5247802821E /* TimelineMediaPreviewDataSource.swift */; }; 2A61D2B4A225332CECA3B937 /* ReportRoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20403084A320D588ACED200 /* ReportRoomScreenViewModelProtocol.swift */; }; @@ -355,6 +356,7 @@ 39A987B3E41B976D1DF944C6 /* CallScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */; }; 39DFC4B9EB6A8757210BDEC6 /* RoomSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD2A058F3566FEEBA1D11B3 /* RoomSelectionScreenViewModelProtocol.swift */; }; 3A08584ECDD4A4541DBF21F8 /* EmojiLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201305507D7DFD16E544563A /* EmojiLoaderProtocol.swift */; }; + 3A0D1A201E2CD209A04CE89B /* SearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD63A653BCA2C8112654B19 /* SearchScreenModels.swift */; }; 3A164187907DA43B7858F9EC /* CompletionSuggestionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5EA0312A6262484AA393AC9 /* CompletionSuggestionServiceTests.swift */; }; 3A2640C52505BAB3D983A92A /* RemotePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69A05E85E4872C3221C5C287 /* RemotePreference.swift */; }; 3A64A93A651A3CB8774ADE8E /* SwiftOGG in Frameworks */ = {isa = PBXBuildFile; productRef = 3FE40E79C36E7903121E6E3B /* SwiftOGG */; }; @@ -505,6 +507,7 @@ 53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; }; 5415236FA271AF7885D4995E /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AB32690C6CE5C62A86D6FA /* NotificationItemProxy.swift */; }; 5470E62F65AE1803BBF3D528 /* CXProviderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86E1BAA7232081635662A83F /* CXProviderMock.swift */; }; + 54804A7D8298F7E8ACAFDD76 /* SearchScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C70BD39FC34A9FD2467540 /* SearchScreenViewModelProtocol.swift */; }; 54A9F957307CC55242564CE5 /* MapTilerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C3A7F76FE12E274D2ED8DF4 /* MapTilerSettings.swift */; }; 54AE8860D668AFD96E7E177B /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; }; 54FDA3625AACBD9E438D084D /* BlurEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07934EF08BB39353E4A94272 /* BlurEffectView.swift */; }; @@ -514,7 +517,6 @@ 558E2673B04FDD06A1A12DD3 /* LogViewerScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */; }; 558F37B1A8F2C4CC9B1ACEDA /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = CAA3B9DF998B397C9EE64E8B /* Collections */; }; 55CDD3968D95D1A820B5491E /* PlaceholderAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */; }; - 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */; }; 55DF6DEEF2CEEF40F84B53B0 /* VoiceMessageRoomPlaybackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E3B41C36800DD4558D7BDA7 /* VoiceMessageRoomPlaybackView.swift */; }; 5618ED25F092DF5712003829 /* KeychainControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */; }; 562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933B074F006F8E930DB98B4E /* TimelineMediaFrame.swift */; }; @@ -535,6 +537,7 @@ 5894C2514400A4FBC9327632 /* ServerConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03277E40D0E0DE0712021A71 /* ServerConfirmationScreenCoordinator.swift */; }; 5897A59DDBD3592282092223 /* MediaSourceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49B9785E3AD7D1C15A29F2F /* MediaSourceProxy.swift */; }; 58F357A9D130A654ABCB1638 /* RageshakeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC598338E7CF41107293AB5 /* RageshakeConfiguration.swift */; }; + 592A5B88471676D6F50B9798 /* SearchScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6B10AEA5983B4A2336387B /* SearchScreenCoordinator.swift */; }; 5992EF10AA157EBD97D88910 /* AudioRecorderState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6569593FA36B22259E806A67 /* AudioRecorderState.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; 59F940FCBE6BC343AECEF75E /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; }; @@ -649,6 +652,7 @@ 6B3AB95A6993646A5081BFF9 /* TestablePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E43F773904F87FF5ADFE4DD1 /* TestablePreview.swift */; }; 6B61F5B27412ED4BC2F9769C /* test_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 66B96842BF5F8ACA1AC84C55 /* test_audio.mp3 */; }; 6B80C24A52411EAF10E06E96 /* SecurityAndPrivacyScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBADF8010C813D905C172CE /* SecurityAndPrivacyScreenModels.swift */; }; + 6B8F03A85E83755D98DF0443 /* SearchScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C4724D5CB16CFA5853EF8F9 /* SearchScreenViewModel.swift */; }; 6BA9214F31A24C7A84287A56 /* SentryEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337AD37403015231CAE3E80B /* SentryEvent.swift */; }; 6BAE34CFA9821709CFE61E50 /* DeveloperOptionsScreenHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F60FDB3AEFC60830BD289FE /* DeveloperOptionsScreenHook.swift */; }; 6BEB10499149444909EBE42F /* TrackedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10D751743B20B314D84D79FE /* TrackedUserDefaults.swift */; }; @@ -761,7 +765,6 @@ 7C9BDF1FC7BD46C4676536AB /* AuthenticationStartScreenBackgroundImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682BC7BAF0EFEF512A8C5140 /* AuthenticationStartScreenBackgroundImage.swift */; }; 7CD16990BA843BE9ED639129 /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DFE4453AB0B34C203447162 /* ImageRoomTimelineItem.swift */; }; 7D249465ED00988EEEC14E05 /* JoinedRoomProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867DC9530C42F7B5176BE465 /* JoinedRoomProxyMock.swift */; }; - 7D261B5119E78CC8E771CA15 /* GlobalSearchScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */; }; 7D58B4F46CAA9A7C3E4C6A30 /* UserDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88410BD213FDF9B28E8B671F /* UserDetailsEditScreen.swift */; }; 7D6DC832DE7A3DE874E2E9BC /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = BB111AE9D390233CDD2C7FD5 /* MatrixRustSDK */; }; 7DCFC31B49BDD2BC32184E58 /* TimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C0D5AE752AF87DA0A920812 /* TimelineControllerFactory.swift */; }; @@ -968,7 +971,6 @@ 9EE71509E6E7519A2B2388B3 /* KnockRequestsListScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD9C9A31D9AB3B6D8128E69 /* KnockRequestsListScreenModels.swift */; }; 9EF72884B74BC39B01640098 /* BugReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DF257F5D968E5DD719583C /* BugReportTests.swift */; }; 9F11B9F347F9E2D236799FB3 /* ElementCallServiceConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */; }; - 9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */; }; 9F8BEA86540D8980BDD7C176 /* AuthenticationStartScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE3AB4D399D39FBD78119011 /* AuthenticationStartScreenModels.swift */; }; 9FB41B0E8B2AA9B404E52C8B /* AppLockSetupBiometricsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */; }; 9FBE1FB20171012260A32492 /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */; }; @@ -1280,7 +1282,6 @@ D304343515CE0464C5089537 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD5523BDEDB247E29228476 /* AppSettings.swift */; }; D31B34B3902BC597593F3ABB /* preview_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 200626E8353AB2729444F991 /* preview_image.jpg */; }; D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5098DA7799946A61E34A2373 /* FileRoomTimelineItem.swift */; }; - D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */; }; D38E59C48BE5499A48D12031 /* CreateRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AC8FCE224D4185F28636FF /* CreateRoomScreenCoordinator.swift */; }; D3ED5692672892F9F1E7375A /* LiveLocationSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2FEFA393FC7D2870263012 /* LiveLocationSession.swift */; }; D3FD96913D2B1AAA3149DAC7 /* CreateRoomViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */; }; @@ -1298,6 +1299,7 @@ D5FE90A6AF5FD5AE91BD37C7 /* NotificationSettingsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780258F1B9D15E30549FF4BE /* NotificationSettingsEditScreenViewModel.swift */; }; D6152E21036B88C44ECB22E7 /* EncryptionResetPasswordScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */; }; D63974A88CF2BC721F109C77 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = DCA3C4A997AD28E6918D4CE5 /* Compound */; }; + D69CCEC785E6C819551B402F /* SearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5413213B56FB76C1071E2BFB /* SearchScreenViewModelTests.swift */; }; D6DE764B17FB4A9A12C33BF4 /* MessageComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1DF3FFFE5ED2B8133F43A7 /* MessageComposer.swift */; }; D75C591C44C2510FDFA9C3D8 /* Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = AFAD718C6621B94813B960B4 /* Dynamic */; }; D7CDBAE82782BD0529DECB5F /* AttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52BD6ED18E2EB61E28C340AD /* AttributedString.swift */; }; @@ -1357,7 +1359,6 @@ E313BDD2B8813144139B2E00 /* UserDiscoveryServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */; }; E323A54F317604BDD6968D79 /* UITestsSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */; }; E3291AD16D7A5CB14781819C /* UserNotificationCenterProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */; }; - E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B788615712FED326F73D3F83 /* GlobalSearchScreenViewModelProtocol.swift */; }; E32E71A1AF5E5E69E8363B26 /* OAuthAuthenticationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA11EF88B2848B12FA6FA36 /* OAuthAuthenticationPresenter.swift */; }; E3AC72E3E58F364EF15C1CC7 /* NotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514363244AE7D68080D44C6F /* NotificationSettingsScreenViewModelTests.swift */; }; E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; }; @@ -1429,7 +1430,6 @@ EE17B7154DCA50677D931A94 /* NavigationTabCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B67DC84B42D86035FCFF6F8 /* NavigationTabCoordinatorTests.swift */; }; EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; }; EE4F6D6F54B8F09F10CA66BE /* ClassicAppAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384A744714571BAF138C6B86 /* ClassicAppAES.swift */; }; - EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEB9C1555C63B93CA9C372C2 /* EmojiPickerScreenHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B5E29E9A22F45534FBD5B58 /* EmojiPickerScreenHeaderView.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; @@ -1449,7 +1449,6 @@ F08F7BC07CA9AEF5CD157918 /* Snapshotting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF17EA323AD0205A6AB621AA /* Snapshotting.swift */; }; F0A027BB2369606DBDE3BDAD /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 32B8F4CD937AA9C1F8FC3CBC /* KeychainAccess */; }; F0A26CD502C3A5868353B0FA /* ServerConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */; }; - F0DACC95F24128A54CD537E4 /* GlobalSearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B8177BD2AF45A286F5DA31 /* GlobalSearchScreen.swift */; }; F0F82C3C848C865C3098AA52 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = BA93CD75CCE486660C9040BD /* Collections */; }; F103924DED414ADFE398CE99 /* RoomPollsHistoryScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */; }; F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; }; @@ -1714,6 +1713,7 @@ 0CBB0B65E5C1686700EF1276 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/SAS.strings; sourceTree = ""; }; 0CCA4547BE58E17899E95CDE /* UserDefaultsProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsProtocol.swift; sourceTree = ""; }; 0CCC6C31102E1D8B9106DEDE /* AppLockSetupBiometricsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupBiometricsScreenViewModelProtocol.swift; sourceTree = ""; }; + 0CD63A653BCA2C8112654B19 /* SearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreenModels.swift; sourceTree = ""; }; 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenModels.swift; sourceTree = ""; }; 0D432771B389EC486E7F90C6 /* CreateRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenModels.swift; sourceTree = ""; }; 0D879FC4E881E748BB9B34DC /* RoomChangePermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreenCoordinator.swift; sourceTree = ""; }; @@ -1808,6 +1808,7 @@ 1C21A715237F2B6D6E80998C /* SecureBackupControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerProtocol.swift; sourceTree = ""; }; 1C25B6EBEB414431187D73B7 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; 1C3A7F76FE12E274D2ED8DF4 /* MapTilerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerSettings.swift; sourceTree = ""; }; + 1C4724D5CB16CFA5853EF8F9 /* SearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreenViewModel.swift; sourceTree = ""; }; 1C78111573987B1D79ED0868 /* LinkMetadataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMetadataProvider.swift; sourceTree = ""; }; 1C782FCBBCC9A0CD30453C50 /* ChatsTabFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsTabFlowCoordinator.swift; sourceTree = ""; }; 1C7F63EB1525E697CAEB002B /* BlankFormCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlankFormCoordinator.swift; sourceTree = ""; }; @@ -1850,7 +1851,6 @@ 22730A30C50AC2E3D5BA8642 /* InviteUsersScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenViewModelProtocol.swift; sourceTree = ""; }; 227AC5D71A4CE43512062243 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 22B384D54464FA39C6C7F6E7 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ca; path = ca.lproj/Localizable.stringsdict; sourceTree = ""; }; - 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCell.swift; sourceTree = ""; }; 233D5F7E5E9F49ABF3413291 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = hr; path = hr.lproj/Localizable.stringsdict; sourceTree = ""; }; 2355398E4A55DA5A89128AD1 /* EncryptionKeyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionKeyProvider.swift; sourceTree = ""; }; 2363DB6162BBCC511B67B527 /* AccessibilityTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = AccessibilityTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1862,7 +1862,6 @@ 2429224EB0EEA34D35CE9249 /* UserIndicatorControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorControllerTests.swift; sourceTree = ""; }; 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorderTests.swift; sourceTree = ""; }; 248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyMock.swift; sourceTree = ""; }; - 24B8177BD2AF45A286F5DA31 /* GlobalSearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreen.swift; sourceTree = ""; }; 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModel.swift; sourceTree = ""; }; 24E637CF570711FB5FD63DEA /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; 24EC819497BB5F8C4998D760 /* RoomListFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFilterView.swift; sourceTree = ""; }; @@ -1967,7 +1966,6 @@ 37FEE10AB666891E6A675E5E /* SecureBackupLogoutConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreen.swift; sourceTree = ""; }; 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyMock.swift; sourceTree = ""; }; 38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenPINKeypad.swift; sourceTree = ""; }; - 38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModel.swift; sourceTree = ""; }; 384A744714571BAF138C6B86 /* ClassicAppAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppAES.swift; sourceTree = ""; }; 38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreen.swift; sourceTree = ""; }; 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenModelProtocol.swift; sourceTree = ""; }; @@ -1976,6 +1974,7 @@ 39C78D5FF15343724902EB20 /* ClassicAppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppManager.swift; sourceTree = ""; }; 3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionService.swift; sourceTree = ""; }; 3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitedRoomProxyMock.swift; sourceTree = ""; }; + 3A6B10AEA5983B4A2336387B /* SearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreenCoordinator.swift; sourceTree = ""; }; 3AB34956C87731AB094DB33A /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = ""; }; 3AD253E7EFF88F308D644272 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SAS.strings"; sourceTree = ""; }; 3B5E97E9615A158C76B2AB77 /* DateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTests.swift; sourceTree = ""; }; @@ -2031,7 +2030,6 @@ 430C73079A84654BF46A7FF5 /* FileMediaEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMediaEventsTimelineView.swift; sourceTree = ""; }; 4338BB055A26094673CDC220 /* SpaceAddRoomsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceAddRoomsScreenViewModel.swift; sourceTree = ""; }; 434522ED2BDED08759048077 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; - 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = ""; }; 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = ""; }; 43C2067FF58B4996323EB40C /* SessionDirectories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectories.swift; sourceTree = ""; }; 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = ""; }; @@ -2040,6 +2038,7 @@ 44AEEE13AC1BF303AE48CBF8 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; 44B71F6D9062E8EB8929BB97 /* KnockRequestCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestCell.swift; sourceTree = ""; }; 44C314C00533E2C297796B60 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; + 44C70BD39FC34A9FD2467540 /* SearchScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreenViewModelProtocol.swift; sourceTree = ""; }; 44DDC82DB6A84E700CD5DEC0 /* RoomRolesAndPermissionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsTests.swift; sourceTree = ""; }; 44ECC9D66400727DFFEE12E8 /* TimelineStartRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineView.swift; sourceTree = ""; }; 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; @@ -2127,6 +2126,7 @@ 536E72DCBEEC4A1FE66CFDCE /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; 53F41CEAAE2BB4E74CDC2278 /* TimelineMediaPreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMediaPreviewViewModel.swift; sourceTree = ""; }; 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = test_animated_image.gif; sourceTree = ""; }; + 5413213B56FB76C1071E2BFB /* SearchScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreenViewModelTests.swift; sourceTree = ""; }; 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 544B9262E1BC6F489C03FFFA /* RoomThreadListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomThreadListScreen.swift; sourceTree = ""; }; 5484457C81B325660901B161 /* AppLockSetupSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupSettingsScreen.swift; sourceTree = ""; }; @@ -2296,7 +2296,6 @@ 74240BE69DACAD01AA670730 /* ClassicAppMXAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppMXAccount.swift; sourceTree = ""; }; 7447C0AD7EF302CD027D6230 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SAS.strings; sourceTree = ""; }; 7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModelProtocol.swift; sourceTree = ""; }; - 74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCoordinator.swift; sourceTree = ""; }; 7475C5AE20BA896930907EA8 /* AudioRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineItemContent.swift; sourceTree = ""; }; 748AE77AC3B0A01223033B87 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 74AE4F02A507882743973214 /* ManageAuthorizedSpacesScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManageAuthorizedSpacesScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2583,6 +2582,7 @@ A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; A768CA51A59B8A5D8C8FD599 /* AuthenticationStartScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreen.swift; sourceTree = ""; }; A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationTokenTests.swift; sourceTree = ""; }; + A79A78330B9329E3A99E4105 /* SearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreen.swift; sourceTree = ""; }; A7A1B80FE6E3BA72F9C748AD /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2693,7 +2693,6 @@ B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsCustomSectionView.swift; sourceTree = ""; }; B7728AA8046D460145EAC740 /* RoomTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTests.swift; sourceTree = ""; }; B7884BD256C091EB511B2EDF /* AppLockSetupPINScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModelProtocol.swift; sourceTree = ""; }; - B788615712FED326F73D3F83 /* GlobalSearchScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModelProtocol.swift; sourceTree = ""; }; B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsSignalling.swift; sourceTree = ""; }; B80D1901BA0B095E27793EDE /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenCoordinator.swift; sourceTree = ""; }; @@ -2965,7 +2964,6 @@ E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallInviteRoomTimelineItem.swift; sourceTree = ""; }; E9B796D347E53631576F631C /* PreviewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewTests.swift; sourceTree = ""; }; E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = ""; }; - EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModelTests.swift; sourceTree = ""; }; EA551A98778CEE7366838CE2 /* QRCodeLoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeLoginScreenCoordinator.swift; sourceTree = ""; }; EA880E78AF4BD24E45A7808C /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = ""; }; EACD4855BDAD0799FD86B7B5 /* SpaceRoomListProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxyProtocol.swift; sourceTree = ""; }; @@ -4019,15 +4017,6 @@ path = LegalInformationScreen; sourceTree = ""; }; - 390C92506AD893EC593D405D /* View */ = { - isa = PBXGroup; - children = ( - 24B8177BD2AF45A286F5DA31 /* GlobalSearchScreen.swift */, - 22DB19219E6CC4D002E15D48 /* GlobalSearchScreenCell.swift */, - ); - path = View; - sourceTree = ""; - }; 39557ADF21345E18F3865B9E /* Emojis */ = { isa = PBXGroup; children = ( @@ -4739,6 +4728,14 @@ path = View; sourceTree = ""; }; + 62E6DBFA3C4FCB80E2B490CD /* View */ = { + isa = PBXGroup; + children = ( + A79A78330B9329E3A99E4105 /* SearchScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 638665D7260F974BA9A3BFA1 /* TestUtilities */ = { isa = PBXGroup; children = ( @@ -4826,6 +4823,18 @@ name = Products; sourceTree = ""; }; + 68E0BC14DDA20EB062EBDE95 /* SearchScreen */ = { + isa = PBXGroup; + children = ( + 3A6B10AEA5983B4A2336387B /* SearchScreenCoordinator.swift */, + 0CD63A653BCA2C8112654B19 /* SearchScreenModels.swift */, + 1C4724D5CB16CFA5853EF8F9 /* SearchScreenViewModel.swift */, + 44C70BD39FC34A9FD2467540 /* SearchScreenViewModelProtocol.swift */, + 62E6DBFA3C4FCB80E2B490CD /* View */, + ); + path = SearchScreen; + sourceTree = ""; + }; 6B0910BCE4F1B02F124E1A09 /* TimelineItemContent */ = { isa = PBXGroup; children = ( @@ -4964,7 +4973,6 @@ 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */, 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */, 1A7ED2EF5BDBAD2A7DBC4636 /* GeoURITests.swift */, - EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */, B73587C2E3CF5998361AE516 /* HomeScreenRoomTests.swift */, 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */, ECBBCD7444C2B515D02EC0E8 /* IdentityConfirmationScreenViewModelTests.swift */, @@ -5023,6 +5031,7 @@ F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */, 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */, B7728AA8046D460145EAC740 /* RoomTests.swift */, + 5413213B56FB76C1071E2BFB /* SearchScreenViewModelTests.swift */, 848F69921527D31CAACB93AF /* SecureBackupLogoutConfirmationScreenViewModelTests.swift */, 0315C328FF40F84276364E66 /* SecurityAndPrivacyScreenViewModelTests.swift */, 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */, @@ -5460,18 +5469,6 @@ path = MediaPickerScreen; sourceTree = ""; }; - 8A4738BBA7C7A299BAD70372 /* GlobalSearchScreen */ = { - isa = PBXGroup; - children = ( - 74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */, - 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */, - 38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */, - B788615712FED326F73D3F83 /* GlobalSearchScreenViewModelProtocol.swift */, - 390C92506AD893EC593D405D /* View */, - ); - path = GlobalSearchScreen; - sourceTree = ""; - }; 8A9C09B6A392465E03B8D1B1 /* IntegrationTests */ = { isa = PBXGroup; children = ( @@ -6753,7 +6750,6 @@ F5A65D1D3B83593598DC278D /* EmojiPickerScreen */, 8656AFF06650360A5D0695FF /* EncryptionReset */, 448435400B561C40E514BE1C /* FilePreviewScreen */, - 8A4738BBA7C7A299BAD70372 /* GlobalSearchScreen */, B53CA9BECD3F97805E1432D0 /* HomeScreen */, F12966DF3DA87FEF21348D60 /* InviteUsersScreen */, FFD7C58CA6A7D6BBC2F584B5 /* JoinRoomScreen */, @@ -6788,6 +6784,7 @@ 679E9837ECA8D6776079D16E /* RoomScreen */, 2E42D43DB6835A58D88B2F91 /* RoomSelectionScreen */, B329D7B5D250717039B2814A /* RoomThreadListScreen */, + 68E0BC14DDA20EB062EBDE95 /* SearchScreen */, 2565414373E6F68005966B8E /* SecureBackup */, C59BA103987B953BA374509F /* SecurityAndPrivacyScreen */, 70B74A432C241E56A7ACE610 /* Settings */, @@ -7946,7 +7943,6 @@ 25618589E0DE0F1E95FC7B5C /* EmojiProviderTests.swift in Sources */, 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */, 07756D532EFE33DD1FA258E5 /* GeoURITests.swift in Sources */, - EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */, 6817EAD73DC1FFD8B943B5B9 /* HomeScreenRoomTests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, 00091F143129B9A4904563C4 /* IdentityConfirmationScreenViewModelTests.swift in Sources */, @@ -8014,6 +8010,7 @@ 6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */, 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */, 62811275F1ED9EA55638838E /* RoomTests.swift in Sources */, + D69CCEC785E6C819551B402F /* SearchScreenViewModelTests.swift in Sources */, EB87DF90CF6F8D5D12404C6E /* SecureBackupLogoutConfirmationScreenViewModelTests.swift in Sources */, CB9FB2BEF313072C705AC9B5 /* SecurityAndPrivacyScreenViewModelTests.swift in Sources */, 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */, @@ -8431,12 +8428,6 @@ 46BA7F4B4D3A7164DED44B88 /* FullscreenDialog.swift in Sources */, F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */, 4295E5F850897710A51AE114 /* GeoURI.swift in Sources */, - F0DACC95F24128A54CD537E4 /* GlobalSearchScreen.swift in Sources */, - 9F11E743EA01482E78A438B0 /* GlobalSearchScreenCell.swift in Sources */, - 7D261B5119E78CC8E771CA15 /* GlobalSearchScreenCoordinator.swift in Sources */, - D34E328E9E65904358248FDD /* GlobalSearchScreenModels.swift in Sources */, - 55D18AA4F4A2257642EBDB94 /* GlobalSearchScreenViewModel.swift in Sources */, - E32A18802EB37EEE3EF7B965 /* GlobalSearchScreenViewModelProtocol.swift in Sources */, F3C9CAD26FD4D7D6EBACF501 /* HTMLFixtures.swift in Sources */, E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */, 0C1E537A49ABB386F7554D4A /* HighlightedTimelineItemModifier.swift in Sources */, @@ -8918,6 +8909,11 @@ 0437765FF480249486893CC7 /* ScreenTrackerViewModifier.swift in Sources */, 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */, 67160204A8D362BB7D4AD259 /* Search.swift in Sources */, + 2A4B7856611589DD7B1B36CA /* SearchScreen.swift in Sources */, + 592A5B88471676D6F50B9798 /* SearchScreenCoordinator.swift in Sources */, + 3A0D1A201E2CD209A04CE89B /* SearchScreenModels.swift in Sources */, + 6B8F03A85E83755D98DF0443 /* SearchScreenViewModel.swift in Sources */, + 54804A7D8298F7E8ACAFDD76 /* SearchScreenViewModelProtocol.swift in Sources */, 22B380C579C148BA0BFB5952 /* Secrets.swift in Sources */, 339BC18777912E1989F2F17D /* Section.swift in Sources */, F833D5B5BE6707F961FA88DB /* SecureBackupController.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 450402793..91af059f3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -1,3 +1,7 @@ +"screen_home_tab_search" = "Search"; +"screen_search_empty_state_message" = "Search for rooms"; +"screen_search_empty_state_title" = "Start searching..."; +"screen_search_no_results_message" = "There are no results for “%1$@.” Try a new search term."; "soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages."; "soft_logout_clear_data_dialog_title" = "Clear data"; "soft_logout_clear_data_notice" = "Warning: Your personal data (including encryption keys) is still stored on this device.\n\nClear it if you’re finished using this device, or want to sign in to another account."; diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift index f11bbd83f..16867ed01 100644 --- a/ElementX/Sources/Application/Application.swift +++ b/ElementX/Sources/Application/Application.swift @@ -74,7 +74,7 @@ struct Application: App { CommandGroup(after: .windowArrangement) { Button("Global Search") { - appCoordinator.handleAppRoute(.globalSearch, windowType: nil) + appCoordinator.handleAppRoute(.search, windowType: nil) } .keyboardShortcut("k", modifiers: [.command]) } diff --git a/ElementX/Sources/Application/Navigation/AppRoutes.swift b/ElementX/Sources/Application/Navigation/AppRoutes.swift index de57cde51..c2a458801 100644 --- a/ElementX/Sources/Application/Navigation/AppRoutes.swift +++ b/ElementX/Sources/Application/Navigation/AppRoutes.swift @@ -54,8 +54,8 @@ enum AppRoute: Hashable { case transferOwnership(roomID: String) /// A thread within a room, only to be used to handle tap on notification for threaded events. case thread(roomID: String, threadRootEventID: String, focusEventID: String?) - /// The global search screen - case globalSearch + /// The search screen + case search /// Whether or not the route should be handled by the authentication flow. var isAuthenticationRoute: Bool { diff --git a/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift b/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift index 0ada38c54..b7bef22b8 100644 --- a/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift +++ b/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift @@ -23,17 +23,21 @@ import SwiftUI let title: String let icon: KeyPath let selectedIcon: KeyPath + /// When `true` the tab is given the `.search` role, which on iOS 26 detaches it + /// into the trailing search button in the tab bar. + let isSearch: Bool var badgeCount = 0 /// Provide the tab's split coordinator in here to have the tab bar automatically hidden /// when pushing a child into the split view's details on iPhone/compact iPad. weak var navigationSplitCoordinator: NavigationSplitCoordinator? - init(tag: Tag, title: String, icon: KeyPath, selectedIcon: KeyPath) { + init(tag: Tag, title: String, icon: KeyPath, selectedIcon: KeyPath, isSearch: Bool = false) { self.tag = tag self.title = title self.icon = icon self.selectedIcon = selectedIcon + self.isSearch = isSearch } func barVisibility(in horizontalSizeClass: UserInterfaceSizeClass?) -> Visibility { @@ -294,18 +298,18 @@ private struct NavigationTabCoordinatorView: View { var body: some View { TabView(selection: $navigationTabCoordinator.selectedTab) { ForEach(navigationTabCoordinator.tabModules) { module in - module.coordinator?.toPresentable() - .id(module.id) - .tabItem { - Label { - Text(module.details.title) - } icon: { - CompoundIcon(module.details.tag == navigationTabCoordinator.selectedTab ? module.details.selectedIcon : module.details.icon) - } + Tab(value: module.details.tag, role: module.details.isSearch ? .search : nil) { + module.coordinator?.toPresentable() + .id(module.id) + .toolbar(module.details.barVisibility(in: horizontalSizeClass), for: .tabBar) + } label: { + Label { + Text(module.details.title) + } icon: { + CompoundIcon(module.details.tag == navigationTabCoordinator.selectedTab ? module.details.selectedIcon : module.details.icon) } - .tag(module.details.tag) - .badge(module.details.badgeCount) - .toolbar(module.details.barVisibility(in: horizontalSizeClass), for: .tabBar) + } + .badge(module.details.badgeCount) } } .backportTabBarMinimizeBehaviorOnScrollDown() diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index 9053a9d76..9a004f6f7 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -24,6 +24,7 @@ nonisolated protocol CommonSettingsProtocol: AnyObject, Sendable { var enableOnlySignedDeviceIsolationMode: Bool { get } var threadsEnabled: Bool { get } + var globalSearchEnabled: Bool { get } var hideQuietNotificationAlerts: Bool { get } } @@ -79,6 +80,7 @@ final nonisolated class AppSettings: @unchecked Sendable { case enableOnlySignedDeviceIsolationMode case knockingEnabled case threadsEnabled + case globalSearchEnabled case roomThreadListEnabled case linkPreviewsEnabled case jumpToReadMarkerEnabled @@ -439,6 +441,9 @@ final nonisolated class AppSettings: @unchecked Sendable { @UserPreference nonisolated(unsafe) var roomThreadListEnabled: Bool + @UserPreference + nonisolated(unsafe) var globalSearchEnabled: Bool + @UserPreference nonisolated(unsafe) var focusEventOnNotificationTap: Bool @@ -496,6 +501,7 @@ final nonisolated class AppSettings: @unchecked Sendable { _enableOnlySignedDeviceIsolationMode = UserPreference(key: .enableOnlySignedDeviceIsolationMode, defaultValue: false, storage: store) _knockingEnabled = UserPreference(key: .knockingEnabled, defaultValue: false, storage: store) _threadsEnabled = UserPreference(key: .threadsEnabled, defaultValue: false, storage: store) + _globalSearchEnabled = UserPreference(key: .globalSearchEnabled, defaultValue: ProcessInfo().isiOSAppOnMac, storage: store) _roomThreadListEnabled = UserPreference(key: .roomThreadListEnabled, defaultValue: false, storage: store) _focusEventOnNotificationTap = UserPreference(key: .focusEventOnNotificationTap, defaultValue: false, storage: store) _linkPreviewsEnabled = UserPreference(key: .linkPreviewsEnabled, defaultValue: false, storage: store) diff --git a/ElementX/Sources/Application/Windowing/WindowManager.swift b/ElementX/Sources/Application/Windowing/WindowManager.swift index 90a463ded..2981212f9 100644 --- a/ElementX/Sources/Application/Windowing/WindowManager.swift +++ b/ElementX/Sources/Application/Windowing/WindowManager.swift @@ -17,7 +17,6 @@ class WindowManager: SecureWindowManagerProtocol { private(set) var mainWindow: UIWindow! private(set) var overlayWindow: UIWindow! - private(set) var globalSearchWindow: UIWindow! private(set) var alternateWindow: UIWindow! private(set) var openWindowAction: OpenWindowAction! @@ -32,7 +31,7 @@ class WindowManager: SecureWindowManagerProtocol { } var windows: [UIWindow] { - [mainWindow, overlayWindow, globalSearchWindow, alternateWindow] + [mainWindow, overlayWindow, alternateWindow] } // periphery:ignore - auto cancels when reassigned @@ -80,11 +79,6 @@ class WindowManager: SecureWindowManagerProtocol { overlayWindow.backgroundColor = .clear overlayWindow.isHidden = false - globalSearchWindow = UIWindow(windowScene: scene) - globalSearchWindow.tintColor = .compound.textActionPrimary - globalSearchWindow.backgroundColor = .clear - globalSearchWindow.isHidden = true - alternateWindow = UIWindow(windowScene: scene) alternateWindow.tintColor = .compound.textActionPrimary @@ -138,8 +132,6 @@ class WindowManager: SecureWindowManagerProtocol { // e.g. the keyboard being displayed on top of a call sheet. mainWindow.endEditing(true) - hideGlobalSearch() - // alternateWindow.isHidden = false cannot got inside the Task otherwise the timing // is poor when you lock the phone - you briefly see the main window for a few // frames after you've unlocked the phone and then the placeholder animates in. @@ -149,41 +141,9 @@ class WindowManager: SecureWindowManagerProtocol { mainWindow.isHidden = true overlayWindow.isHidden = true - globalSearchWindow.isHidden = true } } - func showGlobalSearch() { - MXLog.info("Received global search presentation request.") - - guard alternateWindow.isHidden else { - MXLog.info("The alternate window is visible, ignoring.") - return - } - - if let mainSession { - let request = UISceneSessionActivationRequest(session: mainSession) - UIApplication.shared.activateSceneSession(for: request) { error in - MXLog.error("Failed to focus window with error: \(error)") - } - } - - globalSearchWindow.isHidden = false - globalSearchWindow.makeKeyAndVisible() - } - - func hideGlobalSearch() { - MXLog.info("Received global search dismissal request.") - - guard alternateWindow.isHidden else { - MXLog.info("The alternate window is visible, ignoring.") - return - } - - globalSearchWindow.isHidden = true - mainWindow.makeKey() - } - // MARK: - OrientationManager func setOrientation(_ orientation: UIInterfaceOrientationMask) { diff --git a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift index d96e98d96..b7247e83e 100644 --- a/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift +++ b/ElementX/Sources/Application/Windowing/WindowManagerProtocol.swift @@ -50,19 +50,12 @@ protocol WindowManagerProtocol: AnyObject, OrientationManagerProtocol { var mainWindow: UIWindow! { get } /// Presented on top of the main window, to display e.g. user indicators. var overlayWindow: UIWindow! { get } - /// A window layered on top of the main one. Used by the global search function - var globalSearchWindow: UIWindow! { get } /// A secondary window that can be presented instead of the main/overlay window combo. var alternateWindow: UIWindow! { get } /// All the windows being managed var windows: [UIWindow] { get } - /// Makes the global search window key. Used to get automatic text field focus. - func showGlobalSearch() - - func hideGlobalSearch() - // MARK: - Secondary window support var secondaryWindowsEnabled: Bool { get set } diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift index 470bc4a9a..a763244ec 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift @@ -43,9 +43,6 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { // periphery:ignore - retaining purpose private var startChatFlowCoordinator: StartChatFlowCoordinator? - // periphery:ignore - retaining purpose - private var globalSearchScreenCoordinator: GlobalSearchScreenCoordinator? - private var cancellables = Set() private let sidebarNavigationStackCoordinator: NavigationStackCoordinator @@ -172,11 +169,9 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { } else { stateMachine.processEvent(.presentTransferOwnershipScreen(roomID: roomID)) } - case .globalSearch: - presentGlobalSearch() case .chatBackupSettings: actionsSubject.send(.showChatBackupSettings) - case .accountProvisioningLink, .oAuthCallback, .settings, .call: + case .accountProvisioningLink, .oAuthCallback, .settings, .call, .search: break // These routes cannot be handled. } } @@ -720,45 +715,6 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { } } - // MARK: Global search - - private func presentGlobalSearch() { - let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider - - let coordinator = GlobalSearchScreenCoordinator(parameters: .init(roomSummaryProvider: roomSummaryProvider, - mediaProvider: userSession.mediaProvider)) - - globalSearchScreenCoordinator = coordinator - - coordinator.actions - .sink { [weak self] action in - guard let self else { return } - - switch action { - case .dismiss: - dismissGlobalSearch() - case .select(let roomID): - dismissGlobalSearch() - handleAppRoute(.room(roomID: roomID, via: []), animated: true) - actionsSubject.send(.switchToChatsTab) - } - } - .store(in: &cancellables) - - let hostingController = UIHostingController(rootView: coordinator.toPresentable()) - hostingController.view.backgroundColor = .clear - flowParameters.windowManager.globalSearchWindow.rootViewController = hostingController - - flowParameters.windowManager.showGlobalSearch() - } - - private func dismissGlobalSearch() { - flowParameters.windowManager.globalSearchWindow.rootViewController = nil - flowParameters.windowManager.hideGlobalSearch() - - globalSearchScreenCoordinator = nil - } - // MARK: Room Directory Search private func presentRoomDirectorySearch() { diff --git a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift index e6dd287d2..a783442eb 100644 --- a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift @@ -89,7 +89,7 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol { .roomDetails, .roomMemberDetails, .userProfile, .thread, .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, .call, .settings, .share, .transferOwnership, - .globalSearch: + .search: // These routes aren't in this flow so clear the entire stack. clearRoute(animated: animated) case .chatBackupSettings: diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index f7bba9cad..af8c4b4e1 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -208,7 +208,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } case .roomAlias, .childRoomAlias, .eventOnRoomAlias, .childEventOnRoomAlias: break // These are converted to a room ID route one level above. - case .accountProvisioningLink, .oAuthCallback, .roomList, .userProfile, .call, .settings, .chatBackupSettings, .globalSearch: + case .accountProvisioningLink, .oAuthCallback, .roomList, .userProfile, .call, .settings, .chatBackupSettings, .search: break // These routes can't be handled. case .transferOwnership(let roomID): guard self.roomID == roomID else { fatalError("Navigation route doesn't belong to this room flow.") } diff --git a/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift index 7c3298875..5de13ff4a 100644 --- a/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift @@ -123,7 +123,7 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol { case .accountProvisioningLink, .oAuthCallback, .roomList, .room, .roomDetails, .event, .userProfile, .call, .settings, .chatBackupSettings, - .share, .transferOwnership, .thread, .globalSearch: + .share, .transferOwnership, .thread, .search: break } } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index f6555662b..0da890f78 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -20,7 +20,7 @@ enum UserSessionFlowCoordinatorAction { } class UserSessionFlowCoordinator: FlowCoordinatorProtocol { - enum HomeTab: Hashable { case chats, spaces } + enum HomeTab: Hashable { case chats, spaces, search } private let navigationRootCoordinator: NavigationRootCoordinator private let navigationTabCoordinator: NavigationTabCoordinator @@ -38,6 +38,10 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { private let spacesTabFlowCoordinator: SpacesTabFlowCoordinator private let spacesTabDetails: NavigationTabCoordinator.TabDetails + private let searchScreenCoordinator: SearchScreenCoordinator? + private let searchTabNavigationStackCoordinator: NavigationStackCoordinator? + private let searchTabDetails: NavigationTabCoordinator.TabDetails? + // periphery:ignore - retaining purpose private var settingsFlowCoordinator: SettingsFlowCoordinator? @@ -92,16 +96,35 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { spacesTabDetails = .init(tag: HomeTab.spaces, title: L10n.screenHomeTabSpaces, icon: \.space, selectedIcon: \.spaceSolid) spacesTabDetails.navigationSplitCoordinator = spacesSplitCoordinator + if flowParameters.appSettings.globalSearchEnabled, #available(iOS 26.0, *) { + let searchCoordinator = SearchScreenCoordinator(parameters: .init(roomSummaryProvider: flowParameters.userSession.clientProxy.alternateRoomSummaryProvider, + mediaProvider: flowParameters.userSession.mediaProvider)) + let searchStackCoordinator = NavigationStackCoordinator() + searchStackCoordinator.setRootCoordinator(searchCoordinator) + + searchScreenCoordinator = searchCoordinator + searchTabNavigationStackCoordinator = searchStackCoordinator + searchTabDetails = .init(tag: HomeTab.search, title: UntranslatedL10n.screenHomeTabSearch, icon: \.search, selectedIcon: \.search, isSearch: true) + } else { + searchScreenCoordinator = nil + searchTabNavigationStackCoordinator = nil + searchTabDetails = nil + } + onboardingStackCoordinator = NavigationStackCoordinator() onboardingFlowCoordinator = OnboardingFlowCoordinator(isNewLogin: isNewLogin, appLockService: appLockService, navigationStackCoordinator: onboardingStackCoordinator, flowParameters: flowParameters) - navigationTabCoordinator.setTabs([ + var tabs: [NavigationTabCoordinator.Tab] = [ .init(coordinator: chatsSplitCoordinator, details: chatsTabDetails), .init(coordinator: spacesSplitCoordinator, details: spacesTabDetails) - ]) + ] + if let searchTabNavigationStackCoordinator, let searchTabDetails { + tabs.append(.init(coordinator: searchTabNavigationStackCoordinator, details: searchTabDetails)) + } + navigationTabCoordinator.setTabs(tabs) stateMachine = flowParameters.stateMachineFactory.makeUserSessionFlowStateMachine(state: .initial) configureStateMachine() @@ -137,12 +160,17 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, .roomDetails, .roomMemberDetails, .userProfile, .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, - .share, .transferOwnership, .thread, .globalSearch: + .share, .transferOwnership, .thread: clearPresentedSheets(animated: animated) // Make sure the presented route is visible. chatsTabFlowCoordinator.handleAppRoute(appRoute, animated: animated) if navigationTabCoordinator.selectedTab != .chats { navigationTabCoordinator.selectedTab = .chats } + case .search: + // Switch to the dedicated search tab when it's available (iOS 26 + flag), otherwise ignore. + if searchTabNavigationStackCoordinator != nil { + navigationTabCoordinator.selectedTab = .search + } } } @@ -195,6 +223,16 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { } private func setupObservers() { + searchScreenCoordinator?.actionsPublisher + .sink { [weak self] action in + guard let self else { return } + switch action { + case .presentRoom(let roomID): + handleAppRoute(.room(roomID: roomID, via: []), animated: true) + } + } + .store(in: &cancellables) + chatsTabFlowCoordinator.actionsPublisher .sink { [weak self] action in guard let self else { return } diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index ef372cbae..a24c4b413 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -10,6 +10,16 @@ import Foundation // swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces internal nonisolated enum UntranslatedL10n { + /// Search + internal static var screenHomeTabSearch: String { return UntranslatedL10n.tr("Untranslated", "screen_home_tab_search") } + /// Search for rooms + internal static var screenSearchEmptyStateMessage: String { return UntranslatedL10n.tr("Untranslated", "screen_search_empty_state_message") } + /// Start searching... + internal static var screenSearchEmptyStateTitle: String { return UntranslatedL10n.tr("Untranslated", "screen_search_empty_state_title") } + /// There are no results for “%1$@.” Try a new search term. + internal static func screenSearchNoResultsMessage(_ p1: Any) -> String { + return UntranslatedL10n.tr("Untranslated", "screen_search_no_results_message", String(describing: p1)) + } /// Clear all data currently stored on this device? /// Sign in again to access your account data and messages. internal static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 1501ef653..cc79c7eec 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -14961,7 +14961,6 @@ nonisolated class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol, @unche nonisolated class WindowManagerMock: WindowManagerProtocol, @unchecked Sendable { nonisolated(unsafe) var mainWindow: UIWindow! nonisolated(unsafe) var overlayWindow: UIWindow! - nonisolated(unsafe) var globalSearchWindow: UIWindow! nonisolated(unsafe) var alternateWindow: UIWindow! nonisolated(unsafe) var windows: [UIWindow] = [] var secondaryWindowsEnabled: Bool { @@ -14970,40 +14969,6 @@ nonisolated class WindowManagerMock: WindowManagerProtocol, @unchecked Sendable } nonisolated(unsafe) var underlyingSecondaryWindowsEnabled: Bool! - //MARK: - showGlobalSearch - - private let showGlobalSearchCallsCountLock = NSLock() - private nonisolated(unsafe) var showGlobalSearchUnderlyingCallsCount = 0 - var showGlobalSearchCallsCount: Int { - get { showGlobalSearchCallsCountLock.withLock { showGlobalSearchUnderlyingCallsCount } } - set { showGlobalSearchCallsCountLock.withLock { showGlobalSearchUnderlyingCallsCount = newValue } } - } - var showGlobalSearchCalled: Bool { - return showGlobalSearchCallsCount > 0 - } - nonisolated(unsafe) var showGlobalSearchClosure: (() -> Void)? - - func showGlobalSearch() { - showGlobalSearchCallsCountLock.withLock { showGlobalSearchUnderlyingCallsCount += 1 } - showGlobalSearchClosure?() - } - //MARK: - hideGlobalSearch - - private let hideGlobalSearchCallsCountLock = NSLock() - private nonisolated(unsafe) var hideGlobalSearchUnderlyingCallsCount = 0 - var hideGlobalSearchCallsCount: Int { - get { hideGlobalSearchCallsCountLock.withLock { hideGlobalSearchUnderlyingCallsCount } } - set { hideGlobalSearchCallsCountLock.withLock { hideGlobalSearchUnderlyingCallsCount = newValue } } - } - var hideGlobalSearchCalled: Bool { - return hideGlobalSearchCallsCount > 0 - } - nonisolated(unsafe) var hideGlobalSearchClosure: (() -> Void)? - - func hideGlobalSearch() { - hideGlobalSearchCallsCountLock.withLock { hideGlobalSearchUnderlyingCallsCount += 1 } - hideGlobalSearchClosure?() - } //MARK: - registerCoordinator private let registerCoordinatorFlowCoordinatorForWindowTypeCallsCountLock = NSLock() diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index e3b577bd4..28ef538f7 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -150,9 +150,9 @@ enum RoomAvatarSizeOnScreen { 30 case .timeline, .leaveSpace, .roomDirectorySearch, .completionSuggestions, .authorizedSpaces, .createRoomSelectSpace, - .spaceFilters: + .spaceFilters, .globalSearch: 32 - case .messageForwarding, .globalSearch, .roomSelection, + case .messageForwarding, .roomSelection, .spaceAddRooms: 36 case .chats, .spaces, .spaceSettings, diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index d4862ffd8..5ffd9b792 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -52,8 +52,6 @@ enum TestablePreviewsDictionary { "FormattedBodyText_Previews" : FormattedBodyText_Previews.self, "FormattingToolbar_Previews" : FormattingToolbar_Previews.self, "FullscreenDialog_Previews" : FullscreenDialog_Previews.self, - "GlobalSearchScreenListRow_Previews" : GlobalSearchScreenListRow_Previews.self, - "GlobalSearchScreen_Previews" : GlobalSearchScreen_Previews.self, "HighlightedTimelineItemModifier_Previews" : HighlightedTimelineItemModifier_Previews.self, "HomeScreenEmptyStateView_Previews" : HomeScreenEmptyStateView_Previews.self, "HomeScreenInviteCell_Previews" : HomeScreenInviteCell_Previews.self, @@ -160,6 +158,7 @@ enum TestablePreviewsDictionary { "RoomSelectionScreen_Previews" : RoomSelectionScreen_Previews.self, "RoomThreadListScreen_Previews" : RoomThreadListScreen_Previews.self, "SFNumberedListView_Previews" : SFNumberedListView_Previews.self, + "SearchScreen_Previews" : SearchScreen_Previews.self, "SecureBackupKeyBackupScreen_Previews" : SecureBackupKeyBackupScreen_Previews.self, "SecureBackupLogoutConfirmationScreen_Previews" : SecureBackupLogoutConfirmationScreen_Previews.self, "SecureBackupRecoveryKeyScreen_Previews" : SecureBackupRecoveryKeyScreen_Previews.self, diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenCoordinator.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenCoordinator.swift deleted file mode 100644 index 69c5cfd35..000000000 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenCoordinator.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 2023-2025 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Combine -import Compound -import SwiftUI - -struct GlobalSearchScreenCoordinatorParameters { - let roomSummaryProvider: RoomSummaryProviderProtocol - let mediaProvider: MediaProviderProtocol -} - -enum GlobalSearchControllerAction { - case dismiss - case select(roomID: String) -} - -class GlobalSearchScreenCoordinator: CoordinatorProtocol { - private let viewModel: GlobalSearchScreenViewModelProtocol - - private var cancellables = Set() - - private let actionsSubject: PassthroughSubject = .init() - var actions: AnyPublisher { - actionsSubject.eraseToAnyPublisher() - } - - init(parameters: GlobalSearchScreenCoordinatorParameters) { - viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: parameters.roomSummaryProvider, - mediaProvider: parameters.mediaProvider) - - viewModel.actions - .sink { [weak self] action in - guard let self else { return } - - switch action { - case .dismiss: - actionsSubject.send(.dismiss) - case .select(let roomID): - actionsSubject.send(.select(roomID: roomID)) - } - } - .store(in: &cancellables) - } - - func toPresentable() -> AnyView { - AnyView(GlobalSearchScreen(context: viewModel.context)) - } - - func stop() { - viewModel.stop() - } -} diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift deleted file mode 100644 index 67a890ebf..000000000 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenModels.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 2022-2025 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Foundation - -enum GlobalSearchScreenViewModelAction { - case dismiss - case select(roomID: String) -} - -struct GlobalSearchScreenViewState: BindableState { - var rooms = [GlobalSearchRoom]() - var bindings: GlobalSearchScreenViewStateBindings -} - -struct GlobalSearchScreenViewStateBindings { - var searchQuery: String -} - -enum GlobalSearchScreenViewAction { - case dismiss - case select(roomID: String) - case reachedTop - case reachedBottom -} - -struct GlobalSearchRoom: Identifiable, Equatable { - let id: String - let title: String - let description: String - let avatar: RoomAvatar -} diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift deleted file mode 100644 index c78269b6a..000000000 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 2022-2025 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Combine -import SwiftUI - -typealias GlobalSearchScreenViewModelType = StateStoreViewModel - -class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearchScreenViewModelProtocol { - private let roomSummaryProvider: RoomSummaryProviderProtocol - - private var actionsSubject: PassthroughSubject = .init() - var actions: AnyPublisher { - actionsSubject.eraseToAnyPublisher() - } - - init(roomSummaryProvider: RoomSummaryProviderProtocol, - mediaProvider: MediaProviderProtocol) { - self.roomSummaryProvider = roomSummaryProvider - - super.init(initialViewState: GlobalSearchScreenViewState(bindings: .init(searchQuery: "")), - mediaProvider: mediaProvider) - - roomSummaryProvider.roomListPublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] summaries in - self?.updateRooms(with: summaries) - } - .store(in: &cancellables) - - context.$viewState - .map(\.bindings.searchQuery) - .removeDuplicates() - .sink { [weak self] searchQuery in - if searchQuery.isEmpty { - self?.roomSummaryProvider.setFilter(.all(filters: [])) - } else { - self?.roomSummaryProvider.setFilter(.search(query: searchQuery)) - } - } - .store(in: &cancellables) - - updateRooms(with: roomSummaryProvider.roomListPublisher.value) - } - - func stop() { - // This is a shared provider so we should reset the filtering when we are done with the view - roomSummaryProvider.setFilter(.all(filters: [])) - } - - // MARK: - Public - - override func process(viewAction: GlobalSearchScreenViewAction) { - MXLog.info("View model: received view action: \(viewAction)") - - switch viewAction { - case .dismiss: - actionsSubject.send(.dismiss) - case .select(let roomID): - actionsSubject.send(.select(roomID: roomID)) - case .reachedTop: - updateVisibleRange(edge: .top) - case .reachedBottom: - updateVisibleRange(edge: .bottom) - } - } - - // MARK: - Private - - private func updateRooms(with summaries: [RoomSummary]) { - state.rooms = summaries.compactMap { summary in - GlobalSearchRoom(id: summary.id, - title: summary.name, - description: summary.roomListDescription, - avatar: summary.avatar) - } - } - - /// The actual range values don't matter as long as they contain the lower - /// or upper bounds. updateVisibleRange is a hybrid API that powers both - /// sliding sync visible range update and list paginations - /// For lists other than the home screen one we don't care about visible ranges, - /// we just need the respective bounds to be there to trigger a next page load or - /// a reset to just one page - private func updateVisibleRange(edge: UIRectEdge) { - switch edge { - case .top: - roomSummaryProvider.updateVisibleRange(0..<0) - case .bottom: - let roomCount = roomSummaryProvider.roomListPublisher.value.count - roomSummaryProvider.updateVisibleRange(roomCount.. { get } - var context: GlobalSearchScreenViewModelType.Context { get } - - func stop() -} diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift deleted file mode 100644 index 24e4ac9dd..000000000 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 2022-2025 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Compound -import SwiftUI - -struct GlobalSearchScreen: View { - @ObservedObject var context: GlobalSearchScreenViewModel.Context - - @State private var selectedRoom: GlobalSearchRoom? - @FocusState private var searchFieldFocus - - var body: some View { - List { - header - - Section { - ForEach(context.viewState.rooms) { room in - GlobalSearchScreenListRow(room: room, context: context) - .listRowBackground(backgroundColor(for: room)) - .listRowInsets(.init()) - .contentShape(.rect) - .onTapGesture { - context.send(viewAction: .select(roomID: room.id)) - } - .onAppear { - if room == context.viewState.rooms.first { - context.send(viewAction: .reachedTop) - } else if room == context.viewState.rooms.last { - context.send(viewAction: .reachedBottom) - } - } - } - } - } - .listStyle(.plain) - .frame(maxWidth: 700, maxHeight: 800) - .background(.compound.bgCanvasDefault) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.black.opacity(0.5).ignoresSafeArea()) - .background { keyboardShortcuts } - .onAppear { - selectedRoom = context.viewState.rooms.first - searchFieldFocus = true - } - .onChange(of: context.viewState.rooms) { - selectedRoom = context.viewState.rooms.first - } - .onTapGesture { - context.send(viewAction: .dismiss) - } - } - - private var header: some View { - GlobalSearchTextFieldRepresentable(placeholder: L10n.actionSearch, text: $context.searchQuery) { keyCode in - switch keyCode { - case .keyboardUpArrow: - moveToNextEntry(backwards: true) - return true - case .keyboardDownArrow: - moveToNextEntry() - return true - case .keyboardReturnOrEnter, .keyboardReturn: - if let selectedRoom { - context.send(viewAction: .select(roomID: selectedRoom.id)) - } - return true - case .keyboardEscape: - context.send(viewAction: .dismiss) - return true - default: - return false - } - } endEditingHandler: { - if let selectedRoom { - context.send(viewAction: .select(roomID: selectedRoom.id)) - } else { // Bring the focus back to the text field - searchFieldFocus = true - } - } - .focused($searchFieldFocus) - .autocorrectionDisabled(true) - .autocapitalization(.none) - .textInputAutocapitalization(.never) - } - - private var keyboardShortcuts: some View { - Button("") { - context.send(viewAction: .dismiss) - } - // Need this to enable escape on the textField and forward the presses - .keyboardShortcut(.escape, modifiers: []) - } - - private func backgroundColor(for room: GlobalSearchRoom) -> Color { - if selectedRoom == room { - .compound.bgSubtlePrimary - } else { - .compound.bgCanvasDefault - } - } - - private func moveToNextEntry(backwards: Bool = false) { - guard let selectedRoom else { - selectedRoom = context.viewState.rooms.first - return - } - - guard let currentIndex = context.viewState.rooms.firstIndex(of: selectedRoom) else { - return - } - - let nextIndex = (backwards ? currentIndex - 1 : currentIndex + 1) - - guard context.viewState.rooms.indices.contains(nextIndex) else { - return - } - - self.selectedRoom = context.viewState.rooms[nextIndex] - } -} - -private struct GlobalSearchTextFieldRepresentable: UIViewRepresentable { - let placeholder: String - @Binding var text: String - let keyPressHandler: (UIKeyboardHIDUsage) -> Bool - let endEditingHandler: () -> Void - - func makeUIView(context: Context) -> UITextField { - let textField = GlobalSearchTextField(keyPressHandler: keyPressHandler) - textField.delegate = context.coordinator - textField.autocorrectionType = .no - textField.placeholder = placeholder - return textField - } - - func updateUIView(_ uiView: UITextField, context: Context) { - uiView.text = text - } - - func makeCoordinator() -> Coordinator { - Coordinator(text: $text, endEditingHandler: endEditingHandler) - } - - class Coordinator: NSObject, UITextFieldDelegate { - var text: Binding - let endEditingHandler: () -> Void - - init(text: Binding, endEditingHandler: @escaping () -> Void) { - self.text = text - self.endEditingHandler = endEditingHandler - } - - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - // pressesBegan sometimes doesn't receive return events. Handle it here instead - if string.rangeOfCharacter(from: .newlines) != nil { - endEditingHandler() - return false - } - - let currentText = textField.text ?? "" - DispatchQueue.main.async { - self.text.wrappedValue = (currentText as NSString).replacingCharacters(in: range, with: string) - } - return true - } - } -} - -private class GlobalSearchTextField: UITextField { - let keyPressHandler: (UIKeyboardHIDUsage) -> Bool - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError() - } - - init(keyPressHandler: @escaping (UIKeyboardHIDUsage) -> Bool) { - self.keyPressHandler = keyPressHandler - super.init(frame: .zero) - } - - override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { - guard let key = presses.first?.key else { - super.pressesBegan(presses, with: event) - return - } - - if keyPressHandler(key.keyCode) { - return - } - - super.pressesBegan(presses, with: event) - } -} - -// MARK: - Previews - -struct GlobalSearchScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), - mediaProvider: MediaProviderMock(.init())) - - static var previews: some View { - ElementNavigationStack { - GlobalSearchScreen(context: viewModel.context) - } - } -} diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift deleted file mode 100644 index f69e2bc2c..000000000 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreenCell.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 2024-2025 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Compound -import SwiftUI - -struct GlobalSearchScreenListRow: View { - @Environment(\.dynamicTypeSize) var dynamicTypeSize - - let room: GlobalSearchRoom - let context: GlobalSearchScreenViewModel.Context - - var body: some View { - ZStack { // The list row swallows listRowBackgrounds for some reason - ListRow(label: .avatar(title: room.title, - description: room.description, - icon: avatar), - kind: .label) - } - } - - @ViewBuilder - var avatar: some View { - if dynamicTypeSize < .accessibility3 { - RoomAvatarImage(avatar: room.avatar, - avatarSize: .room(on: .messageForwarding), - mediaProvider: context.mediaProvider) - .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) - .accessibilityHidden(true) - } - } -} - -struct GlobalSearchScreenListRow_Previews: PreviewProvider, TestablePreview { - static let viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), - mediaProvider: MediaProviderMock(.init())) - - static var previews: some View { - List { - GlobalSearchScreenListRow(room: .init(id: "123", - title: "Tech central", - description: "The best place in the whole wide world", - avatar: .room(id: "123", - name: "Tech central", - avatarURL: .mockMXCAvatar)), - context: viewModel.context) - } - } -} diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 24936d38b..b741f3491 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -115,6 +115,9 @@ struct HomeScreenViewState: BindableState { var shouldShowSpaceFilters = false var selectedSpaceFilter: SpaceServiceFilter? + /// Inline room list search is disabled when the dedicated global search tab is shown instead (see `UserSessionFlowCoordinator`). + var isRoomListSearchEnabled = true + var visibleRooms: [HomeScreenRoom] { if roomListMode == .skeletons { return placeholderRooms diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index beccd67f9..4760859c3 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -49,6 +49,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol bindings: .init(filtersState: .init(appSettings: appSettings))), mediaProvider: userSession.mediaProvider) + if appSettings.globalSearchEnabled, #available(iOS 26.0, *) { + state.isRoomListSearchEnabled = false + } + userSession.clientProxy.userAvatarURLPublisher .receive(on: DispatchQueue.main) .weakAssign(to: \.state.userAvatarURL, on: self) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift index 1715dd028..50716ff96 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift @@ -59,10 +59,9 @@ struct HomeScreenContent: View { topSection } } - .isSearching($context.isSearchFieldFocused) - .searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always)) - .compoundSearchField() - .disableAutocorrection(true) + .roomListSearchable(isEnabled: context.viewState.isRoomListSearchEnabled, + isSearchFieldFocused: $context.isSearchFieldFocused, + searchQuery: $context.searchQuery) } } .introspect(.scrollView, on: .supportedVersions) { scrollView in @@ -161,3 +160,17 @@ struct HomeScreenContent: View { context.send(viewAction: .updateVisibleItemRange(firstIndex.., searchQuery: Binding) -> some View { + if isEnabled { + isSearching(isSearchFieldFocused) + .searchable(text: searchQuery, placement: .navigationBarDrawer(displayMode: .always)) + .compoundSearchField() + .disableAutocorrection(true) + } else { + self + } + } +} diff --git a/ElementX/Sources/Screens/SearchScreen/SearchScreenCoordinator.swift b/ElementX/Sources/Screens/SearchScreen/SearchScreenCoordinator.swift new file mode 100644 index 000000000..e663ed8d1 --- /dev/null +++ b/ElementX/Sources/Screens/SearchScreen/SearchScreenCoordinator.swift @@ -0,0 +1,49 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import SwiftUI + +struct SearchScreenCoordinatorParameters { + let roomSummaryProvider: RoomSummaryProviderProtocol + let mediaProvider: MediaProviderProtocol +} + +enum SearchScreenCoordinatorAction { + case presentRoom(roomID: String) +} + +final class SearchScreenCoordinator: CoordinatorProtocol { + private let viewModel: SearchScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: SearchScreenCoordinatorParameters) { + viewModel = SearchScreenViewModel(roomSummaryProvider: parameters.roomSummaryProvider, + mediaProvider: parameters.mediaProvider) + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .presentRoom(let roomID): + actionsSubject.send(.presentRoom(roomID: roomID)) + } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(SearchScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/SearchScreen/SearchScreenModels.swift b/ElementX/Sources/Screens/SearchScreen/SearchScreenModels.swift new file mode 100644 index 000000000..d8f423eb6 --- /dev/null +++ b/ElementX/Sources/Screens/SearchScreen/SearchScreenModels.swift @@ -0,0 +1,39 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation + +enum SearchScreenViewModelAction { + case presentRoom(roomID: String) +} + +struct SearchScreenViewState: BindableState { + var rooms = [SearchScreenRoom]() + var bindings: SearchScreenViewStateBindings + + var isSearching: Bool { + !bindings.searchQuery.isEmpty + } +} + +struct SearchScreenViewStateBindings { + var searchQuery = "" +} + +enum SearchScreenViewAction { + case appeared + case selectRoom(roomID: String) + case reachedTop + case reachedBottom +} + +struct SearchScreenRoom: Identifiable, Equatable { + let id: String + let title: String + let description: String + let avatar: RoomAvatar +} diff --git a/ElementX/Sources/Screens/SearchScreen/SearchScreenViewModel.swift b/ElementX/Sources/Screens/SearchScreen/SearchScreenViewModel.swift new file mode 100644 index 000000000..05a7baf2d --- /dev/null +++ b/ElementX/Sources/Screens/SearchScreen/SearchScreenViewModel.swift @@ -0,0 +1,115 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import AsyncAlgorithms +import Combine +import SwiftUI + +typealias SearchScreenViewModelType = StateStoreViewModelV2 + +class SearchScreenViewModel: SearchScreenViewModelType, SearchScreenViewModelProtocol { + private let roomSummaryProvider: RoomSummaryProviderProtocol + private var searchQueryObservationTask: Task? + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(roomSummaryProvider: RoomSummaryProviderProtocol, + mediaProvider: MediaProviderProtocol, + initialSearchQuery: String = "") { + self.roomSummaryProvider = roomSummaryProvider + + super.init(initialViewState: SearchScreenViewState(bindings: .init(searchQuery: initialSearchQuery)), + mediaProvider: mediaProvider) + + roomSummaryProvider.roomListPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] summaries in + self?.updateRooms(with: summaries) + } + .store(in: &cancellables) + + searchQueryObservationTask = Task { [weak self] in + guard let stream = self?.context.observe(\.viewState.bindings.searchQuery).removeDuplicates() else { return } + for await searchQuery in stream { + self?.updateFilter(for: searchQuery) + } + } + + updateRooms(with: roomSummaryProvider.roomListPublisher.value) + } + + isolated deinit { + searchQueryObservationTask?.cancel() + } + + // MARK: - Public + + override func process(viewAction: SearchScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .appeared: + // The provider is shared, so other consumers may have changed its filter while we were off-screen. + // Re-apply ours on every appearance to keep the displayed results in sync with the query. + updateFilter(for: state.bindings.searchQuery) + case .selectRoom(let roomID): + actionsSubject.send(.presentRoom(roomID: roomID)) + case .reachedTop: + updateVisibleRange(edge: .top) + case .reachedBottom: + updateVisibleRange(edge: .bottom) + } + } + + // MARK: - Private + + private func updateFilter(for searchQuery: String) { + if searchQuery.isEmpty { + roomSummaryProvider.setFilter(.excludeAll) + } else { + roomSummaryProvider.setFilter(.search(query: searchQuery)) + } + } + + private func updateRooms(with summaries: [RoomSummary]) { + state.rooms = summaries.map { summary in + // Show the matrix identifier as the subtitle: the other member's user ID for DMs, + // otherwise the room's canonical alias. + let identifier = if summary.isDirect { + summary.heroes.first?.userID ?? summary.canonicalAlias + } else { + summary.canonicalAlias + } + + return SearchScreenRoom(id: summary.id, + title: summary.name, + description: identifier ?? "", + avatar: summary.avatar) + } + } + + /// The actual range values don't matter as long as they contain the lower + /// or upper bounds. updateVisibleRange is a hybrid API that powers both + /// sliding sync visible range update and list paginations. + /// For lists other than the home screen one we don't care about visible ranges, + /// we just need the respective bounds to be there to trigger a next page load or + /// a reset to just one page. + private func updateVisibleRange(edge: UIRectEdge) { + switch edge { + case .top: + roomSummaryProvider.updateVisibleRange(0..<0) + case .bottom: + let roomCount = roomSummaryProvider.roomListPublisher.value.count + roomSummaryProvider.updateVisibleRange(roomCount.. { get } + var context: SearchScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/SearchScreen/View/SearchScreen.swift b/ElementX/Sources/Screens/SearchScreen/View/SearchScreen.swift new file mode 100644 index 000000000..8273ba77b --- /dev/null +++ b/ElementX/Sources/Screens/SearchScreen/View/SearchScreen.swift @@ -0,0 +1,263 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import GameController +import SwiftUI +import SwiftUIIntrospect + +struct SearchScreen: View { + @Bindable var context: SearchScreenViewModel.Context + + @FocusState private var isSearchFieldFocused: Bool + /// The room highlighted for hardware keyboard selection (arrow keys + return). + @State private var selectedRoomID: String? + /// The selection is only meaningful with a hardware keyboard, so don't highlight anything otherwise. + @State private var isHardwareKeyboardConnected = false + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + // Rendered as content rather than a navigation title so it stays visible while the search field is focused. + Text(L10n.actionSearch) + .font(.compound.headingXLBold) + .foregroundStyle(.compound.textPrimary) + .accessibilityAddTraits(.isHeader) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.vertical, 8) + + if context.viewState.rooms.isEmpty { + emptyState + } else { + roomList + } + } + .frame(maxHeight: .infinity, alignment: .top) + .background(Color.compound.bgCanvasDefault) + .searchable(text: $context.searchQuery, placement: .toolbarPrincipal) + .searchFocused($isSearchFieldFocused) + .autocorrectionDisabled(true) + .onSubmit(of: .search) { + if let selectedRoomID { + context.send(viewAction: .selectRoom(roomID: selectedRoomID)) + } + } + .searchResultsKeyboardNavigation(moveUp: { moveSelection(backwards: true) }, + moveDown: { moveSelection(backwards: false) }) + // The TabView calls onAppear each time the search tab is selected, so the field re-focuses + // and the selection resets on every switch. + .onAppear { + context.send(viewAction: .appeared) + isSearchFieldFocused = true + selectedRoomID = context.viewState.rooms.first?.id + updateHardwareKeyboardConnected() + } + .onReceive(NotificationCenter.default.publisher(for: .GCKeyboardDidConnect)) { _ in + updateHardwareKeyboardConnected() + } + .onReceive(NotificationCenter.default.publisher(for: .GCKeyboardDidDisconnect)) { _ in + updateHardwareKeyboardConnected() + } + // Reset to the first result when the results change (e.g. a new query), keyed on the top result + // so pagination and background updates that leave it untouched don't clobber the selection. + .onChange(of: context.viewState.rooms.first?.id) { + selectedRoomID = context.viewState.rooms.first?.id + } + } + + @ViewBuilder + private var emptyState: some View { + if context.viewState.isSearching { + TitleAndIcon(title: L10n.commonNoResults, + subtitle: UntranslatedL10n.screenSearchNoResultsMessage(context.searchQuery), + icon: \.search, + iconStyle: .defaultSolid) + .frame(maxWidth: .infinity) + .padding(40) + } else { + TitleAndIcon(title: UntranslatedL10n.screenSearchEmptyStateTitle, + subtitle: UntranslatedL10n.screenSearchEmptyStateMessage, + icon: \.search, + iconStyle: .defaultSolid) + .frame(maxWidth: .infinity) + .padding(40) + } + } + + private var roomList: some View { + List { + ForEach(context.viewState.rooms) { room in + SearchScreenRoomCell(room: room, + context: context, + isLast: room == context.viewState.rooms.last, + isSelected: isHardwareKeyboardConnected && selectedRoomID == room.id) + .onAppear { + if room == context.viewState.rooms.first { + context.send(viewAction: .reachedTop) + } else if room == context.viewState.rooms.last { + context.send(viewAction: .reachedBottom) + } + } + } + } + .compoundList(.plain) + } + + private func moveSelection(backwards: Bool) { + let rooms = context.viewState.rooms + guard let selectedRoomID, + let currentIndex = rooms.firstIndex(where: { $0.id == selectedRoomID }) + else { + selectedRoomID = rooms.first?.id + return + } + + let nextIndex = backwards ? currentIndex - 1 : currentIndex + 1 + guard rooms.indices.contains(nextIndex) else { return } + self.selectedRoomID = rooms[nextIndex].id + } + + private func updateHardwareKeyboardConnected() { + isHardwareKeyboardConnected = GCKeyboard.coalesced != nil + } +} + +private struct SearchScreenRoomCell: View { + let room: SearchScreenRoom + let context: SearchScreenViewModel.Context + let isLast: Bool + let isSelected: Bool + + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + + var body: some View { + Button { + context.send(viewAction: .selectRoom(roomID: room.id)) + } label: { + HStack(spacing: 12) { + avatar + + VStack(alignment: .leading, spacing: 0) { + Text(room.title) + .font(.compound.bodyLG) + .foregroundStyle(.compound.textPrimary) + .lineLimit(1) + + if !room.description.isEmpty { + Text(room.description) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + .lineLimit(1) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + } + .buttonStyle(SearchScreenRoomCellButtonStyle(isSelected: isSelected)) + .listRowInsets(.init()) + .listRowSeparator(.hidden) + .rowDivider() + } + + @ViewBuilder + private var avatar: some View { + if dynamicTypeSize < .accessibility3 { + RoomAvatarImage(avatar: room.avatar, + avatarSize: .room(on: .globalSearch), + mediaProvider: context.mediaProvider) + .dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1) + .accessibilityHidden(true) + } + } +} + +private struct SearchScreenRoomCellButtonStyle: ButtonStyle { + let isSelected: Bool + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .background(isSelected || configuration.isPressed ? Color.compound.bgSubtleSecondary : Color.compound.bgCanvasDefault) + .contentShape(Rectangle()) + .animation(isSelected ? .none : .easeOut(duration: 0.1).disabledDuringTests(), value: isSelected) + } +} + +private extension View { + /// Forwards hardware up/down arrow presses from the system search field to the given closures. + /// + /// The `.search` role tab owns its text field, so there's no SwiftUI hook for its key presses. We reach the + /// field through the navigation stack (as ``SearchFieldStyle`` does) and swap its class for one that overrides + /// `pressesBegan` — the same key interception the old global search performed on its own field. + func searchResultsKeyboardNavigation(moveUp: @escaping () -> Void, moveDown: @escaping () -> Void) -> some View { + introspect(.navigationStack, on: .supportedVersions, scope: .ancestor) { navigationController in + guard let textField = navigationController.navigationBar.topItem?.searchController?.searchBar.searchTextField else { + return + } + + if !(textField is KeyNavigatingSearchTextField) { + object_setClass(textField, KeyNavigatingSearchTextField.self) + } + + KeyNavigatingSearchTextField.keyHandler = { keyCode in + switch keyCode { + case .keyboardUpArrow: + moveUp() + case .keyboardDownArrow: + moveDown() + default: + return false + } + return true + } + } + } +} + +/// A `UISearchTextField` whose class is installed at runtime via `object_setClass`, so it must not declare any +/// stored properties. The handler is static, which is fine as there's only ever a single search field. +private final class KeyNavigatingSearchTextField: UISearchTextField { + static var keyHandler: ((UIKeyboardHIDUsage) -> Bool)? + + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { + if let keyCode = presses.first?.key?.keyCode, Self.keyHandler?(keyCode) == true { + return + } + super.pressesBegan(presses, with: event) + } +} + +// MARK: - Previews + +struct SearchScreen_Previews: PreviewProvider, TestablePreview { + static let emptyViewModel = SearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded([]))), + mediaProvider: MediaProviderMock(.init())) + static let loadedViewModel = SearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), + mediaProvider: MediaProviderMock(.init()), + initialSearchQuery: "Foundation") + static let noResultsViewModel = SearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded([]))), + mediaProvider: MediaProviderMock(.init()), + initialSearchQuery: "John Doe") + + static var previews: some View { + ElementNavigationStack { + SearchScreen(context: emptyViewModel.context) + } + .previewDisplayName("Empty") + + ElementNavigationStack { + SearchScreen(context: noResultsViewModel.context) + } + .previewDisplayName("No results") + + ElementNavigationStack { + SearchScreen(context: loadedViewModel.context) + } + .previewDisplayName("Loaded") + } +} diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 31d4535dc..3a47c7fa9 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -74,6 +74,8 @@ protocol DeveloperOptionsProtocol: AnyObject { var linkNewDeviceEnabled: Bool { get set } var roomThreadListEnabled: Bool { get set } + + var globalSearchEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index c9a55adc2..a46932155 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -49,6 +49,11 @@ struct DeveloperOptionsScreen: View { Text("Link new device with QR code") } + Toggle(isOn: $context.globalSearchEnabled) { + Text("Global search") + Text("Moves search to a separate tab") + } + context.viewState.appHooks .developerOptionsScreenHook .generalSectionRows() diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index f0b679814..ebf7a1e8b 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -121,7 +121,7 @@ extension RoomSummary: CustomStringConvertible { extension RoomSummary { /// An empty summary used when the room info is momentarily unavailable (e.g. while the client tears /// down on logout). Keeps the diff indices aligned with the SDK instead of crashing. - static func placeholder(room: Room) -> RoomSummary { + nonisolated static func placeholder(room: Room) -> RoomSummary { RoomSummary(room: room, id: room.id(), joinRequestType: nil, diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 3a298a916..9430626f3 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -312,20 +312,6 @@ extension PreviewTests { } } - @Test - func globalSearchScreenListRow() async throws { - for (index, preview) in GlobalSearchScreenListRow_Previews._allPreviews.enumerated() { - try await assertSnapshots(matching: preview, step: index) - } - } - - @Test - func globalSearchScreen() async throws { - for (index, preview) in GlobalSearchScreen_Previews._allPreviews.enumerated() { - try await assertSnapshots(matching: preview, step: index) - } - } - @Test func highlightedTimelineItemModifier() async throws { for (index, preview) in HighlightedTimelineItemModifier_Previews._allPreviews.enumerated() { @@ -1068,6 +1054,13 @@ extension PreviewTests { } } + @Test + func searchScreen() async throws { + for (index, preview) in SearchScreen_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + @Test func secureBackupKeyBackupScreen() async throws { for (index, preview) in SecureBackupKeyBackupScreen_Previews._allPreviews.enumerated() { diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png deleted file mode 100644 index 061669913..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-en-GB-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2e96b757eacb7be702a40a8f9494f8900e2ada38838c78a38190a3837d56a2ec -size 171848 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png deleted file mode 100644 index a27319c3f..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPad-pseudo-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:456fe50194ca5e15d431437a6342bc0664266cdc54d8084dd18639980cf0eb60 -size 172156 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-en-GB-0.png deleted file mode 100644 index 9b8006abe..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-en-GB-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:611812894a3f67b80d5f32a9192eb787262c4c05fb869a79ceb561ac88865047 -size 106611 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-pseudo-0.png deleted file mode 100644 index a670d3f9b..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreen.iPhone-pseudo-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80c1dffec6a31d5383f694ad063dbb1f6614f4c8da21b94e8d4b587fa355b706 -size 106942 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPad-en-GB-0.png deleted file mode 100644 index 077de0b19..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPad-en-GB-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b9d73c339d27730080a8620d3c3dfee9e8bb05f22a911d30a57bcab71fb18a1 -size 98755 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPad-pseudo-0.png deleted file mode 100644 index 077de0b19..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPad-pseudo-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b9d73c339d27730080a8620d3c3dfee9e8bb05f22a911d30a57bcab71fb18a1 -size 98755 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPhone-en-GB-0.png deleted file mode 100644 index cf52f1455..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPhone-en-GB-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b47056a891a64a3c4a5ead1161a98ca8e16319727cb8962f95f8a8ddc47e2a45 -size 50155 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPhone-pseudo-0.png deleted file mode 100644 index cf52f1455..000000000 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/globalSearchScreenListRow.iPhone-pseudo-0.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b47056a891a64a3c4a5ead1161a98ca8e16319727cb8962f95f8a8ddc47e2a45 -size 50155 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPad-en-GB.png new file mode 100644 index 000000000..fd8c9a10a --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a93a6802d8a053abd390e9311921294dfbab18844eb17a63eaf4b8c623173a9 +size 85119 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPad-pseudo.png new file mode 100644 index 000000000..cd515fcab --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72835c30b6f459759cbbae44aaba4aed0c39674c49879602c7a8a319323b9db1 +size 88205 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPhone-en-GB.png new file mode 100644 index 000000000..c92f88904 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25f1c159081bfcf7e1fb181bb6f1ee2f614a1ae3759052cc5dac3f7266b271ba +size 42662 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPhone-pseudo.png new file mode 100644 index 000000000..d1ff6de07 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Empty-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:513a8d3815582253be1eecf22a7726c7917c9e83e7465aa5e7b90ef2d7944c1e +size 49713 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPad-en-GB.png new file mode 100644 index 000000000..5406ebdb9 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69ddd3c435d8c50a231bdc128c059127219d35624a16912579ce0f878830a274 +size 148724 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPad-pseudo.png new file mode 100644 index 000000000..c76f439d9 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680e82d9b7877d868e734783ac7d9dc9c615d7c53a80b816e7529cbda6e3be0d +size 149586 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPhone-en-GB.png new file mode 100644 index 000000000..dead27c49 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a419004009267be00c3bb862408e82639e2fb45f6951e77da9309b1245f046d +size 103648 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPhone-pseudo.png new file mode 100644 index 000000000..d1bb683f8 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.Loaded-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a6a0b062ae788f675ada0f9011ff70dea366bf36c6b5d5a13a5bd909a03bc6c +size 104605 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPad-en-GB.png new file mode 100644 index 000000000..dba50b36d --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d6cd51861f45a9e393eb3194ee7224a094646ce4385f2aa8ecb1be404bbaa6d +size 87898 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPad-pseudo.png new file mode 100644 index 000000000..808e93546 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c96db7daacd17e38fb6d0d74cc9de51f52b0c17fb2c02c2c162d8384ca1ab6d2 +size 94243 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPhone-en-GB.png new file mode 100644 index 000000000..c789b0415 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPhone-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31369bff674e454cb3cacdcb7a33efe035c838e4d09b549ba07979597d74a744 +size 49368 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPhone-pseudo.png new file mode 100644 index 000000000..977678ec9 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/searchScreen.No-results-iPhone-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cc94abaa65358e0cf82a0635b0aaa5f7858f0a765b3ddf0f8b647cba493cfe6 +size 58249 diff --git a/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift b/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift deleted file mode 100644 index 55bcbb9ed..000000000 --- a/UnitTests/Sources/GlobalSearchScreenViewModelTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// Copyright 2025 Element Creations Ltd. -// Copyright 2022-2025 New Vector Ltd. -// -// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. -// Please see LICENSE files in the repository root for full details. -// - -import Combine -@testable import ElementX -import Testing - -@MainActor -struct GlobalSearchScreenViewModelTests { - var viewModel: GlobalSearchScreenViewModelProtocol! - var context: GlobalSearchScreenViewModelType.Context! - - init() { - viewModel = GlobalSearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), - mediaProvider: MediaProviderMock(.init())) - context = viewModel.context - } - - @Test - mutating func searching() async throws { - let deferred = deferFulfillment(context.$viewState) { state in - state.rooms.count == 1 - } - - context.searchQuery = "Second" - - try await deferred.fulfill() - } - - @Test - func roomSelection() async throws { - let deferred = deferFulfillment(viewModel.actions) { action in - switch action { - case .select(let roomID): - return roomID == "2" - default: - return false - } - } - - context.send(viewAction: .select(roomID: "2")) - - try await deferred.fulfill() - } -} diff --git a/UnitTests/Sources/SearchScreenViewModelTests.swift b/UnitTests/Sources/SearchScreenViewModelTests.swift new file mode 100644 index 000000000..52c7e76f1 --- /dev/null +++ b/UnitTests/Sources/SearchScreenViewModelTests.swift @@ -0,0 +1,43 @@ +// +// Copyright 2026 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +@testable import ElementX +import Testing + +@MainActor +struct SearchScreenViewModelTests { + let viewModel: SearchScreenViewModelProtocol + var context: SearchScreenViewModelType.Context { + viewModel.context + } + + init() { + viewModel = SearchScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))), + mediaProvider: MediaProviderMock(.init())) + } + + @Test + func searching() async throws { + let deferred = deferFulfillment(context.observe(\.viewState.rooms)) { $0.count == 1 } + context.searchQuery = "Second" + try await deferred.fulfill() + } + + @Test + func roomSelection() async throws { + let deferred = deferFulfillment(viewModel.actionsPublisher) { action in + switch action { + case .presentRoom(let roomID): + roomID == "2" + } + } + + context.send(viewAction: .selectRoom(roomID: "2")) + + try await deferred.fulfill() + } +}