From c41a38159b5364d08e999f32f55efebebfe00a15 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:44:33 +0000 Subject: [PATCH] Only offer joined rooms in the forward and share pickers (#6255) * Initial plan * Offer only joined rooms in destination pickers Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Clean whitespace in share picker test Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> * Restrict the shared picker room list to joined rooms The alternate room summary provider, which backs the forward, share and Add rooms pickers and global search, now always includes the SDK's joined filter in its base filter. This replaces the per-call joinedOnly flags, so Add rooms no longer offers invited rooms or spaces and no screen can forget the flag. A filter set before the provider has its room list is now applied once the list is set, instead of being reset to the default. The share picker hit this on a cold launch from the share extension. The forward and share pickers also hide upgraded rooms, matching the Suggestions section. The SDK has no filter for these. Also registers the share picker's test file in the project, fixes the blank-line indentation SwiftFormat flagged, and re-records the picker snapshots without the upgraded room. * Check room membership for forwarding suggestions and search breadcrumbs RoomSummary now has isJoined, built from the room's membership. The forwarding suggestions and the search breadcrumbs used a missing joinRequestType to mean joined, which is also the case for rooms that have been left or banned from. It only worked because the room list's base filter already drops those. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Velin92 <34335419+Velin92@users.noreply.github.com> Co-authored-by: Mauro Romito --- ElementX.xcodeproj/project.pbxproj | 4 ++ .../Mocks/RoomSummaryProviderMock.swift | 18 ++++++++- .../View/HomeScreenInviteCell.swift | 2 + .../View/HomeScreenKnockedCell.swift | 2 + .../HomeScreen/View/HomeScreenRoomCell.swift | 1 + .../MessageForwardingScreenViewModel.swift | 4 +- .../RoomSelectionScreenViewModel.swift | 2 +- .../SearchScreen/SearchScreenViewModel.swift | 4 +- .../Sources/Services/Client/ClientProxy.swift | 1 + .../Services/Client/ClientProxyProtocol.swift | 2 +- .../Room/RoomSummary/RoomSummary.swift | 3 ++ .../RoomSummary/RoomSummaryProvider.swift | 19 +++++---- .../RoomSummaryProviderProtocol.swift | 4 +- ...orwardingScreen.Suggestions-iPad-en-GB.png | 4 +- ...rwardingScreen.Suggestions-iPad-pseudo.png | 4 +- ...wardingScreen.Suggestions-iPhone-en-GB.png | 4 +- ...ardingScreen.Suggestions-iPhone-pseudo.png | 4 +- .../roomSelectionScreen.iPad-en-GB-0.png | 4 +- .../roomSelectionScreen.iPad-pseudo-0.png | 4 +- .../roomSelectionScreen.iPhone-en-GB-0.png | 4 +- .../roomSelectionScreen.iPhone-pseudo-0.png | 4 +- UnitTests/Sources/HomeScreenRoomTests.swift | 1 + UnitTests/Sources/LoggingTests.swift | 1 + ...essageForwardingScreenViewModelTests.swift | 6 ++- .../RoomSelectionScreenViewModelTests.swift | 20 ++++++++++ .../Sources/RoomSummaryProviderTests.swift | 40 ++++++++++++++++++- UnitTests/Sources/RoomSummaryTests.swift | 1 + .../Sources/SearchScreenViewModelTests.swift | 30 +++++--------- 28 files changed, 141 insertions(+), 56 deletions(-) create mode 100644 UnitTests/Sources/RoomSelectionScreenViewModelTests.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 0a3905e4e..f64dedaaa 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -1437,6 +1437,7 @@ E3E1E255DC8CB34BD8573E0D /* UserIndicatorControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12D3B1BCF920880CA8BBB6B /* UserIndicatorControllerProtocol.swift */; }; E3EBC3BF7CE3960B41757BAA /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7310D8DFE01AF45F0689C3AA /* Publisher.swift */; }; E468CC731C3F4D678499E52F /* LAContextMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BA5A62DA4B543827FF82354 /* LAContextMock.swift */; }; + E471DCA49612AEBF4DD8C8D4 /* RoomSelectionScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69A3015DB39E3465B00B8BF /* RoomSelectionScreenViewModelTests.swift */; }; E481C8FDCB6C089963C95344 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = BC01130651CB23340B899032 /* DeviceKit */; }; E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */; }; E4B07FF075C99D04D9AF792D /* AppLockSetupPINScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B410B32B72C90BF94E481F33 /* AppLockSetupPINScreenModels.swift */; }; @@ -2948,6 +2949,7 @@ C5CB64E711E86270B722FAF7 /* LiveLocationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveLocationManagerTests.swift; sourceTree = ""; }; C5F06F2F09B2EDD067DC2174 /* NotificationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsScreen.swift; sourceTree = ""; }; C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; + C69A3015DB39E3465B00B8BF /* RoomSelectionScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSelectionScreenViewModelTests.swift; sourceTree = ""; }; C6A9F49B3EE59147AF2F70BB /* SeparatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineItem.swift; sourceTree = ""; }; C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAvatarImage.swift; sourceTree = ""; }; C715CFE00686DACA59D836EA /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/SAS.strings; sourceTree = ""; }; @@ -5322,6 +5324,7 @@ B40233F2989AD49906BB310D /* RoomPollsHistoryScreenViewModelTests.swift */, 48FEFF746DB341CDB18D7AAA /* RoomRolesAndPermissionsScreenViewModelTests.swift */, 93CF7B19FFCF8EFBE0A8696A /* RoomScreenViewModelTests.swift */, + C69A3015DB39E3465B00B8BF /* RoomSelectionScreenViewModelTests.swift */, AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */, F46E441BA50705E6CEC89FE0 /* RoomSummaryProviderTests.swift */, 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */, @@ -8440,6 +8443,7 @@ 7B1605C6FFD4D195F264A684 /* RoomPollsHistoryScreenViewModelTests.swift in Sources */, 84C631E734FD2555B39B681C /* RoomRolesAndPermissionsScreenViewModelTests.swift in Sources */, 46562110EE202E580A5FFD9C /* RoomScreenViewModelTests.swift in Sources */, + E471DCA49612AEBF4DD8C8D4 /* RoomSelectionScreenViewModelTests.swift in Sources */, CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */, 6AB306367E56A6F6DFA0E2FF /* RoomSummaryProviderTests.swift in Sources */, 15913A5B07118C1268A840E4 /* RoomSummaryTests.swift in Sources */, diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index ff1f376b1..fea209e7c 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -43,8 +43,8 @@ extension RoomSummaryProviderMock { setFilterClosure = { [initialRooms, roomListSubject] filter in switch filter { - case let .search(query, joinedOnly): - var rooms = joinedOnly ? initialRooms.filter { $0.joinRequestType == nil } : initialRooms + case let .search(query): + var rooms = initialRooms if !query.isEmpty { rooms = rooms.filter { $0.name.localizedCaseInsensitiveContains(query) } @@ -73,10 +73,12 @@ extension RoomSummary { static func mock(id: String, name: String, isSpace: Bool = false, + isJoined: Bool = true, canonicalAlias: String? = nil) -> RoomSummary { RoomSummary(room: RoomSDKMock(.init()), id: id, joinRequestType: nil, + isJoined: isJoined, name: name, isDirect: false, isSpace: isSpace, @@ -105,6 +107,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "1", joinRequestType: nil, + isJoined: true, name: "Foundation 🔭🪐🌌", isDirect: false, isSpace: false, @@ -128,6 +131,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "2", joinRequestType: nil, + isJoined: true, name: "Foundation and Empire", isDirect: false, isSpace: false, @@ -151,6 +155,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "3", joinRequestType: nil, + isJoined: true, name: "Second Foundation", isDirect: false, isSpace: false, @@ -174,6 +179,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "4", joinRequestType: nil, + isJoined: true, name: "Foundation's Edge", isDirect: false, isSpace: false, @@ -197,6 +203,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "5", joinRequestType: nil, + isJoined: true, name: "Foundation and Earth", isDirect: true, isSpace: false, @@ -220,6 +227,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "6", joinRequestType: nil, + isJoined: true, name: "Prelude to Foundation", isDirect: true, isSpace: false, @@ -243,6 +251,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "7", joinRequestType: nil, + isJoined: true, name: "Tombstoned", isDirect: false, isSpace: false, @@ -266,6 +275,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "0", joinRequestType: nil, + isJoined: true, name: "Unknown", isDirect: false, isSpace: false, @@ -322,6 +332,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "someAwesomeRoomId1", joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), + isJoined: false, name: "First room", isDirect: false, isSpace: false, @@ -345,6 +356,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "someAwesomeRoomId2", joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), + isJoined: false, name: "Second room", isDirect: true, isSpace: false, @@ -371,6 +383,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "!space1:matrix.org", joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), + isJoined: false, name: "First space", isDirect: false, isSpace: true, @@ -394,6 +407,7 @@ extension Array where Element == RoomSummary { RoomSummary(room: RoomSDKMock(), id: "!space2:matrix.org", joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie), + isJoined: false, name: "Second space", isDirect: false, isSpace: true, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift index 31e968161..6d620b8b8 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift @@ -218,6 +218,7 @@ private extension HomeScreenRoom { let summary = RoomSummary(room: RoomSDKMock(), id: "@someone:somewhere.com", joinRequestType: .invite(inviter: inviter), + isJoined: false, name: "Some Guy", isDirect: true, isSpace: false, @@ -254,6 +255,7 @@ private extension HomeScreenRoom { let summary = RoomSummary(room: RoomSDKMock(), id: "@someone:somewhere.com", joinRequestType: .invite(inviter: inviter), + isJoined: false, name: name, isDirect: false, isSpace: isSpace, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift index 93f16192e..f78fa8225 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift @@ -150,6 +150,7 @@ private extension HomeScreenRoom { let summary = RoomSummary(room: RoomSDKMock(), id: "@someone:somewhere.com", joinRequestType: .invite(inviter: inviter), + isJoined: false, name: "Some Guy", isDirect: true, isSpace: false, @@ -183,6 +184,7 @@ private extension HomeScreenRoom { let summary = RoomSummary(room: RoomSDKMock(), id: "@someone:somewhere.com", joinRequestType: .invite(inviter: inviter), + isJoined: false, name: "Awesome Room", isDirect: false, isSpace: false, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index 040ae85c9..90830d19d 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -268,6 +268,7 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview { let summary = RoomSummary(room: RoomSDKMock(), id: UUID().uuidString, joinRequestType: nil, + isJoined: true, name: name, isDirect: heroes.count == 1, isSpace: false, diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift index 873dff16e..4cc24ac6b 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift @@ -88,7 +88,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me private func updateRooms() { state.rooms = roomSummaryProvider.roomListPublisher.value - .filter { $0.id != forwardingPayload.roomID } + .filter { $0.id != forwardingPayload.roomID && !$0.isTombstoned } .map(MessageForwardingRoom.init(summary:)) } @@ -99,7 +99,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me state.suggestedRooms = roomIDs .filter { $0 != forwardingPayload.roomID } .compactMap(clientProxy.roomSummaryForIdentifier) - .filter { $0.joinRequestType == nil && !$0.isSpace && !$0.isTombstoned } + .filter { $0.isJoined && !$0.isSpace && !$0.isTombstoned } .prefix(Self.maxSuggestedRoomCount) .map(MessageForwardingRoom.init(summary:)) } diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift index 5e775a6e3..e6d3e6506 100644 --- a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift @@ -77,7 +77,7 @@ class RoomSelectionScreenViewModel: RoomSelectionScreenViewModelType, RoomSelect private func updateRooms() { var rooms = [RoomSelectionRoom]() - for summary in roomSummaryProvider.roomListPublisher.value { + for summary in roomSummaryProvider.roomListPublisher.value where !summary.isTombstoned { rooms.append(.init(id: summary.id, title: summary.name, description: summary.roomListDescription, diff --git a/ElementX/Sources/Screens/SearchScreen/SearchScreenViewModel.swift b/ElementX/Sources/Screens/SearchScreen/SearchScreenViewModel.swift index 2ba00118f..0d03b14f0 100644 --- a/ElementX/Sources/Screens/SearchScreen/SearchScreenViewModel.swift +++ b/ElementX/Sources/Screens/SearchScreen/SearchScreenViewModel.swift @@ -194,7 +194,7 @@ class SearchScreenViewModel: SearchScreenViewModelType, SearchScreenViewModelPro switch mode { case .rooms: searchedQueries[mode] = searchQuery - roomSummaryProvider.setFilter(.search(query: searchQuery, joinedOnly: true)) + roomSummaryProvider.setFilter(.search(query: searchQuery)) case .messages: setQueryTask = Task { [weak self] in // Debounce message queries; superseded keystrokes cancel this before it commits. @@ -215,7 +215,7 @@ class SearchScreenViewModel: SearchScreenViewModelType, SearchScreenViewModelPro return .query(query) case .room(let roomID): // Drop rooms the user has since left or is only invited to, we've got nothing to show for them. - guard let summary = clientProxy.roomSummaryForIdentifier(roomID), summary.joinRequestType == nil else { return nil } + guard let summary = clientProxy.roomSummaryForIdentifier(roomID), summary.isJoined else { return nil } return .room(SearchScreenRoom(summary)) } } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 1888faf1b..136fa02c1 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -1582,6 +1582,7 @@ private struct ClientProxyServices { alternateRoomSummaryProvider = RoomSummaryProvider(roomListService: roomListService, eventStringBuilder: eventStringBuilder, name: "AlternateAllRooms", + includesOnlyJoinedRooms: true, notificationSettings: notificationSettings, userSettings: userSettings) diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index aa37bac07..c372ed036 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -149,7 +149,7 @@ protocol ClientProxyProtocol: AnyObject { /// Used for listing rooms that shouldn't be affected by the main `roomSummaryProvider` filtering /// But can still be filtered by queries, since this may be shared across multiple views, remember to reset - /// The filtering state when you are done with it + /// The filtering state when you are done with it. Only lists joined rooms. var alternateRoomSummaryProvider: RoomSummaryProviderProtocol { get } /// Used for listing rooms, can't be filtered nor its state observed diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index 9459daeae..f11a170f0 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -30,6 +30,7 @@ nonisolated struct RoomSummary { let id: String let joinRequestType: JoinRequestType? + let isJoined: Bool let name: String let isDirect: Bool @@ -118,6 +119,7 @@ nonisolated extension RoomSummary { RoomSummary(room: room, id: room.id(), joinRequestType: nil, + isJoined: false, name: room.id(), isDirect: false, isSpace: false, @@ -165,6 +167,7 @@ nonisolated extension RoomSummary { activeCallIntent = nil joinRequestType = nil + isJoined = true isMarkedUnread = false isFavourite = false isTombstoned = false diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 58ca185e0..4240bdadf 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -17,6 +17,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { private let shouldUpdateVisibleRange: Bool private let notificationSettings: NotificationSettingsProxyProtocol private let userSettings: UserSettings + private let baseFilter: [RoomListEntriesDynamicFilterKind] private let roomListPageSize: UInt32 /// Remember how many rooms we had on the previous requests so we can deduplicate @@ -62,10 +63,12 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { /// - shouldUpdateVisibleRange: whether this summary provider should forward visible ranges /// to the room list service through the `applyInput(input: .viewport(ranges` api. Only useful for /// lists that need to update the visible range on Sliding Sync + /// - includesOnlyJoinedRooms: whether to leave out invites and other rooms the user hasn't joined, whatever the filter init(roomListService: RoomListServiceProtocol, eventStringBuilder: RoomEventStringBuilder, name: String, shouldUpdateVisibleRange: Bool = false, + includesOnlyJoinedRooms: Bool = false, roomListPageSize: UInt32 = 100, notificationSettings: NotificationSettingsProxyProtocol, userSettings: UserSettings) { @@ -77,6 +80,10 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { self.userSettings = userSettings self.roomListPageSize = roomListPageSize + baseFilter = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]), + .all(filters: [.space, .invite])]), + .deduplicateVersions] + (includesOnlyJoinedRooms ? [.joined] : []) + let (diffsStream, diffsContinuation) = AsyncStream<[RoomListEntriesUpdate]>.makeStream() self.diffsContinuation = diffsContinuation @@ -120,8 +127,9 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { }) // Forces the listener above to be called with the current state + let filter = currentFilter ?? .all(filters: []) currentFilter = nil - setFilter(.all(filters: [])) + setFilter(filter) let stateUpdatesSubscriptionResult = try roomList.loadingState(listener: SDKListener { [loadingStateContinuation] state in loadingStateContinuation.yield(state) @@ -146,15 +154,11 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { currentFilter = filter - let baseFilter: [RoomListEntriesDynamicFilterKind] = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]), - .all(filters: [.space, .invite])]), - .deduplicateVersions] - switch filter { case .excludeAll: _ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .none) - case let .search(query, joinedOnly): - let filters = nameFilter(for: query) + baseFilter + (joinedOnly ? [.joined] : []) + case let .search(query): + let filters = nameFilter(for: query) + baseFilter _ = listUpdatesSubscriptionResult?.controller().setFilter(kind: .all(filters: filters)) case .rooms(let roomIDs, let filters): var rustFilters = filters.map(\.rustFilter) + baseFilter @@ -372,6 +376,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { return RoomSummary(room: room, id: roomInfo.id, joinRequestType: joinRequestType, + isJoined: roomInfo.membership == .joined, name: roomInfo.displayName ?? roomInfo.id, isDirect: roomInfo.isDirect, isSpace: roomInfo.isSpace, diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift index 5729af7cf..659f9adb5 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProviderProtocol.swift @@ -36,8 +36,8 @@ enum RoomSummaryProviderState { enum RoomSummaryProviderFilter: Equatable { /// Filters out everything case excludeAll - /// Includes only the items that satisfy the predicate logic, optionally restricted to joined rooms - case search(query: String, joinedOnly: Bool = false) + /// Includes only the items that satisfy the predicate logic + case search(query: String) /// Includes only what satisfies the filters used case all(filters: Set) /// Include only rooms from the given that satisfy the given filters diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-en-GB.png index 1bbae6df0..a2d62d0a9 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:175fc36dbb3f8b53610b87a06fb8f69d9a7c03416699a732678dc719ea7227e0 -size 170430 +oid sha256:3d518a38be7144846c00dcf2f1fa8c610c9d081d2ae4f1f3916268a34b4bd384 +size 166840 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-pseudo.png index 621930925..88807ceb7 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1a433bd71d9eae827259513c29d4765202c38aac1a97c4e6e0527ca624779ac -size 174737 +oid sha256:6353e5a7813a4eb3a58286d31c45923fdf3b53ebf2dae152c096735ad00bdf1c +size 171136 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-en-GB.png index fb762bb62..94ae298c1 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:143c4962bc624c86d81996a122894b18b76acbd587942942ca736f3ac5213c5d -size 118839 +oid sha256:4e255a55f696f81294ea7706f61f7fe6377ab3b46a8ae8b078e5b10e65f23eae +size 115043 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-pseudo.png index a4eb35bd0..53d85fb76 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/messageForwardingScreen.Suggestions-iPhone-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78e40d11b882b09e271b345bbbe941216d15629a501191382ed5047e0f227c54 -size 119142 +oid sha256:cbc6fe61ccf86ee6e8c23d282e008f533ccd2aef40935bd0e2cc4105bb36aba4 +size 115346 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png index 3efc1e0ba..a336a7211 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ace5cc2b095335d929c4209d96aa07bdd251ca8195eaf0e99949c978edca743 -size 160670 +oid sha256:b650e360efd5fcb17b5756b5eb582bb6b9a5e25b46d4247ed87f652b1befd972 +size 156916 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png index 542aa1dd7..6fe0dd4da 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b7ee1aeecfabf71746ca20c3e1211659c6ec0acb2c1a434c5ce39e9bac62295 -size 162114 +oid sha256:bc1633ed6b191833ff4df4835e5298b4460efc38663e253517b82b0a64aac746 +size 158360 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-en-GB-0.png index b2891d30f..f1abf9f9b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:065d06fb64af86ad92bdae8ed3f9df7adc0b00a9ac574668a318281c899d3f2c -size 111147 +oid sha256:6589ac4a2211cf44fcb0c83c131299528d534aee9d5ccb291025621d962a4e48 +size 107113 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-pseudo-0.png index 45d75d951..1425ab245 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomSelectionScreen.iPhone-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89f6c4e043bf495a143c26649bd485e638643dbe5750f662d7c7dbbff37fb640 -size 110563 +oid sha256:562a960d20d2071d90e3bf618ae624f9bc73fd06e2f0cdf26ca9bd1b32516483 +size 106529 diff --git a/UnitTests/Sources/HomeScreenRoomTests.swift b/UnitTests/Sources/HomeScreenRoomTests.swift index 08db9eafb..0379720d8 100644 --- a/UnitTests/Sources/HomeScreenRoomTests.swift +++ b/UnitTests/Sources/HomeScreenRoomTests.swift @@ -25,6 +25,7 @@ struct HomeScreenRoomTests { roomSummary = RoomSummary(room: RoomSDKMock(), id: "Test room", joinRequestType: nil, + isJoined: true, name: "Test room", isDirect: false, isSpace: false, diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index 7b4929508..980606434 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -73,6 +73,7 @@ final class LoggingTests { let roomSummary = RoomSummary(room: RoomSDKMock(), id: "myroomid", joinRequestType: nil, + isJoined: true, name: roomName, isDirect: true, isSpace: false, diff --git a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift index 37fc333e3..44d7e9fb0 100644 --- a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift +++ b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift @@ -35,6 +35,7 @@ struct MessageForwardingScreenViewModelTests { @Test func initialState() { #expect(!context.viewState.rooms.contains { $0.id == forwardingPayload.roomID }, "The source room ID shouldn't be shown") + #expect(!context.viewState.rooms.contains { $0.id == "7" }, "Upgraded rooms shouldn't be shown") } @Test @@ -50,7 +51,7 @@ struct MessageForwardingScreenViewModelTests { @Test mutating func suggestionsOnlyIncludeRoomsThatCanBeForwardedTo() async throws { - viewModel = makeViewModel(recentlyVisitedRoomIDs: [forwardingPayload.roomID, "space", "7", "someAwesomeRoomId1", "!unknown:matrix.org", "2"]) + viewModel = makeViewModel(recentlyVisitedRoomIDs: [forwardingPayload.roomID, "space", "7", "someAwesomeRoomId1", "left", "!unknown:matrix.org", "2"]) context = viewModel.context let deferred = deferFulfillment(context.$viewState) { !$0.suggestedRooms.isEmpty } @@ -289,7 +290,8 @@ struct MessageForwardingScreenViewModelTests { let clientProxy = ClientProxyMock(.init()) clientProxy.roomForIdentifierClosure = { .joined(JoinedRoomProxyMock(.init(id: $0))) } clientProxy.recentlyVisitedRoomIDsReturnValue = .success(recentlyVisitedRoomIDs) - let knownRooms: [RoomSummary] = .mockRooms + .mockInvites + [.mock(id: "space", name: "Space", isSpace: true)] + let knownRooms: [RoomSummary] = .mockRooms + .mockInvites + [.mock(id: "space", name: "Space", isSpace: true), + .mock(id: "left", name: "Left", isJoined: false)] clientProxy.roomSummaryForIdentifierClosure = { roomID in knownRooms.first { $0.id == roomID } } return MessageForwardingScreenViewModel(forwardingPayload: forwardingPayload, diff --git a/UnitTests/Sources/RoomSelectionScreenViewModelTests.swift b/UnitTests/Sources/RoomSelectionScreenViewModelTests.swift new file mode 100644 index 000000000..bf1f83767 --- /dev/null +++ b/UnitTests/Sources/RoomSelectionScreenViewModelTests.swift @@ -0,0 +1,20 @@ +// +// 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 + +struct RoomSelectionScreenViewModelTests { + @Test + func upgradedRoomsAreNotListed() { + let viewModel = RoomSelectionScreenViewModel(userSession: UserSessionMock(.init()), + roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))) + + #expect(!viewModel.context.viewState.rooms.isEmpty) + #expect(!viewModel.context.viewState.rooms.contains { $0.id == "7" }, "Upgraded rooms shouldn't be shown") + } +} diff --git a/UnitTests/Sources/RoomSummaryProviderTests.swift b/UnitTests/Sources/RoomSummaryProviderTests.swift index b7083db84..663c0cf93 100644 --- a/UnitTests/Sources/RoomSummaryProviderTests.swift +++ b/UnitTests/Sources/RoomSummaryProviderTests.swift @@ -41,6 +41,38 @@ final class RoomSummaryProviderTests { #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters)) } + @Test + func joinedRoomsOnlyRustFilters() async { + // Given a new room provider that only includes joined rooms. + setup(includesOnlyJoinedRooms: true) + await Task.yield() + + // Then the joined filter should be added to the default Rust filters. + #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters + [.joined])) + + // When searching. + roomSummaryProvider.setFilter(.search(query: "Foundation")) + await Task.yield() + + // Then the search should be restricted to joined rooms too. + #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.normalizedMatchRoomName(pattern: "Foundation")] + baseFilters + [.joined])) + } + + @Test + func filterSetBeforeTheRoomListIsKept() async { + // Given a new room provider that is filtered before it has a room list. + setup(setsRoomList: false) + roomSummaryProvider.setFilter(.search(query: "Foundation")) + + // When setting its room list. + roomSummaryProvider.setRoomList(roomList) + await Task.yield() + + // Then the filter should be applied instead of the default one. + #expect(dynamicEntriesController.setFilterKindCallsCount == 1) + #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.normalizedMatchRoomName(pattern: "Foundation")] + baseFilters)) + } + @Test func lowPriorityRustFilters() async { // Given a new room provider with the low priority filter enabled. @@ -89,7 +121,7 @@ final class RoomSummaryProviderTests { // MARK: - Helpers - private func setup(isLowPriorityFilterEnabled: Bool = false) { + private func setup(isLowPriorityFilterEnabled: Bool = false, includesOnlyJoinedRooms: Bool = false, setsRoomList: Bool = true) { userSettings = UserSettings.volatile() userSettings.lowPriorityFilterEnabled = isLowPriorityFilterEnabled @@ -103,6 +135,7 @@ final class RoomSummaryProviderTests { roomSummaryProvider = RoomSummaryProvider(roomListService: RoomListServiceSDKMock(), eventStringBuilder: eventStringBuilder, name: "Test", + includesOnlyJoinedRooms: includesOnlyJoinedRooms, notificationSettings: NotificationSettingsProxyMock(with: .init()), userSettings: userSettings) @@ -113,6 +146,9 @@ final class RoomSummaryProviderTests { roomList = RoomListSDKMock() roomList.entriesWithDynamicAdaptersPageSizeListenerReturnValue = dynamicAdaptersResult roomList.loadingStateListenerReturnValue = .some(.init(state: .notLoaded, stateStream: TaskHandleSDKMock())) - roomSummaryProvider.setRoomList(roomList) + + if setsRoomList { + roomSummaryProvider.setRoomList(roomList) + } } } diff --git a/UnitTests/Sources/RoomSummaryTests.swift b/UnitTests/Sources/RoomSummaryTests.swift index 1fb2da855..1ad6836a9 100644 --- a/UnitTests/Sources/RoomSummaryTests.swift +++ b/UnitTests/Sources/RoomSummaryTests.swift @@ -100,6 +100,7 @@ struct RoomSummaryTests { RoomSummary(room: RoomSDKMock(), id: roomDetails.id, joinRequestType: nil, + isJoined: true, name: roomDetails.name, isDirect: isDirect, isSpace: isSpace, diff --git a/UnitTests/Sources/SearchScreenViewModelTests.swift b/UnitTests/Sources/SearchScreenViewModelTests.swift index 6cc35922a..7f8eb1401 100644 --- a/UnitTests/Sources/SearchScreenViewModelTests.swift +++ b/UnitTests/Sources/SearchScreenViewModelTests.swift @@ -73,25 +73,6 @@ struct SearchScreenViewModelTests { #expect(!searchService.setQueryCalled) } - @Test - func searchingExcludesRoomsThatAreNotJoined() async throws { - let roomSummaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms + .mockInvites))) - let clientProxy = ClientProxyMock(.init()) - clientProxy.searchService = searchService - let viewModel = SearchScreenViewModel(roomSummaryProvider: roomSummaryProvider, - clientProxy: clientProxy, - mediaProvider: MediaProviderMock(.init()), - userIndicatorController: userIndicatorController, - userSettings: userSettings) - - let deferred = deferFulfillment(viewModel.context.observe(\.viewState.rooms)) { !$0.isEmpty } - viewModel.context.searchQuery = "room" - try await deferred.fulfill() - - #expect(roomSummaryProvider.setFilterReceivedFilter == .search(query: "room", joinedOnly: true)) - #expect(!viewModel.context.viewState.rooms.contains { $0.title == "First room" }) - } - @Test func messageSearch() async throws { let deferred = deferFulfillment(setQuerySubject) { $0 == "Foundation" } @@ -194,6 +175,17 @@ struct SearchScreenViewModelTests { #expect(context.viewState.breadcrumbs.map(\.id) == ["query-Second", "room-2"]) } + @Test + func breadcrumbsOfRoomsThatAreNotJoinedAreHidden() async throws { + staticRoomListSubject.send([.mock(id: "2", name: "Second"), .mock(id: "left", name: "Left", isJoined: false)]) + + let deferred = deferFulfillment(context.observe(\.viewState.breadcrumbs)) { !$0.isEmpty } + userSettings.searchBreadcrumbs = [.room(roomID: "left"), .room(roomID: "2")] + try await deferred.fulfill() + + #expect(context.viewState.breadcrumbs.map(\.id) == ["room-2"]) + } + /// The room list is empty when the app launches, so the rooms need resolving again once it has loaded. @Test func roomBreadcrumbsAreDisplayedAfterTheRoomListLoads() async throws {