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
@@ -61,7 +61,7 @@ class InviteUsersScreenViewModelTests: XCTestCase {
func testInviteButton() async {
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 })
context.send(viewAction: .toggleUser(.mockAlice))
@@ -86,7 +86,10 @@ class InviteUsersScreenViewModelTests: XCTestCase {
userDiscoveryService.fetchSuggestionsReturnValue = .success([])
userDiscoveryService.searchProfilesWithReturnValue = .success([])
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])
self.viewModel = viewModel
@@ -21,13 +21,13 @@ import XCTest
@MainActor
class RoomDetailsScreenViewModelTests: XCTestCase {
var viewModel: RoomDetailsScreenViewModelProtocol!
var viewModel: RoomDetailsScreenViewModel!
var roomProxyMock: RoomProxyMock!
var context: RoomDetailsScreenViewModelType.Context { viewModel.context }
override func setUp() {
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test"))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", joinedMembersCount: 0))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
AppSettings.reset()
}
@@ -35,7 +35,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testLeaveRoomTappedWhenPublic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
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()
context.send(viewAction: .processTapLeave)
XCTAssertEqual(context.leaveRoomAlertItem?.state, .public)
@@ -45,7 +45,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testLeaveRoomTappedWhenRoomNotPublic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
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()
context.send(viewAction: .processTapLeave)
XCTAssertEqual(context.leaveRoomAlertItem?.state, .private)
@@ -88,8 +88,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testInitialDMDetailsState() async {
let recipient = RoomMemberProxyMock.mockDan
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
}
@@ -101,8 +101,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .success(())
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -123,8 +123,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .failure(.ignoreUserFailed)
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -146,8 +146,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .success(())
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -168,8 +168,8 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
return .failure(.unignoreUserFailed)
}
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, recipient]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isEncrypted: true, members: mockedMembers, joinedMembersCount: mockedMembers.count))
viewModel = RoomDetailsScreenViewModel(accountUserID: "@owner:somewhere.com", roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
await context.nextViewState()
XCTAssertEqual(context.viewState.dmRecipient, RoomMemberDetails(withProxy: recipient))
@@ -186,28 +186,32 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testCannotInvitePeople() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test",
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)
}
func testInvitePeople() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isPublic: true, members: mockedMembers, 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()
XCTAssertTrue(context.viewState.canInviteUsers)
var callbackCorrectlyCalled = false
viewModel.callback = { action in
switch action {
case .requestInvitePeoplePresentation(let members):
callbackCorrectlyCalled = members.map(\.userID) == mockedMembers.map(\.userID)
case .requestInvitePeoplePresentation:
callbackCorrectlyCalled = true
default:
callbackCorrectlyCalled = false
}
@@ -219,16 +223,16 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
}
func testRoomSubscription() async {
await context.nextViewState()
XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1)
}
func testCanEditAvatar() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomAvatar])
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
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)
XCTAssertFalse(context.viewState.canEditRoomName)
@@ -237,11 +241,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
}
func testCanEditName() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomName]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomName])
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
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)
XCTAssertTrue(context.viewState.canEditRoomName)
@@ -250,11 +255,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
}
func testCanEditTopic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomTopic])
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
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.canEditRoomName)
@@ -263,11 +269,12 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
}
func testCannotEditRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: []), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [])
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
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.canEditRoomName)
@@ -278,9 +285,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
func testCannotEditDirectRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice]
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)
}
@@ -75,6 +75,7 @@ class RoomMembersListScreenViewModelTests: XCTestCase {
}
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())
}
}