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
@@ -20,35 +20,36 @@ import SwiftUI
typealias RoomDetailsScreenViewModelType = StateStoreViewModel<RoomDetailsScreenViewState, RoomDetailsScreenViewAction>
class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScreenViewModelProtocol {
private let accountUserID: String
private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxyProtocol] = []
private var accountOwner: RoomMemberProxyProtocol? {
didSet { updatePowerLevelPermissions() }
}
private var dmRecipient: RoomMemberProxyProtocol?
private var accountOwner: RoomMemberProxyProtocol?
@CancellableTask
private var buildMembersDetailsTask: Task<Void, Never>?
var callback: ((RoomDetailsScreenViewModelAction) -> Void)?
init(roomProxy: RoomProxyProtocol,
init(accountUserID: String,
roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol) {
self.accountUserID = accountUserID
self.roomProxy = roomProxy
super.init(initialViewState: .init(roomId: roomProxy.id,
canonicalAlias: roomProxy.canonicalAlias,
isEncrypted: roomProxy.isEncrypted,
isDirect: roomProxy.isDirect,
permalink: roomProxy.permalink,
title: roomProxy.roomTitle,
topic: roomProxy.topic,
avatarURL: roomProxy.avatarURL,
permalink: roomProxy.permalink,
joinedMembersCount: roomProxy.joinedMembersCount,
bindings: .init()),
imageProvider: mediaProvider)
setupSubscriptions()
Task {
await roomProxy.updateMembers()
}
setupRoomSubscription()
fetchMembers()
}
// MARK: - Public
@@ -57,12 +58,11 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
override func process(viewAction: RoomDetailsScreenViewAction) {
switch viewAction {
case .processTapPeople:
callback?(.requestMemberDetailsPresentation(members))
callback?(.requestMemberDetailsPresentation)
case .processTapInvite:
callback?(.requestInvitePeoplePresentation(members))
callback?(.requestInvitePeoplePresentation)
case .processTapLeave:
let joinedMembers = members.filter { $0.membership == .join }
guard joinedMembers.count > 1 else {
guard state.joinedMembersCount > 1 else {
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomId: roomProxy.id, state: .empty)
return
}
@@ -88,7 +88,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
// MARK: - Private
private func setupSubscriptions() {
private func setupRoomSubscription() {
switch roomProxy.registerTimelineListenerIfNeeded() {
case .success, .failure(.roomListenerAlreadyRegistered):
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)")
}
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
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
@@ -129,33 +103,50 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
self.state.title = self.roomProxy.roomTitle
self.state.topic = self.roomProxy.topic
self.state.avatarURL = self.roomProxy.avatarURL
self.state.joinedMembersCount = self.roomProxy.joinedMembersCount
}
.store(in: &cancellables)
}
private func buildMembersDetails(members: [RoomMemberProxyProtocol]) async -> RoomMembersDetails {
await Task.detached {
// accessing RoomMember's properties is very slow. We need to do it in a background thread.
var roomMembersDetails: [RoomMemberDetails] = []
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)
private func fetchMembers() {
Task {
await fetchMembersIfNeeded()
await fetchAccountOwner()
}
.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"
@@ -196,9 +187,3 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
}
}
private struct RoomMembersDetails {
let members: [RoomMemberDetails]
let joinedMembersCount: Int
let accountOwner: RoomMemberProxyProtocol?
}