Files
element-x-ios/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift
T
Mauro f91b7441e7 Suggest recently visited rooms when forwarding messages (#6251)
* Suggest recently visited rooms when forwarding messages

The Forward to screen now shows up to 5 recently visited rooms in a
Suggestions section, above a Chats section that lists every room. The
suggestions are never searched: while searching, only the Chats section
is shown and filtered. The source room, invites, spaces and upgraded
rooms are never suggested, and the section is hidden when there is
nothing to suggest.

The suggestions are resolved from the room summaries rather than room
proxies, so they show up without holding back the list, and they are
re-resolved as the room list loads after a cold start. ClientProxy now
exposes the recently visited room IDs for this.

The recently visited rooms filter is now async, so callers choose its
isolation instead of asserting it at runtime.

* Apply review suggestions to the forwarding suggestions

Rooms:
- Each room is listed once. Suggested rooms are left out of Chats unless
  searching, where Chats lists every result. This also removes the
  duplicated row whose selection changed without the tapped row's
  animation.
- The Chats header is hidden when the section has no rooms.

Suggestions:
- Resolve them once when the screen opens instead of on every room list
  update.
2026-10-02 12:04:07 +02:00

419 lines
17 KiB
Swift

//
// 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 Combine
import Foundation
import MatrixRustSDKMocks
enum RoomSummaryProviderMockConfigurationState {
case loading
case loaded([RoomSummary])
}
struct RoomSummaryProviderMockConfiguration {
var state: RoomSummaryProviderMockConfigurationState = .loading
}
extension RoomSummaryProviderMock {
convenience init(_ configuration: RoomSummaryProviderMockConfiguration) {
self.init()
let initialRooms: [RoomSummary]
let roomListSubject: CurrentValueSubject<[RoomSummary], Never>
let stateSubject: CurrentValueSubject<RoomSummaryProviderState, Never>
switch configuration.state {
case .loading:
initialRooms = []
roomListSubject = .init(initialRooms)
stateSubject = .init(.notLoaded)
case .loaded(let rooms):
initialRooms = rooms
roomListSubject = .init(initialRooms)
stateSubject = .init(.loaded(totalNumberOfRooms: UInt(rooms.count)))
}
roomListPublisher = roomListSubject.asCurrentValuePublisher()
statePublisher = stateSubject.asCurrentValuePublisher()
setFilterClosure = { [initialRooms, roomListSubject] filter in
switch filter {
case let .search(query, joinedOnly):
var rooms = joinedOnly ? initialRooms.filter { $0.joinRequestType == nil } : initialRooms
if !query.isEmpty {
rooms = rooms.filter { $0.name.localizedCaseInsensitiveContains(query) }
}
roomListSubject.send(rooms)
case let .rooms(_, filters), let .all(filters):
var rooms = initialRooms
if filters.count > 1 {
// for testing purpose chaining more than one filter will always return an empty state
rooms = []
} else if let filter = filters.first {
rooms = rooms.filter { filter == .people ? $0.isDirect : !$0.isDirect }
}
roomListSubject.send(rooms)
case .excludeAll:
roomListSubject.send([])
}
}
}
}
extension RoomSummary {
static func mock(id: String,
name: String,
isSpace: Bool = false,
canonicalAlias: String? = nil) -> RoomSummary {
RoomSummary(room: RoomSDKMock(.init()),
id: id,
joinRequestType: nil,
name: name,
isDirect: false,
isSpace: isSpace,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: .allMessages,
canonicalAlias: canonicalAlias,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false)
}
}
extension Array where Element == RoomSummary {
static let mockRooms: [Element] = [
RoomSummary(room: RoomSDKMock(),
id: "1",
joinRequestType: nil,
name: "Foundation 🔭🪐🌌",
isDirect: false,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: .allMessages,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "2",
joinRequestType: nil,
name: "Foundation and Empire",
isDirect: false,
isSpace: false,
avatarURL: .mockMXCAvatar,
heroes: [],
activeMembersCount: 0,
lastMessage: AttributedString("How do you see the Emperor then? You think he keeps office hours?"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 2,
unreadMentionsCount: 0,
unreadNotificationsCount: 2,
notificationMode: .mute,
canonicalAlias: "#foundation-and-empire:matrix.org",
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "3",
joinRequestType: nil,
name: "Second Foundation",
isDirect: false,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: try? AttributedString(markdown: "He certainly seemed no *mental genius* to me"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 3,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: .mentionsAndKeywordsOnly,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "4",
joinRequestType: nil,
name: "Foundation's Edge",
isDirect: false,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: AttributedString("There's an archaic measure of time that's called the month"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 2,
unreadMentionsCount: 2,
unreadNotificationsCount: 2,
notificationMode: .allMessages,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "5",
joinRequestType: nil,
name: "Foundation and Earth",
isDirect: true,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: AttributedString("Clearly, if Earth is powerful enough to do that, it might also be capable of adjusting minds in order to force belief in its radioactivity"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 1,
unreadMentionsCount: 1,
unreadNotificationsCount: 1,
notificationMode: .allMessages,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: true,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "6",
joinRequestType: nil,
name: "Prelude to Foundation",
isDirect: true,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: AttributedString("Are you groping for the word 'paranoia'?"),
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 6,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: .mute,
canonicalAlias: "#prelude-foundation:matrix.org",
alternativeAliases: [],
hasOngoingCall: true,
activeCallIntent: .audio,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "7",
joinRequestType: nil,
name: "Tombstoned",
isDirect: false,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: .mock,
lastMessageState: nil,
unreadMessagesCount: 1,
unreadMentionsCount: 0,
unreadNotificationsCount: 1,
notificationMode: .allMessages,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: true),
RoomSummary(room: RoomSDKMock(),
id: "0",
joinRequestType: nil,
name: "Unknown",
isDirect: false,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false)
]
static let mockRoomsWithNotificationsState: [Element] = {
var result: [Element] = []
// Iterate over settings modes
for mode in RoomNotificationModeProxy.allCases {
// Iterate over unread messages states
for hasUnreadMessages in [false, true] {
// Iterate over unread mentions states
for hasUnreadMentions in [false, true] {
// Iterate over unread notifications states
for hasUnreadNotifications in [false, true] {
// Incrementing id value for each combination
let id = result.count + 1
let room = RoomSummary(room: RoomSDKMock(),
id: "\(id)",
settingsMode: mode,
hasUnreadMessages: hasUnreadMessages,
hasUnreadMentions: hasUnreadMentions,
hasUnreadNotifications: hasUnreadNotifications)
result.append(room)
}
}
}
}
return result
}()
static let mockInvites: [Element] = [
RoomSummary(room: RoomSDKMock(),
id: "someAwesomeRoomId1",
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
name: "First room",
isDirect: false,
isSpace: false,
avatarURL: .mockMXCAvatar,
heroes: [],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: "#footest:somewhere.org",
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "someAwesomeRoomId2",
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
name: "Second room",
isDirect: true,
isSpace: false,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false)
]
static let mockSpaceInvites: [Element] = [
RoomSummary(room: RoomSDKMock(),
id: "!space1:matrix.org",
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
name: "First space",
isDirect: false,
isSpace: true,
avatarURL: .mockMXCAvatar,
heroes: [],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: "#footest:somewhere.org",
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false),
RoomSummary(room: RoomSDKMock(),
id: "!space2:matrix.org",
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
name: "Second space",
isDirect: false,
isSpace: true,
avatarURL: nil,
heroes: [],
activeMembersCount: 0,
lastMessage: nil,
lastMessageDate: nil,
lastMessageState: nil,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
notificationMode: nil,
canonicalAlias: nil,
alternativeAliases: [],
hasOngoingCall: false,
activeCallIntent: nil,
isMarkedUnread: false,
isFavourite: false,
isTombstoned: false)
]
}