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
@@ -22,6 +22,7 @@ struct InviteUsersScreenCoordinatorParameters {
let roomType: InviteUsersScreenRoomType
let mediaProvider: MediaProviderProtocol
let userDiscoveryService: UserDiscoveryServiceProtocol
weak var userIndicatorController: UserIndicatorControllerProtocol?
}
enum InviteUsersScreenCoordinatorAction {
@@ -46,7 +47,8 @@ final class InviteUsersScreenCoordinator: CoordinatorProtocol {
viewModel = InviteUsersScreenViewModel(selectedUsers: parameters.selectedUsers,
roomType: parameters.roomType,
mediaProvider: parameters.mediaProvider,
userDiscoveryService: parameters.userDiscoveryService)
userDiscoveryService: parameters.userDiscoveryService,
userIndicatorController: parameters.userIndicatorController)
}
func start() {
@@ -29,7 +29,7 @@ enum InviteUsersScreenViewModelAction {
enum InviteUsersScreenRoomType {
case draft
case room(members: [RoomMemberProxyProtocol], userIndicatorController: UserIndicatorControllerProtocol)
case room(roomProxy: RoomProxyProtocol)
}
struct InviteUsersScreenViewState: BindableState {
@@ -24,7 +24,9 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private let mediaProvider: MediaProviderProtocol
private let userDiscoveryService: UserDiscoveryServiceProtocol
private let roomType: InviteUsersScreenRoomType
private weak var userIndicatorController: UserIndicatorControllerProtocol?
private let actionsSubject: PassthroughSubject<InviteUsersScreenViewModelAction, Never> = .init()
@CancellableTask private var showLoaderTask: Task<Void, Never>?
var actions: AnyPublisher<InviteUsersScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
@@ -33,14 +35,16 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
init(selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
roomType: InviteUsersScreenRoomType,
mediaProvider: MediaProviderProtocol,
userDiscoveryService: UserDiscoveryServiceProtocol) {
userDiscoveryService: UserDiscoveryServiceProtocol,
userIndicatorController: UserIndicatorControllerProtocol?) {
self.roomType = roomType
self.mediaProvider = mediaProvider
self.userDiscoveryService = userDiscoveryService
self.userIndicatorController = userIndicatorController
super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: selectedUsers.value, isCreatingRoom: roomType.isCreatingRoom), imageProvider: mediaProvider)
buildMembershipStateIfNeeded()
setupSubscriptions(selectedUsers: selectedUsers)
fetchMembersIfNeeded()
}
// MARK: - Public
@@ -60,13 +64,11 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
actionsSubject.send(.toggleUser(user))
}
}
// MARK: - Private
private func buildMembershipStateIfNeeded() {
guard case let .room(members, userIndicatorController) = roomType else {
return
}
let indicatorID = UUID().uuidString
userIndicatorController.submitIndicator(UserIndicator(id: indicatorID, type: .modal, title: L10n.commonLoading, persistent: true))
private func buildMembershipStateIfNeeded(members: [RoomMemberProxyProtocol]) {
showLoader()
Task.detached { [members] in
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
@@ -77,12 +79,10 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
Task { @MainActor in
self.state.membershipState = membershipState
userIndicatorController.retractIndicatorWithId(indicatorID)
self.hideLoader()
}
}
}
// MARK: - Private
@CancellableTask
private var fetchUsersTask: Task<Void, Never>?
@@ -103,6 +103,26 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
.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() {
guard searchQuery.count >= 3 else {
fetchSuggestions()
@@ -139,6 +159,17 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr
private var searchQuery: String {
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 {
@@ -140,7 +140,7 @@ struct InviteUsersScreen_Previews: PreviewProvider {
let userDiscoveryService = UserDiscoveryServiceMock()
userDiscoveryService.fetchSuggestionsReturnValue = .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 {