Fixes #2624, fixes #2625 - Add user suggestions in the create room flows.

This commit is contained in:
Stefan Ceriu
2024-04-01 11:37:39 +03:00
committed by Stefan Ceriu
parent c85c1593ee
commit 97d9189077
17 changed files with 252 additions and 60 deletions
+1 -1
View File
@@ -7176,7 +7176,7 @@
repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift";
requirement = {
kind = exactVersion;
version = 1.1.55;
version = 1.1.56;
};
};
821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = {
@@ -139,8 +139,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-rust-components-swift",
"state" : {
"revision" : "407ad3da9444148ec71a775029c69c25e4a8bb2e",
"version" : "1.1.55"
"revision" : "1d47d496ca46e123e102e64d45635fab73de3d78",
"version" : "1.1.56"
}
},
{
@@ -263,7 +263,7 @@
{
"identity" : "swiftui-introspect",
"kind" : "remoteSourceControl",
"location" : "https://github.com/siteline/SwiftUI-Introspect",
"location" : "https://github.com/siteline/SwiftUI-Introspect.git",
"state" : {
"revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290",
"version" : "0.9.2"
@@ -1009,11 +1009,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let selectedUsersSubject: CurrentValueSubject<[UserProfileProxy], Never> = .init([])
let stackCoordinator = NavigationStackCoordinator()
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsersSubject),
roomType: .room(roomProxy: roomProxy),
let inviteParameters = InviteUsersScreenCoordinatorParameters(clientProxy: userSession.clientProxy,
mediaProvider: userSession.mediaProvider,
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy),
userIndicatorController: userIndicatorController)
userIndicatorController: userIndicatorController,
selectedUsers: .init(selectedUsersSubject),
roomType: .room(roomProxy: roomProxy))
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
stackCoordinator.setRootCoordinator(coordinator)
@@ -460,6 +460,10 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
if case .invitesScreen = stateMachine.state, availableInvitesCount == 1 {
dismissInvitesList(animated: true)
}
Task {
await userSession.clientProxy.trackRecentlyVisitedRoom(roomID)
}
}
private func tearDownRoomFlow(animated: Bool) {
@@ -73,6 +73,10 @@ extension ClientProxyMock {
ignoreUserReturnValue = .success(())
unignoreUserReturnValue = .success(())
trackRecentlyVisitedRoomReturnValue = .success(())
recentlyVisitedRoomsReturnValue = .success([])
recentConversationCounterpartsReturnValue = []
loadMediaContentForSourceThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
loadMediaThumbnailForSourceWidthHeightThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
loadMediaFileForSourceBodyThrowableError = ClientProxyError.sdkError(ClientProxyMockError.generic)
@@ -1211,6 +1211,61 @@ class ClientProxyMock: ClientProxyProtocol {
return unignoreUserReturnValue
}
}
//MARK: - trackRecentlyVisitedRoom
var trackRecentlyVisitedRoomCallsCount = 0
var trackRecentlyVisitedRoomCalled: Bool {
return trackRecentlyVisitedRoomCallsCount > 0
}
var trackRecentlyVisitedRoomReceivedRoomID: String?
var trackRecentlyVisitedRoomReceivedInvocations: [String] = []
var trackRecentlyVisitedRoomReturnValue: Result<Void, ClientProxyError>!
var trackRecentlyVisitedRoomClosure: ((String) async -> Result<Void, ClientProxyError>)?
func trackRecentlyVisitedRoom(_ roomID: String) async -> Result<Void, ClientProxyError> {
trackRecentlyVisitedRoomCallsCount += 1
trackRecentlyVisitedRoomReceivedRoomID = roomID
trackRecentlyVisitedRoomReceivedInvocations.append(roomID)
if let trackRecentlyVisitedRoomClosure = trackRecentlyVisitedRoomClosure {
return await trackRecentlyVisitedRoomClosure(roomID)
} else {
return trackRecentlyVisitedRoomReturnValue
}
}
//MARK: - recentlyVisitedRooms
var recentlyVisitedRoomsCallsCount = 0
var recentlyVisitedRoomsCalled: Bool {
return recentlyVisitedRoomsCallsCount > 0
}
var recentlyVisitedRoomsReturnValue: Result<[String], ClientProxyError>!
var recentlyVisitedRoomsClosure: (() async -> Result<[String], ClientProxyError>)?
func recentlyVisitedRooms() async -> Result<[String], ClientProxyError> {
recentlyVisitedRoomsCallsCount += 1
if let recentlyVisitedRoomsClosure = recentlyVisitedRoomsClosure {
return await recentlyVisitedRoomsClosure()
} else {
return recentlyVisitedRoomsReturnValue
}
}
//MARK: - recentConversationCounterparts
var recentConversationCounterpartsCallsCount = 0
var recentConversationCounterpartsCalled: Bool {
return recentConversationCounterpartsCallsCount > 0
}
var recentConversationCounterpartsReturnValue: [UserProfileProxy]!
var recentConversationCounterpartsClosure: (() async -> [UserProfileProxy])?
func recentConversationCounterparts() async -> [UserProfileProxy] {
recentConversationCounterpartsCallsCount += 1
if let recentConversationCounterpartsClosure = recentConversationCounterpartsClosure {
return await recentConversationCounterpartsClosure()
} else {
return recentConversationCounterpartsReturnValue
}
}
//MARK: - loadMediaContentForSource
var loadMediaContentForSourceThrowableError: Error?
@@ -1,4 +1,4 @@
// Generated using Sourcery 2.1.7 — https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.1.8 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable all
@@ -344,6 +344,27 @@ class SDKClientMock: SDKClientProtocol {
return getProfileUserIdReturnValue
}
}
//MARK: - getRecentlyVisitedRooms
public var getRecentlyVisitedRoomsThrowableError: Error?
public var getRecentlyVisitedRoomsCallsCount = 0
public var getRecentlyVisitedRoomsCalled: Bool {
return getRecentlyVisitedRoomsCallsCount > 0
}
public var getRecentlyVisitedRoomsReturnValue: [String]!
public var getRecentlyVisitedRoomsClosure: (() async throws -> [String])?
public func getRecentlyVisitedRooms() async throws -> [String] {
if let error = getRecentlyVisitedRoomsThrowableError {
throw error
}
getRecentlyVisitedRoomsCallsCount += 1
if let getRecentlyVisitedRoomsClosure = getRecentlyVisitedRoomsClosure {
return try await getRecentlyVisitedRoomsClosure()
} else {
return getRecentlyVisitedRoomsReturnValue
}
}
//MARK: - getSessionVerificationController
public var getSessionVerificationControllerThrowableError: Error?
@@ -749,6 +770,26 @@ class SDKClientMock: SDKClientProtocol {
return syncServiceReturnValue
}
}
//MARK: - trackRecentlyVisitedRoom
public var trackRecentlyVisitedRoomRoomThrowableError: Error?
public var trackRecentlyVisitedRoomRoomCallsCount = 0
public var trackRecentlyVisitedRoomRoomCalled: Bool {
return trackRecentlyVisitedRoomRoomCallsCount > 0
}
public var trackRecentlyVisitedRoomRoomReceivedRoom: String?
public var trackRecentlyVisitedRoomRoomReceivedInvocations: [String] = []
public var trackRecentlyVisitedRoomRoomClosure: ((String) async throws -> Void)?
public func trackRecentlyVisitedRoom(room: String) async throws {
if let error = trackRecentlyVisitedRoomRoomThrowableError {
throw error
}
trackRecentlyVisitedRoomRoomCallsCount += 1
trackRecentlyVisitedRoomRoomReceivedRoom = room
trackRecentlyVisitedRoomRoomReceivedInvocations.append(room)
try await trackRecentlyVisitedRoomRoomClosure?(room)
}
//MARK: - unignoreUser
public var unignoreUserUserIdThrowableError: Error?
@@ -18,11 +18,13 @@ import Combine
import SwiftUI
struct InviteUsersScreenCoordinatorParameters {
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>
let roomType: InviteUsersScreenRoomType
let clientProxy: ClientProxyProtocol
let mediaProvider: MediaProviderProtocol
let userDiscoveryService: UserDiscoveryServiceProtocol
let userIndicatorController: UserIndicatorControllerProtocol
let selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>
let roomType: InviteUsersScreenRoomType
}
enum InviteUsersScreenCoordinatorAction {
@@ -42,11 +44,12 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
}
init(parameters: InviteUsersScreenCoordinatorParameters) {
viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers,
roomType: parameters.roomType,
viewModel = InviteUsersScreenViewModel(clientProxy: parameters.clientProxy,
mediaProvider: parameters.mediaProvider,
userDiscoveryService: parameters.userDiscoveryService,
userIndicatorController: parameters.userIndicatorController)
userIndicatorController: parameters.userIndicatorController,
selectedUsers: parameters.selectedUsers,
roomType: parameters.roomType)
}
func start() {
@@ -21,28 +21,42 @@ import SwiftUI
typealias InviteUsersScreenViewModelType = StateStoreViewModel<InviteUsersScreenViewState, InviteUsersScreenViewAction>
class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol
private let roomType: InviteUsersScreenRoomType
private let userDiscoveryService: UserDiscoveryServiceProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
private var suggestedUsers = [UserProfileProxy]()
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
roomType: InviteUsersScreenRoomType,
init(clientProxy: ClientProxyProtocol,
mediaProvider: MediaProviderProtocol,
userDiscoveryService: UserDiscoveryServiceProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.roomType = roomType
userIndicatorController: UserIndicatorControllerProtocol,
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
roomType: InviteUsersScreenRoomType) {
self.clientProxy = clientProxy
self.userDiscoveryService = userDiscoveryService
self.userIndicatorController = userIndicatorController
self.roomType = roomType
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider)
setupSubscriptions(selectedUsers: selectedUsers)
fetchMembersIfNeeded()
Task {
suggestedUsers = await clientProxy.recentConversationCounterparts()
if state.usersSection.type == .suggestions {
state.usersSection = .init(type: .suggestions, users: suggestedUsers)
}
}
}
// MARK: - Public
@@ -127,29 +141,28 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private func fetchUsers() {
guard searchQuery.count >= 3 else {
state.usersSection = .init(type: .suggestions, users: [])
state.usersSection = .init(type: .suggestions, users: suggestedUsers)
return
}
state.isSearching = true
fetchUsersTask = Task {
let result = await userDiscoveryService.searchProfiles(with: searchQuery)
guard !Task.isCancelled else { return }
handleResult(for: .searchResult, result: result)
state.isSearching = false
switch result {
case .success(let users):
state.usersSection = .init(type: .searchResult, users: users)
case .failure:
break
}
}
}
private func handleResult(for sectionType: UserDiscoverySectionType, result: Result<[UserProfileProxy], UserDiscoveryErrorType>) {
state.isSearching = false
switch result {
case .success(let users):
state.usersSection = .init(type: sectionType, users: users)
case .failure:
break
}
}
private var searchQuery: String {
context.searchQuery
}
@@ -160,11 +160,12 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview {
static let viewModel = {
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
return InviteUsersScreenViewModel(selectedUsers: .init([]),
roomType: .draft,
return InviteUsersScreenViewModel(clientProxy: ClientProxyMock(.init()),
mediaProvider: MockMediaProvider(),
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock())
userIndicatorController: UserIndicatorControllerMock(),
selectedUsers: .init([]),
roomType: .draft)
}()
static var previews: some View {
@@ -84,11 +84,12 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
// MARK: - Private
private func presentInviteUsersScreen() {
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: selectedUsersPublisher,
roomType: .draft,
let inviteParameters = InviteUsersScreenCoordinatorParameters(clientProxy: parameters.userSession.clientProxy,
mediaProvider: parameters.userSession.mediaProvider,
userDiscoveryService: parameters.userDiscoveryService,
userIndicatorController: parameters.userIndicatorController)
userIndicatorController: parameters.userIndicatorController,
selectedUsers: selectedUsersPublisher,
roomType: .draft)
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
@@ -25,8 +25,9 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
private let userIndicatorController: UserIndicatorControllerProtocol
private let userDiscoveryService: UserDiscoveryServiceProtocol
private let actionsSubject: PassthroughSubject<StartChatScreenViewModelAction, Never> = .init()
private var suggestedUsers = [UserProfileProxy]()
private let actionsSubject: PassthroughSubject<StartChatScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<StartChatScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
@@ -43,6 +44,14 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
super.init(initialViewState: StartChatScreenViewState(userID: userSession.userID), imageProvider: userSession.mediaProvider)
setupBindings()
Task {
suggestedUsers = await userSession.clientProxy.recentConversationCounterparts()
if state.usersSection.type == .suggestions {
state.usersSection = .init(type: .suggestions, users: suggestedUsers)
}
}
}
// MARK: - Public
@@ -89,25 +98,24 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
private func fetchUsers() {
guard context.searchQuery.count >= 3 else {
state.usersSection = .init(type: .suggestions, users: [])
state.usersSection = .init(type: .suggestions, users: suggestedUsers)
return
}
fetchUsersTask = Task {
let result = await userDiscoveryService.searchProfiles(with: context.searchQuery)
guard !Task.isCancelled else { return }
handleResult(for: .searchResult, result: result)
switch result {
case .success(let users):
state.usersSection = .init(type: .searchResult, users: users)
case .failure:
break
}
}
}
private func handleResult(for sectionType: UserDiscoverySectionType, result: Result<[UserProfileProxy], UserDiscoveryErrorType>) {
switch result {
case .success(let users):
state.usersSection = .init(type: sectionType, users: users)
case .failure:
state.bindings.alertInfo = AlertInfo(id: .unknown)
}
}
private func createDirectRoom(with user: UserProfileProxy) async {
defer {
hideLoadingIndicator()
@@ -574,7 +574,7 @@ class ClientProxy: ClientProxyProtocol {
RoomDirectorySearchProxy(roomDirectorySearch: client.roomDirectorySearch())
}
// MARK: - Ignored users
// MARK: Ignored users
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError> {
do {
@@ -596,7 +596,58 @@ class ClientProxy: ClientProxyProtocol {
}
}
// MARK: Private
// MARK: Recently visited rooms
func trackRecentlyVisitedRoom(_ roomID: String) async -> Result<Void, ClientProxyError> {
do {
try await client.trackRecentlyVisitedRoom(room: roomID)
return .success(())
} catch {
MXLog.error("Failed tracking recently visited room: \(roomID) with error: \(error)")
return .failure(.sdkError(error))
}
}
func recentlyVisitedRooms() async -> Result<[String], ClientProxyError> {
do {
let result = try await client.getRecentlyVisitedRooms()
return .success(result)
} catch {
MXLog.error("Failed retrieving recently visited rooms with error: \(error)")
return .failure(.sdkError(error))
}
}
func recentConversationCounterparts() async -> [UserProfileProxy] {
let maxResultsToReturn = 5
guard case let .success(roomIdentifiers) = await recentlyVisitedRooms() else {
return []
}
var users = [UserProfileProxy]()
for roomID in roomIdentifiers {
guard let room = await roomForIdentifier(roomID),
room.isDirect,
let members = await room.members() else {
continue
}
for member in members where member.isActive && member.userID != userID {
users.append(.init(userID: member.userID, displayName: member.displayName, avatarURL: member.avatarURL))
// Return early to avoid unnecessary work
if users.count >= maxResultsToReturn {
return users
}
}
}
return users
}
// MARK: - Private
private func loadUserAvatarURLFromCache() {
loadCachedAvatarURLTask = Task {
@@ -39,6 +39,7 @@ enum ClientProxyLoadingState {
enum ClientProxyError: Error {
case sdkError(Error)
case invalidMedia
case failedUploadingMedia(Error, MatrixErrorCode)
}
@@ -147,4 +148,12 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func ignoreUser(_ userID: String) async -> Result<Void, ClientProxyError>
func unignoreUser(_ userID: String) async -> Result<Void, ClientProxyError>
// MARK: - Recently visited rooms
func trackRecentlyVisitedRoom(_ roomID: String) async -> Result<Void, ClientProxyError>
func recentlyVisitedRooms() async -> Result<[String], ClientProxyError>
func recentConversationCounterparts() async -> [UserProfileProxy]
}
@@ -98,10 +98,12 @@ class InviteUsersScreenViewModelTests: XCTestCase {
userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.searchProfilesWithReturnValue = .success([])
usersSubject.send([])
let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(),
roomType: roomType, mediaProvider: MockMediaProvider(),
let viewModel = InviteUsersScreenViewModel(clientProxy: ClientProxyMock(.init()),
mediaProvider: MockMediaProvider(),
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock())
userIndicatorController: UserIndicatorControllerMock(),
selectedUsers: usersSubject.asCurrentValuePublisher(),
roomType: roomType)
viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie])
self.viewModel = viewModel
+3 -4
View File
@@ -222,10 +222,9 @@ class LoggingTests: XCTestCase {
let rustEmoteMessage = EmoteMessageContent(body: emoteString,
formatted: FormattedBody(format: .html, body: "<b>\(emoteString)</b>"))
let pointer = Unmanaged.passRetained(NSURL(fileURLWithPath: "/tmp/file")).toOpaque()
let rustImageMessage = ImageMessageContent(body: "ImageString", formatted: nil, filename: nil, source: MediaSource(unsafeFromRawPointer: pointer), info: nil)
let rustVideoMessage = VideoMessageContent(body: "VideoString", formatted: nil, filename: nil, source: MediaSource(unsafeFromRawPointer: pointer), info: nil)
let rustFileMessage = FileMessageContent(body: "FileString", formatted: nil, filename: "FileName", source: MediaSource(unsafeFromRawPointer: pointer), info: nil)
let rustImageMessage = ImageMessageContent(body: "ImageString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil)
let rustVideoMessage = VideoMessageContent(body: "VideoString", formatted: nil, filename: nil, source: MediaSource(noPointer: .init()), info: nil)
let rustFileMessage = FileMessageContent(body: "FileString", formatted: nil, filename: "FileName", source: MediaSource(noPointer: .init()), info: nil)
// When logging that value
MXLog.info(rustTextMessage)
+1 -1
View File
@@ -48,7 +48,7 @@ packages:
# Element/Matrix dependencies
MatrixRustSDK:
url: https://github.com/matrix-org/matrix-rust-components-swift
exactVersion: 1.1.55
exactVersion: 1.1.56
# path: ../matrix-rust-sdk
Compound:
url: https://github.com/element-hq/compound-ios