mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
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:
@@ -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?
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user