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
@@ -17,9 +17,9 @@
import SwiftUI
struct RoomMembersListScreenCoordinatorParameters {
let navigationStackCoordinator: NavigationStackCoordinator
weak var navigationStackCoordinator: NavigationStackCoordinator?
let mediaProvider: MediaProviderProtocol
let members: [RoomMemberProxyProtocol]
let roomProxy: RoomProxyProtocol
}
enum RoomMembersListScreenCoordinatorAction {
@@ -29,15 +29,17 @@ enum RoomMembersListScreenCoordinatorAction {
final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomMembersListScreenCoordinatorParameters
private var viewModel: RoomMembersListScreenViewModelProtocol
private var navigationStackCoordinator: NavigationStackCoordinator { parameters.navigationStackCoordinator }
private var navigationStackCoordinator: NavigationStackCoordinator? {
parameters.navigationStackCoordinator
}
var callback: ((RoomMembersListScreenCoordinatorAction) -> Void)?
init(parameters: RoomMembersListScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = RoomMembersListScreenViewModel(mediaProvider: parameters.mediaProvider,
members: parameters.members)
viewModel = RoomMembersListScreenViewModel(roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider)
}
func start() {
@@ -63,6 +65,6 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol {
let parameters = RoomMemberDetailsScreenCoordinatorParameters(roomMemberProxy: member, mediaProvider: parameters.mediaProvider)
let coordinator = RoomMemberDetailsScreenCoordinator(parameters: parameters)
navigationStackCoordinator.push(coordinator)
navigationStackCoordinator?.push(coordinator)
}
}
@@ -24,10 +24,16 @@ enum RoomMembersListScreenViewModelAction {
struct RoomMembersListScreenViewState: BindableState {
private var joinedMembers: [RoomMemberDetails]
private var invitedMembers: [RoomMemberDetails]
let joinedMembersCount: Int
var canInviteUsers = false
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.invitedMembers = invitedMembers
self.bindings = bindings
@@ -46,10 +52,6 @@ struct RoomMembersListScreenViewState: BindableState {
member.matches(searchQuery: bindings.searchQuery)
}
}
var joinedMembersCount: Int {
joinedMembers.count
}
}
struct RoomMembersListScreenViewStateBindings {
@@ -19,17 +19,18 @@ import SwiftUI
typealias RoomMembersListScreenViewModelType = StateStoreViewModel<RoomMembersListScreenViewState, RoomMembersListScreenViewAction>
class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMembersListScreenViewModelProtocol {
private let mediaProvider: MediaProviderProtocol
private let members: [RoomMemberProxyProtocol]
private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxyProtocol] = []
@CancellableTask private var showLoaderTask: Task<Void, Never>?
var callback: ((RoomMembersListScreenViewModelAction) -> Void)?
init(mediaProvider: MediaProviderProtocol, members: [RoomMemberProxyProtocol]) {
self.mediaProvider = mediaProvider
self.members = members
super.init(initialViewState: .init(), imageProvider: mediaProvider)
init(roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol) {
self.roomProxy = roomProxy
super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount),
imageProvider: mediaProvider)
setupState(members: members)
setupMembers()
}
// MARK: - Public
@@ -49,17 +50,32 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
// MARK: - Private
private func setupState(members: [RoomMemberProxyProtocol]) {
private func setupMembers() {
Task {
let indicatorId = UUID().uuidString
defer {
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(indicatorId)
showLoader()
await roomProxy.updateMembers()
hideLoader()
}
roomProxy.membersPublisher
.filter { !$0.isEmpty }
.first()
.sink { [weak self] members in
self?.updateState(members: members)
}
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: indicatorId, type: .modal, title: L10n.commonLoading, persistent: true))
.store(in: &cancellables)
}
private func updateState(members: [RoomMemberProxyProtocol]) {
Task {
showLoader()
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
hideLoader()
}
}
@@ -89,6 +105,17 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
}
.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 {
@@ -26,7 +26,7 @@ struct RoomMembersListScreen: View {
ScrollView {
LazyVStack(alignment: .leading) {
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))
@@ -80,8 +80,8 @@ struct RoomMembersListScreen_Previews: PreviewProvider {
.mockBob,
.mockCharlie
]
return RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(),
members: members)
return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
mediaProvider: MockMediaProvider())
}()
static var previews: some View {
@@ -53,8 +53,8 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider {
.mockBob,
.mockCharlie
]
let viewModel = RoomMembersListScreenViewModel(mediaProvider: MockMediaProvider(),
members: members)
let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
mediaProvider: MockMediaProvider())
return VStack {
ForEach(members, id: \.userID) { member in