Room details: improve members count UX (#1050)

* Inject user id into room’s details

* Refactor member fetch logic in RoomDetailsScreenViewModel

* Refactor DI

* Fetch members in RoomMembersListScreenViewModel

* Fetch members in InviteUsersScreen

* Use joinedMembersCount in room members list

* Cleanup

* Limit members refresh

* Cleanup binding

* Fix UT build errors

* Fix RoomDetailsScreenViewModelTests

* Fix RoomMembersListScreenViewModelTests

* Fix UTs

* Fix RoomDetailsScreenUITests

* Refine RoomDetailsScreenViewModelTests

* Add roomProxy extension

* PR comments

* Refactor room’s counter to Int

* Improve updateMembers() performances

* Fix memory leaks
This commit is contained in:
Alfonso Grillo
2023-06-09 16:21:01 +02:00
committed by GitHub
parent 42e083dcaf
commit 63df68fd29
27 changed files with 368 additions and 292 deletions
@@ -317,7 +317,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return return
} }
let params = RoomDetailsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, let params = RoomDetailsScreenCoordinatorParameters(accountUserID: userSession.userID,
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
mediaProvider: userSession.mediaProvider, mediaProvider: userSession.mediaProvider,
userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy)) userDiscoveryService: UserDiscoveryService(clientProxy: userSession.clientProxy))
@@ -437,21 +437,21 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingMembersPublisher = value } set(value) { underlyingMembersPublisher = value }
} }
var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>! var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>!
var invitedMembersCount: UInt { var invitedMembersCount: Int {
get { return underlyingInvitedMembersCount } get { return underlyingInvitedMembersCount }
set(value) { underlyingInvitedMembersCount = value } set(value) { underlyingInvitedMembersCount = value }
} }
var underlyingInvitedMembersCount: UInt! var underlyingInvitedMembersCount: Int!
var joinedMembersCount: UInt { var joinedMembersCount: Int {
get { return underlyingJoinedMembersCount } get { return underlyingJoinedMembersCount }
set(value) { underlyingJoinedMembersCount = value } set(value) { underlyingJoinedMembersCount = value }
} }
var underlyingJoinedMembersCount: UInt! var underlyingJoinedMembersCount: Int!
var activeMembersCount: UInt { var activeMembersCount: Int {
get { return underlyingActiveMembersCount } get { return underlyingActiveMembersCount }
set(value) { underlyingActiveMembersCount = value } set(value) { underlyingActiveMembersCount = value }
} }
var underlyingActiveMembersCount: UInt! var underlyingActiveMembersCount: Int!
var updatesPublisher: AnyPublisher<TimelineDiff, Never> { var updatesPublisher: AnyPublisher<TimelineDiff, Never> {
get { return underlyingUpdatesPublisher } get { return underlyingUpdatesPublisher }
set(value) { underlyingUpdatesPublisher = value } set(value) { underlyingUpdatesPublisher = value }
@@ -100,11 +100,12 @@ extension RoomMemberProxyMock {
isIgnored: true)) isIgnored: true))
} }
static func mockOwner(allowedStateEvents: [StateEventType]) -> RoomMemberProxyMock { static func mockOwner(allowedStateEvents: [StateEventType], canInviteUsers: Bool = true) -> RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@foo:some.org", RoomMemberProxyMock(with: .init(userID: "@foo:some.org",
displayName: "User owner", displayName: "User owner",
membership: .join, membership: .join,
isAccountOwner: true, isAccountOwner: true,
canInviteUsers: canInviteUsers,
canSendStateEvent: { allowedStateEvents.contains($0) })) canSendStateEvent: { allowedStateEvents.contains($0) }))
} }
} }
+5 -3
View File
@@ -33,10 +33,11 @@ struct RoomProxyMockConfiguration {
var hasUnreadNotifications = Bool.random() var hasUnreadNotifications = Bool.random()
var members: [RoomMemberProxyProtocol]? var members: [RoomMemberProxyProtocol]?
var inviter: RoomMemberProxyMock? var inviter: RoomMemberProxyMock?
var memberForID: RoomMemberProxyMock = .mockMe
var invitedMembersCount: UInt = 100 var invitedMembersCount = 100
var joinedMembersCount: UInt = 50 var joinedMembersCount = 50
var activeMembersCount: UInt = 25 var activeMembersCount = 25
} }
extension RoomProxyMock { extension RoomProxyMock {
@@ -77,5 +78,6 @@ extension RoomProxyMock {
underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher() underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher()
setNameClosure = { _ in .success(()) } setNameClosure = { _ in .success(()) }
setTopicClosure = { _ in .success(()) } setTopicClosure = { _ in .success(()) }
getMemberUserIDReturnValue = .success(configuration.memberForID)
} }
} }
@@ -27,7 +27,7 @@ protocol UserIndicatorControllerProtocol: CoordinatorProtocol {
extension UserIndicatorControllerProtocol { extension UserIndicatorControllerProtocol {
/// Allows to submit a delayed indicator, this returns a Task so that it's also possible to cancel the action /// Allows to submit a delayed indicator, this returns a Task so that it's also possible to cancel the action
func submitIndicator(_ indicator: UserIndicator, delay: Duration) -> Task<Void, Never> { func submitIndicator(_ indicator: UserIndicator, delay: Duration) -> Task<Void, Never> {
Task { Task { @MainActor in
try? await Task.sleep(for: delay) try? await Task.sleep(for: delay)
guard !Task.isCancelled else { return } guard !Task.isCancelled else { return }
submitIndicator(indicator) submitIndicator(indicator)
@@ -22,6 +22,7 @@ struct InviteUsersScreenCoordinatorParameters {
let roomType: InviteUsersScreenRoomType let roomType: InviteUsersScreenRoomType
let mediaProvider: MediaProviderProtocol let mediaProvider: MediaProviderProtocol
let userDiscoveryService: UserDiscoveryServiceProtocol let userDiscoveryService: UserDiscoveryServiceProtocol
weak var userIndicatorController: UserIndicatorControllerProtocol?
} }
enum InviteUsersScreenCoordinatorAction { enum InviteUsersScreenCoordinatorAction {
@@ -46,7 +47,8 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers, viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers,
roomType: parameters.roomType, roomType: parameters.roomType,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
userDiscoveryService: parameters.userDiscoveryService) userDiscoveryService: parameters.userDiscoveryService,
userIndicatorController: parameters.userIndicatorController)
} }
func start() { func start() {
@@ -29,7 +29,7 @@ enum InviteUsersScreenViewModelAction {
enum InviteUsersScreenRoomType { enum InviteUsersScreenRoomType {
case draft case draft
case room(members: [RoomMemberProxyProtocol], userIndicatorController: UserIndicatorControllerProtocol) case room(roomProxy: RoomProxyProtocol)
} }
struct InviteUsersScreenViewState: BindableState { struct InviteUsersScreenViewState: BindableState {
@@ -24,7 +24,9 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private let mediaProvider: MediaProviderProtocol private let mediaProvider: MediaProviderProtocol
private let userDiscoveryService: UserDiscoveryServiceProtocol private let userDiscoveryService: UserDiscoveryServiceProtocol
private let roomType: InviteUsersScreenRoomType private let roomType: InviteUsersScreenRoomType
private weak var userIndicatorController: UserIndicatorControllerProtocol?
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init() private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
@CancellableTask private var showLoaderTask: Task<Void, Never>?
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> { var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
@@ -33,14 +35,16 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
init(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>, init(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
roomType: InviteUsersScreenRoomType, roomType: InviteUsersScreenRoomType,
mediaProvider: MediaProviderProtocol, mediaProvider: MediaProviderProtocol,
userDiscoveryService: UserDiscoveryServiceProtocol) { userDiscoveryService: UserDiscoveryServiceProtocol,
userIndicatorController: UserIndicatorControllerProtocol?) {
self.roomType = roomType self.roomType = roomType
self.mediaProvider = mediaProvider self.mediaProvider = mediaProvider
self.userDiscoveryService = userDiscoveryService self.userDiscoveryService = userDiscoveryService
self.userIndicatorController = userIndicatorController
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider) super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider)
buildMembershipStateIfNeeded()
setupSubscriptions(selectedUsers: selectedUsers) setupSubscriptions(selectedUsers: selectedUsers)
fetchMembersIfNeeded()
} }
// MARK: - Public // MARK: - Public
@@ -61,12 +65,10 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
} }
} }
private func buildMembershipStateIfNeeded() { // MARK: - Private
guard case let .room(members, userIndicatorController) = roomType else {
return private func buildMembershipStateIfNeeded(members: [RoomMemberProxyProtocol]) {
} showLoader()
let indicatorID = UUID().uuidString
userIndicatorController.submitIndicator(UserIndicator(id: indicatorID, type: .modal, title: L10n.commonLoading, persistent: true))
Task.detached { [members] in Task.detached { [members] in
// accessing RoomMember's properties is very slow. We need to do it in a background thread. // accessing RoomMember's properties is very slow. We need to do it in a background thread.
@@ -77,13 +79,11 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
Task { @MainActor in Task { @MainActor in
self.state.membershipState = membershipState self.state.membershipState = membershipState
userIndicatorController.retractIndicatorWithId(indicatorID) self.hideLoader()
} }
} }
} }
// MARK: - Private
@CancellableTask @CancellableTask
private var fetchUsersTask: Task<Void, Never>? private var fetchUsersTask: Task<Void, Never>?
@@ -103,6 +103,26 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
.store(in: &cancellables) .store(in: &cancellables)
} }
private func fetchMembersIfNeeded() {
guard case let .room(roomProxy) = roomType else {
return
}
Task {
showLoader()
await roomProxy.updateMembers()
hideLoader()
}
roomProxy.membersPublisher
.filter { !$0.isEmpty }
.first()
.sink { [weak self] members in
self?.buildMembershipStateIfNeeded(members: members)
}
.store(in: &cancellables)
}
private func fetchUsers() { private func fetchUsers() {
guard searchQuery.count >= 3 else { guard searchQuery.count >= 3 else {
fetchSuggestions() fetchSuggestions()
@@ -139,6 +159,17 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private var searchQuery: String { private var searchQuery: String {
context.searchQuery context.searchQuery
} }
private let userIndicatorID = UUID().uuidString
private func showLoader() {
showLoaderTask = userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200))
}
private func hideLoader() {
showLoaderTask = nil
userIndicatorController?.retractIndicatorWithId(userIndicatorID)
}
} }
private extension InviteUsersScreenRoomType { private extension InviteUsersScreenRoomType {
@@ -140,7 +140,7 @@ struct InviteUsersScreen_Previews: PreviewProvider {
let userDiscoveryService = UserDiscoveryServiceMock() let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.fetchSuggestionsReturnValue = .success([.mockAlice]) userDiscoveryService.fetchSuggestionsReturnValue = .success([.mockAlice])
userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice]) userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice])
return InviteUsersScreenViewModel(selectedUsers: .init([]), roomType: .draft, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService) return InviteUsersScreenViewModel(selectedUsers: .init([]), roomType: .draft, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService, userIndicatorController: UserIndicatorControllerMock())
}() }()
static var previews: some View { static var previews: some View {
@@ -20,9 +20,9 @@ import SwiftUI
struct RoomDetailsEditScreenCoordinatorParameters { struct RoomDetailsEditScreenCoordinatorParameters {
let accountOwner: RoomMemberProxyProtocol let accountOwner: RoomMemberProxyProtocol
let mediaProvider: MediaProviderProtocol let mediaProvider: MediaProviderProtocol
let navigationStackCoordinator: NavigationStackCoordinator weak var navigationStackCoordinator: NavigationStackCoordinator?
let roomProxy: RoomProxyProtocol let roomProxy: RoomProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol weak var userIndicatorController: UserIndicatorControllerProtocol?
} }
enum RoomDetailsEditScreenCoordinatorAction { enum RoomDetailsEditScreenCoordinatorAction {
@@ -77,14 +77,14 @@ final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .cancel: case .cancel:
parameters.navigationStackCoordinator.setSheetCoordinator(nil) parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
case .selectMediaAtURL(let url): case .selectMediaAtURL(let url):
parameters.navigationStackCoordinator.setSheetCoordinator(nil) parameters.navigationStackCoordinator?.setSheetCoordinator(nil)
viewModel.didSelectMediaUrl(url: url) viewModel.didSelectMediaUrl(url: url)
} }
} }
stackCoordinator.setRootCoordinator(mediaPickerCoordinator) stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController) parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController)
} }
} }
@@ -22,7 +22,7 @@ typealias RoomDetailsEditScreenViewModelType = StateStoreViewModel<RoomDetailsEd
class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDetailsEditScreenViewModelProtocol { class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDetailsEditScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<RoomDetailsEditScreenViewModelAction, Never> = .init() private let actionsSubject: PassthroughSubject<RoomDetailsEditScreenViewModelAction, Never> = .init()
private let roomProxy: RoomProxyProtocol private let roomProxy: RoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol private weak var userIndicatorController: UserIndicatorControllerProtocol?
private let mediaPreprocessor: MediaUploadingPreprocessor = .init() private let mediaPreprocessor: MediaUploadingPreprocessor = .init()
var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> { var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> {
@@ -32,7 +32,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
init(accountOwner: RoomMemberProxyProtocol, init(accountOwner: RoomMemberProxyProtocol,
mediaProvider: MediaProviderProtocol, mediaProvider: MediaProviderProtocol,
roomProxy: RoomProxyProtocol, roomProxy: RoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) { userIndicatorController: UserIndicatorControllerProtocol?) {
self.roomProxy = roomProxy self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
@@ -75,12 +75,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
Task { Task {
let userIndicatorID = UUID().uuidString let userIndicatorID = UUID().uuidString
defer { defer {
userIndicatorController.retractIndicatorWithId(userIndicatorID) userIndicatorController?.retractIndicatorWithId(userIndicatorID)
} }
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID,
type: .modal(interactiveDismissDisabled: true), type: .modal(interactiveDismissDisabled: true),
title: L10n.commonLoading, title: L10n.commonLoading,
persistent: true)) persistent: true))
let mediaResult = await mediaPreprocessor.processMedia(at: url) let mediaResult = await mediaPreprocessor.processMedia(at: url)
@@ -88,7 +88,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
case .success(.image): case .success(.image):
state.localMedia = try? mediaResult.get() state.localMedia = try? mediaResult.get()
case .failure, .success: case .failure, .success:
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown) userIndicatorController?.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown)
} }
} }
} }
@@ -99,12 +99,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
Task { Task {
let userIndicatorID = UUID().uuidString let userIndicatorID = UUID().uuidString
defer { defer {
userIndicatorController.retractIndicatorWithId(userIndicatorID) userIndicatorController?.retractIndicatorWithId(userIndicatorID)
} }
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, userIndicatorController?.submitIndicator(UserIndicator(id: userIndicatorID,
type: .modal(interactiveDismissDisabled: true), type: .modal(interactiveDismissDisabled: true),
title: L10n.screenRoomDetailsUpdatingRoom, title: L10n.screenRoomDetailsUpdatingRoom,
persistent: true)) persistent: true))
do { do {
try await withThrowingTaskGroup(of: Void.self) { group in try await withThrowingTaskGroup(of: Void.self) { group in
@@ -135,9 +135,9 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
actionsSubject.send(.saveFinished) actionsSubject.send(.saveFinished)
} catch { } catch {
userIndicatorController.alertInfo = .init(id: .init(), userIndicatorController?.alertInfo = .init(id: .init(),
title: L10n.screenRoomDetailsEditionErrorTitle, title: L10n.screenRoomDetailsEditionErrorTitle,
message: L10n.screenRoomDetailsEditionError) message: L10n.screenRoomDetailsEditionError)
} }
} }
} }
@@ -18,7 +18,8 @@ import Combine
import SwiftUI import SwiftUI
struct RoomDetailsScreenCoordinatorParameters { struct RoomDetailsScreenCoordinatorParameters {
let navigationStackCoordinator: NavigationStackCoordinator let accountUserID: String
weak var navigationStackCoordinator: NavigationStackCoordinator?
let roomProxy: RoomProxyProtocol let roomProxy: RoomProxyProtocol
let mediaProvider: MediaProviderProtocol let mediaProvider: MediaProviderProtocol
let userDiscoveryService: UserDiscoveryServiceProtocol let userDiscoveryService: UserDiscoveryServiceProtocol
@@ -33,7 +34,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
private var viewModel: RoomDetailsScreenViewModelProtocol private var viewModel: RoomDetailsScreenViewModelProtocol
private var cancellables: Set<AnyCancellable> = .init() private var cancellables: Set<AnyCancellable> = .init()
private let selectedUsers: CurrentValueSubject<[UserProfileProxy], Never> = .init([]) private let selectedUsers: CurrentValueSubject<[UserProfileProxy], Never> = .init([])
private var navigationStackCoordinator: NavigationStackCoordinator { private var navigationStackCoordinator: NavigationStackCoordinator? {
parameters.navigationStackCoordinator parameters.navigationStackCoordinator
} }
@@ -42,7 +43,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
init(parameters: RoomDetailsScreenCoordinatorParameters) { init(parameters: RoomDetailsScreenCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
viewModel = RoomDetailsScreenViewModel(roomProxy: parameters.roomProxy, viewModel = RoomDetailsScreenViewModel(accountUserID: parameters.accountUserID,
roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider) mediaProvider: parameters.mediaProvider)
} }
@@ -53,10 +55,10 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .requestMemberDetailsPresentation(let members): case .requestMemberDetailsPresentation:
self.presentRoomMembersList(members) self.presentRoomMembersList()
case .requestInvitePeoplePresentation(let members): case .requestInvitePeoplePresentation:
self.presentInviteUsersScreen(members: members) self.presentInviteUsersScreen()
case .leftRoom: case .leftRoom:
self.callback?(.leftRoom) self.callback?(.leftRoom)
case .requestEditDetailsPresentation(let accountOwner): case .requestEditDetailsPresentation(let accountOwner):
@@ -71,32 +73,32 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
// MARK: - Private // MARK: - Private
private func presentRoomMembersList(_ members: [RoomMemberProxyProtocol]) { private func presentRoomMembersList() {
let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, let params = RoomMembersListScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
members: members) roomProxy: parameters.roomProxy)
let coordinator = RoomMembersListScreenCoordinator(parameters: params) let coordinator = RoomMembersListScreenCoordinator(parameters: params)
coordinator.callback = { [weak self] action in coordinator.callback = { [weak self] action in
switch action { switch action {
case .invite: case .invite:
self?.presentInviteUsersScreen(members: members) self?.presentInviteUsersScreen()
} }
} }
navigationStackCoordinator.push(coordinator) navigationStackCoordinator?.push(coordinator)
} }
private func presentInviteUsersScreen(members: [RoomMemberProxyProtocol]) { private func presentInviteUsersScreen() {
let navigationStackCoordinator = NavigationStackCoordinator() let inviteUsersStackCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator) let userIndicatorController = UserIndicatorController(rootCoordinator: inviteUsersStackCoordinator)
let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsers), let inviteParameters = InviteUsersScreenCoordinatorParameters(selectedUsers: .init(selectedUsers),
roomType: .room(members: members, userIndicatorController: userIndicatorController), roomType: .room(roomProxy: parameters.roomProxy),
mediaProvider: parameters.mediaProvider, mediaProvider: parameters.mediaProvider,
userDiscoveryService: parameters.userDiscoveryService) userDiscoveryService: parameters.userDiscoveryService, userIndicatorController: userIndicatorController)
let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters) let coordinator = InviteUsersScreenCoordinator(parameters: inviteParameters)
navigationStackCoordinator.setRootCoordinator(coordinator) inviteUsersStackCoordinator.setRootCoordinator(coordinator)
coordinator.actions.sink { [weak self] result in coordinator.actions.sink { [weak self] result in
guard let self else { return } guard let self else { return }
@@ -112,7 +114,9 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
} }
.store(in: &cancellables) .store(in: &cancellables)
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController) parameters.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController) { [weak self] in
self?.selectedUsers.value = []
}
} }
private func presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) { private func presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) {
@@ -129,14 +133,14 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
roomDetailsEditCoordinator.actions.sink { [weak self] action in roomDetailsEditCoordinator.actions.sink { [weak self] action in
switch action { switch action {
case .dismiss: case .dismiss:
self?.parameters.navigationStackCoordinator.setSheetCoordinator(nil) self?.navigationStackCoordinator?.setSheetCoordinator(nil)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(roomDetailsEditCoordinator) navigationStackCoordinator.setRootCoordinator(roomDetailsEditCoordinator)
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController) self.navigationStackCoordinator?.setSheetCoordinator(userIndicatorController)
} }
private func toggleUser(_ user: UserProfileProxy) { private func toggleUser(_ user: UserProfileProxy) {
@@ -150,7 +154,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
} }
private func inviteUsers(_ users: [String], in room: RoomProxyProtocol) { private func inviteUsers(_ users: [String], in room: RoomProxyProtocol) {
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator?.setSheetCoordinator(nil)
Task { Task {
let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in let result: Result<Void, RoomProxyError> = await withTaskGroup(of: Result<Void, RoomProxyError>.self) { group in
@@ -22,8 +22,8 @@ import UIKit
// MARK: View model // MARK: View model
enum RoomDetailsScreenViewModelAction { enum RoomDetailsScreenViewModelAction {
case requestMemberDetailsPresentation([RoomMemberProxyProtocol]) case requestMemberDetailsPresentation
case requestInvitePeoplePresentation([RoomMemberProxyProtocol]) case requestInvitePeoplePresentation
case leftRoom case leftRoom
case requestEditDetailsPresentation(RoomMemberProxyProtocol) case requestEditDetailsPresentation(RoomMemberProxyProtocol)
} }
@@ -35,22 +35,18 @@ struct RoomDetailsScreenViewState: BindableState {
let canonicalAlias: String? let canonicalAlias: String?
let isEncrypted: Bool let isEncrypted: Bool
let isDirect: Bool let isDirect: Bool
let permalink: URL?
var title = "" var title = ""
var topic: String? var topic: String?
var avatarURL: URL? var avatarURL: URL?
let permalink: URL? var joinedMembersCount: Int
var members: [RoomMemberDetails] = []
var joinedMembersCount = 0
var isProcessingIgnoreRequest = false var isProcessingIgnoreRequest = false
var canInviteUsers = false var canInviteUsers = false
var canEditRoomName = false var canEditRoomName = false
var canEditRoomTopic = false var canEditRoomTopic = false
var canEditRoomAvatar = false var canEditRoomAvatar = false
var isLoadingMembers: Bool {
members.isEmpty
}
var canEdit: Bool { var canEdit: Bool {
!isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar) !isDirect && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
} }
@@ -62,10 +58,6 @@ struct RoomDetailsScreenViewState: BindableState {
var bindings: RoomDetailsScreenViewStateBindings var bindings: RoomDetailsScreenViewStateBindings
var dmRecipient: RoomMemberDetails? var dmRecipient: RoomMemberDetails?
private var isDMRoom: Bool {
isEncrypted && isDirect && members.count == 2
}
} }
struct RoomDetailsScreenViewStateBindings { struct RoomDetailsScreenViewStateBindings {
@@ -20,35 +20,36 @@ import SwiftUI
typealias RoomDetailsScreenViewModelType = StateStoreViewModel<RoomDetailsScreenViewState, RoomDetailsScreenViewAction> typealias RoomDetailsScreenViewModelType = StateStoreViewModel<RoomDetailsScreenViewState, RoomDetailsScreenViewAction>
class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol { class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol {
private let accountUserID: String
private let roomProxy: RoomProxyProtocol private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxyProtocol] = []
private var dmRecipient: RoomMemberProxyProtocol?
private var accountOwner: RoomMemberProxyProtocol?
@CancellableTask private var accountOwner: RoomMemberProxyProtocol? {
private var buildMembersDetailsTask: Task<Void, Never>? didSet { updatePowerLevelPermissions() }
}
private var dmRecipient: RoomMemberProxyProtocol?
var callback: ((RoomDetailsScreenViewModelAction) -> Void)? var callback: ((RoomDetailsScreenViewModelAction) -> Void)?
init(roomProxy: RoomProxyProtocol, init(accountUserID: String,
roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol) { mediaProvider: MediaProviderProtocol) {
self.accountUserID = accountUserID
self.roomProxy = roomProxy self.roomProxy = roomProxy
super.init(initialViewState: .init(roomId: roomProxy.id, super.init(initialViewState: .init(roomId: roomProxy.id,
canonicalAlias: roomProxy.canonicalAlias, canonicalAlias: roomProxy.canonicalAlias,
isEncrypted: roomProxy.isEncrypted, isEncrypted: roomProxy.isEncrypted,
isDirect: roomProxy.isDirect, isDirect: roomProxy.isDirect,
permalink: roomProxy.permalink,
title: roomProxy.roomTitle, title: roomProxy.roomTitle,
topic: roomProxy.topic, topic: roomProxy.topic,
avatarURL: roomProxy.avatarURL, avatarURL: roomProxy.avatarURL,
permalink: roomProxy.permalink, joinedMembersCount: roomProxy.joinedMembersCount,
bindings: .init()), bindings: .init()),
imageProvider: mediaProvider) imageProvider: mediaProvider)
setupSubscriptions() setupRoomSubscription()
fetchMembers()
Task {
await roomProxy.updateMembers()
}
} }
// MARK: - Public // MARK: - Public
@@ -57,12 +58,11 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
override func process(viewAction: RoomDetailsScreenViewAction) { override func process(viewAction: RoomDetailsScreenViewAction) {
switch viewAction { switch viewAction {
case .processTapPeople: case .processTapPeople:
callback?(.requestMemberDetailsPresentation(members)) callback?(.requestMemberDetailsPresentation)
case .processTapInvite: case .processTapInvite:
callback?(.requestInvitePeoplePresentation(members)) callback?(.requestInvitePeoplePresentation)
case .processTapLeave: case .processTapLeave:
let joinedMembers = members.filter { $0.membership == .join } guard state.joinedMembersCount > 1 else {
guard joinedMembers.count > 1 else {
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomId: roomProxy.id, state: .empty) state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomId: roomProxy.id, state: .empty)
return return
} }
@@ -88,7 +88,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
// MARK: - Private // MARK: - Private
private func setupSubscriptions() { private func setupRoomSubscription() {
switch roomProxy.registerTimelineListenerIfNeeded() { switch roomProxy.registerTimelineListenerIfNeeded() {
case .success, .failure(.roomListenerAlreadyRegistered): case .success, .failure(.roomListenerAlreadyRegistered):
break break
@@ -96,32 +96,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
MXLog.error("Failed to register a room listener in room's details for the room \(roomProxy.id)") MXLog.error("Failed to register a room listener in room's details for the room \(roomProxy.id)")
} }
roomProxy.membersPublisher
.sink { [weak self] members in
guard let self else { return }
buildMembersDetailsTask = Task {
let roomMembersDetails = await self.buildMembersDetails(members: members)
guard !Task.isCancelled else { return }
if self.roomProxy.isDirect, self.roomProxy.isEncrypted, members.count == 2 {
self.dmRecipient = members.first(where: { !$0.isAccountOwner })
}
self.state.members = roomMembersDetails.members
self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
self.state.canEditRoomName = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomName) ?? false
self.state.canEditRoomTopic = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomTopic) ?? false
self.state.canEditRoomAvatar = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false
self.members = members
self.accountOwner = roomMembersDetails.accountOwner
}
}
.store(in: &cancellables)
roomProxy.updatesPublisher roomProxy.updatesPublisher
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in .sink { [weak self] _ in
@@ -129,33 +103,50 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
self.state.title = self.roomProxy.roomTitle self.state.title = self.roomProxy.roomTitle
self.state.topic = self.roomProxy.topic self.state.topic = self.roomProxy.topic
self.state.avatarURL = self.roomProxy.avatarURL self.state.avatarURL = self.roomProxy.avatarURL
self.state.joinedMembersCount = self.roomProxy.joinedMembersCount
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails { private func fetchMembers() {
await Task.detached { Task {
// accessing RoomMember's properties is very slow. We need to do it in a background thread. await fetchMembersIfNeeded()
var roomMembersDetails: [RoomMemberDetails] = [] await fetchAccountOwner()
var accountOwner: RoomMemberProxyProtocol?
var joinedMembersCount = 0
roomMembersDetails.reserveCapacity(members.count)
for member in members {
roomMembersDetails.append(RoomMemberDetails(withProxy: member))
if member.membership == .join {
joinedMembersCount += 1
}
if accountOwner == nil, member.isAccountOwner {
accountOwner = member
}
}
return .init(members: roomMembersDetails, joinedMembersCount: joinedMembersCount, accountOwner: accountOwner)
} }
.value }
private func fetchMembersIfNeeded() async {
// We need to fetch members just in 1-to-1 chat to get the member object for the other person
guard roomProxy.isEncryptedOneToOneRoom else {
return
}
roomProxy.membersPublisher
.sink { [weak self] members in
guard let self else { return }
let dmRecipient = members.first(where: { !$0.isAccountOwner })
self.dmRecipient = dmRecipient
self.state.dmRecipient = dmRecipient.map(RoomMemberDetails.init(withProxy:))
}
.store(in: &cancellables)
await roomProxy.updateMembers()
}
private func fetchAccountOwner() async {
switch await roomProxy.getMember(userID: accountUserID) {
case .success(let member):
accountOwner = member
case .failure(let error):
MXLog.error("Failed (error: \(error) to get account owner member with id: \(accountUserID), in room: \(roomProxy.id)")
}
}
private func updatePowerLevelPermissions() {
state.canInviteUsers = accountOwner?.canInviteUsers ?? false
state.canEditRoomName = accountOwner?.canSendStateEvent(type: .roomName) ?? false
state.canEditRoomTopic = accountOwner?.canSendStateEvent(type: .roomTopic) ?? false
state.canEditRoomAvatar = accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false
} }
private static let leaveRoomLoadingID = "LeaveRoomLoading" private static let leaveRoomLoadingID = "LeaveRoomLoading"
@@ -196,9 +187,3 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
} }
} }
} }
private struct RoomMembersDetails {
let members: [RoomMemberDetails]
let joinedMembersCount: Int
let accountOwner: RoomMemberProxyProtocol?
}
@@ -137,13 +137,9 @@ struct RoomDetailsScreen: View {
context.send(viewAction: .processTapPeople) context.send(viewAction: .processTapPeople)
} label: { } label: {
LabeledContent { LabeledContent {
if context.viewState.isLoadingMembers { Text(String(context.viewState.joinedMembersCount))
ProgressView() .foregroundColor(.element.tertiaryContent)
} else { .font(.compound.bodyLG)
Text(String(context.viewState.joinedMembersCount))
.foregroundColor(.element.tertiaryContent)
.font(.compound.bodyLG)
}
} label: { } label: {
Label(L10n.commonPeople, systemImage: "person") Label(L10n.commonPeople, systemImage: "person")
} }
@@ -160,9 +156,8 @@ struct RoomDetailsScreen: View {
} }
} }
.listRowSeparatorTint(.element.quinaryContent) .listRowSeparatorTint(.element.quinaryContent)
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink)) .buttonStyle(FormButtonStyle(accessory: .navigationLink))
.foregroundColor(.element.primaryContent) .foregroundColor(.element.primaryContent)
.disabled(context.viewState.isLoadingMembers)
} }
@ViewBuilder @ViewBuilder
@@ -257,7 +252,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
canonicalAlias: "#alias:domain.com", canonicalAlias: "#alias:domain.com",
members: members)) members: members))
return RoomDetailsScreenViewModel(roomProxy: roomProxy, return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com",
roomProxy: roomProxy,
mediaProvider: MockMediaProvider()) mediaProvider: MockMediaProvider())
}() }()
@@ -274,7 +270,8 @@ struct RoomDetailsScreen_Previews: PreviewProvider {
canonicalAlias: "#alias:domain.com", canonicalAlias: "#alias:domain.com",
members: members)) members: members))
return RoomDetailsScreenViewModel(roomProxy: roomProxy, return RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com",
roomProxy: roomProxy,
mediaProvider: MockMediaProvider()) mediaProvider: MockMediaProvider())
}() }()
@@ -17,9 +17,9 @@
import SwiftUI import SwiftUI
struct RoomMembersListScreenCoordinatorParameters { struct RoomMembersListScreenCoordinatorParameters {
let navigationStackCoordinator: NavigationStackCoordinator weak var navigationStackCoordinator: NavigationStackCoordinator?
let mediaProvider: MediaProviderProtocol let mediaProvider: MediaProviderProtocol
let members: [RoomMemberProxyProtocol] let roomProxy: RoomProxyProtocol
} }
enum RoomMembersListScreenCoordinatorAction { enum RoomMembersListScreenCoordinatorAction {
@@ -29,15 +29,17 @@ enum RoomMembersListScreenCoordinatorAction {
final class RoomMembersListScreenCoordinator: CoordinatorProtocol { final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomMembersListScreenCoordinatorParameters private let parameters: RoomMembersListScreenCoordinatorParameters
private var viewModel: RoomMembersListScreenViewModelProtocol private var viewModel: RoomMembersListScreenViewModelProtocol
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator } private var navigationStackCoordinator: NavigationStackCoordinator? {
parameters.navigationStackCoordinator
}
var callback: ((RoomMembersListScreenCoordinatorAction) -> Void)? var callback: ((RoomMembersListScreenCoordinatorAction) -> Void)?
init(parameters: RoomMembersListScreenCoordinatorParameters) { init(parameters: RoomMembersListScreenCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
viewModel = RoomMembersListScreenViewModel(mediaProvider: parameters.mediaProvider, viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy,
members: parameters.members) mediaProvider: parameters.mediaProvider)
} }
func start() { func start() {
@@ -63,6 +65,6 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomMemberProxy: member, mediaProvider: parameters.mediaProvider) let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomMemberProxy: member, mediaProvider: parameters.mediaProvider)
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: parameters) let coordinator = RoomMemberDetailsScreenCoordinator(parameters: parameters)
navigationStackCoordinator.push(coordinator) navigationStackCoordinator?.push(coordinator)
} }
} }
@@ -24,10 +24,16 @@ enum RoomMembersListScreenViewModelAction {
struct RoomMembersListScreenViewState: BindableState { struct RoomMembersListScreenViewState: BindableState {
private var joinedMembers: [RoomMemberDetails] private var joinedMembers: [RoomMemberDetails]
private var invitedMembers: [RoomMemberDetails] private var invitedMembers: [RoomMemberDetails]
let joinedMembersCount: Int
var canInviteUsers = false var canInviteUsers = false
var bindings: RoomMembersListScreenViewStateBindings var bindings: RoomMembersListScreenViewStateBindings
init(joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], bindings: RoomMembersListScreenViewStateBindings = .init()) { init(joinedMembersCount: Int,
joinedMembers: [RoomMemberDetails] = [],
invitedMembers: [RoomMemberDetails] = [],
bindings: RoomMembersListScreenViewStateBindings = .init()) {
self.joinedMembersCount = joinedMembersCount
self.joinedMembers = joinedMembers self.joinedMembers = joinedMembers
self.invitedMembers = invitedMembers self.invitedMembers = invitedMembers
self.bindings = bindings self.bindings = bindings
@@ -46,10 +52,6 @@ struct RoomMembersListScreenViewState: BindableState {
member.matches(searchQuery: bindings.searchQuery) member.matches(searchQuery: bindings.searchQuery)
} }
} }
var joinedMembersCount: Int {
joinedMembers.count
}
} }
struct RoomMembersListScreenViewStateBindings { struct RoomMembersListScreenViewStateBindings {
@@ -19,17 +19,18 @@ import SwiftUI
typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction> typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction>
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol { class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
private let mediaProvider: MediaProviderProtocol private let roomProxy: RoomProxyProtocol
private let members: [RoomMemberProxyProtocol] private var members: [RoomMemberProxyProtocol] = []
@CancellableTask private var showLoaderTask: Task<Void, Never>?
var callback: ((RoomMembersListScreenViewModelAction) -> Void)? var callback: ((RoomMembersListScreenViewModelAction) -> Void)?
init(mediaProvider: MediaProviderProtocol, members: [RoomMemberProxyProtocol]) { init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol) {
self.mediaProvider = mediaProvider self.roomProxy = roomProxy
self.members = members super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount),
super.init(initialViewState: .init(), imageProvider: mediaProvider) imageProvider: mediaProvider)
setupState(members: members) setupMembers()
} }
// MARK: - Public // MARK: - Public
@@ -49,17 +50,32 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
// MARK: - Private // MARK: - Private
private func setupState(members: [RoomMemberProxyProtocol]) { private func setupMembers() {
Task { Task {
let indicatorId = UUID().uuidString showLoader()
defer { await roomProxy.updateMembers()
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(indicatorId) hideLoader()
} }
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: indicatorId, type: .modal, title: L10n.commonLoading, persistent: true))
roomProxy.membersPublisher
.filter { !$0.isEmpty }
.first()
.sink { [weak self] members in
self?.updateState(members: members)
}
.store(in: &cancellables)
}
private func updateState(members: [RoomMemberProxyProtocol]) {
Task {
showLoader()
let roomMembersDetails = await buildMembersDetails(members: members) let roomMembersDetails = await buildMembersDetails(members: members)
self.state = .init(joinedMembers: roomMembersDetails.joinedMembers, invitedMembers: roomMembersDetails.invitedMembers) self.members = members
self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount,
joinedMembers: roomMembersDetails.joinedMembers,
invitedMembers: roomMembersDetails.invitedMembers)
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
hideLoader()
} }
} }
@@ -89,6 +105,17 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
} }
.value .value
} }
private let userIndicatorID = UUID().uuidString
private func showLoader() {
showLoaderTask = ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true), delay: .milliseconds(200))
}
private func hideLoader() {
showLoaderTask = nil
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(userIndicatorID)
}
} }
private struct RoomMembersDetails { private struct RoomMembersDetails {
@@ -26,7 +26,7 @@ struct RoomMembersListScreen: View {
ScrollView { ScrollView {
LazyVStack(alignment: .leading) { LazyVStack(alignment: .leading) {
membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle) membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle)
membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(context.viewState.joinedMembersCount)) membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount)))
} }
} }
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always)) .searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
@@ -80,8 +80,8 @@ struct RoomMembersListScreen_Previews: PreviewProvider {
.mockBob, .mockBob,
.mockCharlie .mockCharlie
] ]
return RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(), return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
members: members) mediaProvider: MockMediaProvider())
}() }()
static var previews: some View { static var previews: some View {
@@ -53,8 +53,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider {
.mockBob, .mockBob,
.mockCharlie .mockCharlie
] ]
let viewModel = RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(), let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
members: members) mediaProvider: MockMediaProvider())
return VStack { return VStack {
ForEach(members, id: \.userID) { member in ForEach(members, id: \.userID) { member in
@@ -20,7 +20,7 @@ import SwiftUI
struct StartChatScreenCoordinatorParameters { struct StartChatScreenCoordinatorParameters {
let userSession: UserSessionProtocol let userSession: UserSessionProtocol
weak var userIndicatorController: UserIndicatorControllerProtocol? weak var userIndicatorController: UserIndicatorControllerProtocol?
let navigationStackCoordinator: NavigationStackCoordinator weak var navigationStackCoordinator: NavigationStackCoordinator?
let userDiscoveryService: UserDiscoveryServiceProtocol let userDiscoveryService: UserDiscoveryServiceProtocol
} }
@@ -45,7 +45,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
selectedUsers.asCurrentValuePublisher() selectedUsers.asCurrentValuePublisher()
} }
private var navigationStackCoordinator: NavigationStackCoordinator { private var navigationStackCoordinator: NavigationStackCoordinator? {
parameters.navigationStackCoordinator parameters.navigationStackCoordinator
} }
@@ -107,7 +107,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
} }
.store(in: &cancellables) .store(in: &cancellables)
navigationStackCoordinator.push(coordinator) { [weak self] in navigationStackCoordinator?.push(coordinator) { [weak self] in
self?.createRoomParameters.send(.init()) self?.createRoomParameters.send(.init())
self?.selectedUsers.send([]) self?.selectedUsers.send([])
} }
@@ -138,7 +138,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
} }
.store(in: &cancellables) .store(in: &cancellables)
navigationStackCoordinator.push(coordinator) navigationStackCoordinator?.push(coordinator)
} }
// MARK: - Private // MARK: - Private
@@ -152,7 +152,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
guard let self else { return } guard let self else { return }
switch action { switch action {
case .cancel: case .cancel:
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator?.setSheetCoordinator(nil)
case .selectMediaAtURL(let url): case .selectMediaAtURL(let url):
processAvatar(from: url) processAvatar(from: url)
} }
@@ -160,11 +160,11 @@ final class StartChatScreenCoordinator: CoordinatorProtocol {
stackCoordinator.setRootCoordinator(mediaPickerCoordinator) stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
navigationStackCoordinator.setSheetCoordinator(userIndicatorController) navigationStackCoordinator?.setSheetCoordinator(userIndicatorController)
} }
private func processAvatar(from url: URL) { private func processAvatar(from url: URL) {
navigationStackCoordinator.setSheetCoordinator(nil) navigationStackCoordinator?.setSheetCoordinator(nil)
showLoadingIndicator() showLoadingIndicator()
Task { [weak self] in Task { [weak self] in
guard let self else { return } guard let self else { return }
+11 -14
View File
@@ -120,16 +120,16 @@ class RoomProxy: RoomProxyProtocol {
return Asset.Images.encryptionTrusted.image return Asset.Images.encryptionTrusted.image
} }
var invitedMembersCount: UInt { var invitedMembersCount: Int {
UInt(room.invitedMembersCount()) Int(room.invitedMembersCount())
} }
var joinedMembersCount: UInt { var joinedMembersCount: Int {
UInt(room.joinedMembersCount()) Int(room.joinedMembersCount())
} }
var activeMembersCount: UInt { var activeMembersCount: Int {
UInt(room.activeMembersCount()) Int(room.activeMembersCount())
} }
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> { func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> {
@@ -380,11 +380,13 @@ class RoomProxy: RoomProxyProtocol {
func updateMembers() async { func updateMembers() async {
do { do {
let roomMembers = try await Task.dispatch(on: .global()) { let roomMembersProxies = try await Task.dispatch(on: .global()) {
try self.room.members() try self.room.members().map {
RoomMemberProxy(member: $0, backgroundTaskService: self.backgroundTaskService)
}
} }
membersSubject.value = buildRoomMemberProxies(members: roomMembers) membersSubject.value = roomMembersProxies
} catch { } catch {
return return
} }
@@ -422,11 +424,6 @@ class RoomProxy: RoomProxyProtocol {
} }
} }
@MainActor
private func buildRoomMemberProxies(members: [RoomMember]) -> [RoomMemberProxy] {
members.map { RoomMemberProxy(member: $0, backgroundTaskService: backgroundTaskService) }
}
func retryDecryption(for sessionID: String) async { func retryDecryption(for sessionID: String) async {
await Task.dispatch(on: .global()) { [weak self] in await Task.dispatch(on: .global()) { [weak self] in
self?.room.retryDecryption(sessionIds: [sessionID]) self?.room.retryDecryption(sessionIds: [sessionID])
@@ -66,11 +66,11 @@ protocol RoomProxyProtocol {
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get } var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get }
var invitedMembersCount: UInt { get } var invitedMembersCount: Int { get }
var joinedMembersCount: UInt { get } var joinedMembersCount: Int { get }
var activeMembersCount: UInt { get } var activeMembersCount: Int { get }
/// Publishes the room's updates. /// Publishes the room's updates.
/// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()` /// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()`
@@ -157,4 +157,8 @@ extension RoomProxyProtocol {
var roomTitle: String { var roomTitle: String {
displayName ?? name ?? "Unknown room 💥" displayName ?? name ?? "Unknown room 💥"
} }
var isEncryptedOneToOneRoom: Bool {
isDirect && isEncrypted && joinedMembersCount == 2
}
} }
@@ -306,8 +306,11 @@ class MockScreen: Identifiable {
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
displayName: "Room", displayName: "Room",
isEncrypted: true, isEncrypted: true,
members: members)) members: members,
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
userDiscoveryService: UserDiscoveryServiceMock())) userDiscoveryService: UserDiscoveryServiceMock()))
@@ -322,8 +325,11 @@ class MockScreen: Identifiable {
avatarURL: URL.picturesDirectory, avatarURL: URL.picturesDirectory,
isEncrypted: true, isEncrypted: true,
canonicalAlias: "#mock:room.org", canonicalAlias: "#mock:room.org",
members: members)) members: members,
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
userDiscoveryService: UserDiscoveryServiceMock())) userDiscoveryService: UserDiscoveryServiceMock()))
@@ -331,7 +337,8 @@ class MockScreen: Identifiable {
return navigationStackCoordinator return navigationStackCoordinator
case .roomDetailsScreenWithEmptyTopic: case .roomDetailsScreenWithEmptyTopic:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockCharlie] let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomTopic], canInviteUsers: false)
let members: [RoomMemberProxyMock] = [owner, .mockBob, .mockCharlie]
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
displayName: "Room", displayName: "Room",
topic: nil, topic: nil,
@@ -339,8 +346,11 @@ class MockScreen: Identifiable {
isDirect: false, isDirect: false,
isEncrypted: true, isEncrypted: true,
canonicalAlias: "#mock:room.org", canonicalAlias: "#mock:room.org",
members: members)) members: members,
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, memberForID: owner,
joinedMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
userDiscoveryService: UserDiscoveryServiceMock())) userDiscoveryService: UserDiscoveryServiceMock()))
@@ -348,12 +358,34 @@ class MockScreen: Identifiable {
return navigationStackCoordinator return navigationStackCoordinator
case .roomDetailsScreenWithInvite: case .roomDetailsScreenWithInvite:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockCharlie] let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [], canInviteUsers: true)
let members: [RoomMemberProxyMock] = [owner, .mockBob, .mockCharlie]
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier", let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
displayName: "Room", displayName: "Room",
isEncrypted: true, isEncrypted: true,
members: members)) members: members,
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, memberForID: owner,
joinedMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userDiscoveryService: UserDiscoveryServiceMock()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomDetailsScreenDmDetails:
let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockMe, .mockDan]
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
displayName: "Room",
topic: "test",
isDirect: true,
isEncrypted: true,
members: members,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: members.count))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(accountUserID: "@owner:somewhere.com",
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy, roomProxy: roomProxy,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
userDiscoveryService: UserDiscoveryServiceMock())) userDiscoveryService: UserDiscoveryServiceMock()))
@@ -379,7 +411,7 @@ class MockScreen: Identifiable {
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie] let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
members: members)) roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members))))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .roomMembersListScreenPendingInvites: case .roomMembersListScreenPendingInvites:
@@ -387,7 +419,7 @@ class MockScreen: Identifiable {
let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie] let members: [RoomMemberProxyMock] = [.mockInvitedAlice, .mockBob, .mockCharlie]
let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator, let coordinator = RoomMembersListScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
members: members)) roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members))))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .reportContent: case .reportContent:
@@ -433,21 +465,6 @@ class MockScreen: Identifiable {
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider())) let coordinator = RoomMemberDetailsScreenCoordinator(parameters: .init(roomMemberProxy: RoomMemberProxyMock.mockIgnored, mediaProvider: MockMediaProvider()))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .roomDetailsScreenDmDetails:
let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockMe, .mockDan]
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
displayName: "Room",
topic: "test",
isDirect: true,
isEncrypted: true,
members: members))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userDiscoveryService: UserDiscoveryServiceMock()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .invitesWithBadges: case .invitesWithBadges:
ServiceLocator.shared.settings.seenInvites = Set([RoomSummary].mockInvites.dropFirst(1).compactMap(\.id)) ServiceLocator.shared.settings.seenInvites = Set([RoomSummary].mockInvites.dropFirst(1).compactMap(\.id))
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
@@ -487,7 +504,8 @@ class MockScreen: Identifiable {
let mediaProvider = MockMediaProvider() let mediaProvider = MockMediaProvider()
let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([]) let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([])
let members: [RoomMemberProxyMock] = id == .inviteUsersInRoomExistingMembers ? [.mockInvitedAlice, .mockBob] : [] let members: [RoomMemberProxyMock] = id == .inviteUsersInRoomExistingMembers ? [.mockInvitedAlice, .mockBob] : []
let roomType: InviteUsersScreenRoomType = id == .inviteUsers ? .draft : .room(members: members, userIndicatorController: UserIndicatorControllerMock.default) let roomProxy = RoomProxyMock(with: .init(displayName: "test", members: members))
let roomType: InviteUsersScreenRoomType = id == .inviteUsers ? .draft : .room(roomProxy: roomProxy)
let coordinator = InviteUsersScreenCoordinator(parameters: .init(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: mediaProvider, userDiscoveryService: userDiscoveryMock)) let coordinator = InviteUsersScreenCoordinator(parameters: .init(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: mediaProvider, userDiscoveryService: userDiscoveryMock))
coordinator.actions.sink { action in coordinator.actions.sink { action in
switch action { switch action {
@@ -61,7 +61,7 @@ class InviteUsersScreenViewModelTests: XCTestCase {
func testInviteButton() async { func testInviteButton() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob] let mockedMembers: [RoomMemberProxyMock] = [.mockAlice, .mockBob]
setupWithRoomType(roomType: .room(members: mockedMembers, userIndicatorController: UserIndicatorControllerMock.default)) setupWithRoomType(roomType: .room(roomProxy: RoomProxyMock(with: .init(displayName: "test", members: mockedMembers))))
_ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false }) _ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false })
context.send(viewAction: .toggleUser(.mockAlice)) context.send(viewAction: .toggleUser(.mockAlice))
@@ -86,7 +86,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
userDiscoveryService.fetchSuggestionsReturnValue = .success([]) userDiscoveryService.fetchSuggestionsReturnValue = .success([])
userDiscoveryService.searchProfilesWithReturnValue = .success([]) userDiscoveryService.searchProfilesWithReturnValue = .success([])
usersSubject.send([]) usersSubject.send([])
let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(), roomType: roomType, mediaProvider: MockMediaProvider(), userDiscoveryService: userDiscoveryService) let viewModel = InviteUsersScreenViewModel(selectedUsers: usersSubject.asCurrentValuePublisher(),
roomType: roomType, mediaProvider: MockMediaProvider(),
userDiscoveryService: userDiscoveryService,
userIndicatorController: UserIndicatorControllerMock())
viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie]) viewModel.state.usersSection = .init(type: .suggestions, users: [.mockAlice, .mockBob, .mockCharlie])
self.viewModel = viewModel self.viewModel = viewModel
@@ -21,13 +21,13 @@ import XCTest
@MainActor @MainActor
class RoomDetailsScreenViewModelTests: XCTestCase { class RoomDetailsScreenViewModelTests: XCTestCase {
var viewModel: RoomDetailsScreenViewModelProtocol! var viewModel: RoomDetailsScreenViewModel!
var roomProxyMock: RoomProxyMock! var roomProxyMock: RoomProxyMock!
var context: RoomDetailsScreenViewModelType.Context { viewModel.context } var context: RoomDetailsScreenViewModelType.Context { viewModel.context }
override func setUp() { override func setUp() {
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test")) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
AppSettings.reset() AppSettings.reset()
} }
@@ -35,7 +35,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testLeaveRoomTappedWhenPublic() async { func testLeaveRoomTappedWhenPublic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice] let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() await context.nextViewState()
context.send(viewAction: .processTapLeave) context.send(viewAction: .processTapLeave)
XCTAssertEqual(context.leaveRoomAlertItem?.state, .public) XCTAssertEqual(context.leaveRoomAlertItem?.state, .public)
@@ -45,7 +45,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testLeaveRoomTappedWhenRoomNotPublic() async { func testLeaveRoomTappedWhenRoomNotPublic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice] let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: false, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() await context.nextViewState()
context.send(viewAction: .processTapLeave) context.send(viewAction: .processTapLeave)
XCTAssertEqual(context.leaveRoomAlertItem?.state, .private) XCTAssertEqual(context.leaveRoomAlertItem?.state, .private)
@@ -88,8 +88,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testInitialDMDetailsState() async { func testInitialDMDetailsState() async {
let recipient = RoomMemberProxyMock.mockDan let recipient = RoomMemberProxyMock.mockDan
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
} }
@@ -101,8 +101,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .success(()) return .success(())
} }
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -123,8 +123,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .failure(.ignoreUserFailed) return .failure(.ignoreUserFailed)
} }
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -146,8 +146,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .success(()) return .success(())
} }
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -168,8 +168,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .failure(.unignoreUserFailed) return .failure(.unignoreUserFailed)
} }
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient] let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient)) XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -186,28 +186,32 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testCannotInvitePeople() async { func testCannotInvitePeople() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice] let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test",
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) isPublic: true,
members: mockedMembers,
memberForID: .mockOwner(allowedStateEvents: [], canInviteUsers: false),
joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
XCTAssertFalse(context.viewState.canInviteUsers) XCTAssertFalse(context.viewState.canInviteUsers)
} }
func testInvitePeople() async { func testInvitePeople() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice] let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState() _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
XCTAssertTrue(context.viewState.canInviteUsers) XCTAssertTrue(context.viewState.canInviteUsers)
var callbackCorrectlyCalled = false var callbackCorrectlyCalled = false
viewModel.callback = { action in viewModel.callback = { action in
switch action { switch action {
case .requestInvitePeoplePresentation(let members): case .requestInvitePeoplePresentation:
callbackCorrectlyCalled = members.map(\.userID) == mockedMembers.map(\.userID) callbackCorrectlyCalled = true
default: default:
callbackCorrectlyCalled = false callbackCorrectlyCalled = false
} }
@@ -219,16 +223,16 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
} }
func testRoomSubscription() async { func testRoomSubscription() async {
await context.nextViewState()
XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1) XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1)
} }
func testCanEditAvatar() async { func testCanEditAvatar() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar]), .mockBob, .mockAlice] let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomAvatar])
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
XCTAssertTrue(context.viewState.canEditRoomAvatar) XCTAssertTrue(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName) XCTAssertFalse(context.viewState.canEditRoomName)
@@ -237,11 +241,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
} }
func testCanEditName() async { func testCanEditName() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomName]), .mockBob, .mockAlice] let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomName])
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertTrue(context.viewState.canEditRoomName) XCTAssertTrue(context.viewState.canEditRoomName)
@@ -250,11 +255,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
} }
func testCanEditTopic() async { func testCanEditTopic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockAlice] let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomTopic])
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName) XCTAssertFalse(context.viewState.canEditRoomName)
@@ -263,11 +269,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
} }
func testCannotEditRoom() async { func testCannotEditRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: []), .mockBob, .mockAlice] let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [])
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers)) let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers, memberForID: owner))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
XCTAssertFalse(context.viewState.canEditRoomAvatar) XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName) XCTAssertFalse(context.viewState.canEditRoomName)
@@ -278,9 +285,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testCannotEditDirectRoom() async { func testCannotEditDirectRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice] let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isPublic: false, members: mockedMembers)) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty }) _ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
XCTAssertFalse(context.viewState.canEdit) XCTAssertFalse(context.viewState.canEdit)
} }
@@ -75,6 +75,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
} }
private func setup(with members: [RoomMemberProxyMock]) { private func setup(with members: [RoomMemberProxyMock]) {
viewModel = .init(mediaProvider: MockMediaProvider(), members: members) viewModel = .init(roomProxy: RoomProxyMock(with: .init(displayName: "test", members: members, joinedMembersCount: members.filter { $0.membership == .join }.count)),
mediaProvider: MockMediaProvider())
} }
} }