From c1aaf331a365d1f1fdc47aa145b38725c400852b Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 12 Feb 2024 10:39:06 +0000 Subject: [PATCH] Sort members in the member list by power level and show mods and admins. (#2448) --- .../Mocks/Generated/GeneratedMocks.swift | 10 +++++ .../Sources/Mocks/RoomMemberProxyMock.swift | 22 ++++++++++ .../RoomMembersListScreenViewModel.swift | 1 + .../View/RoomMembersListScreen.swift | 4 +- .../RoomMembersListScreenMemberCell.swift | 42 ++++++++++++++----- .../Room/RoomMember/RoomMemberProxy.swift | 4 ++ .../RoomMember/RoomMemberProxyProtocol.swift | 22 ++++++++++ .../RoomMember/RoomMemberDetails.swift | 15 +++++++ .../RoomMembersListViewModelTests.swift | 13 ++++++ .../test_roomMembersListMemberCell.1.png | 4 +- .../test_roomMembersListScreen.1.png | 4 +- changelog.d/2355.feature | 1 + 12 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 changelog.d/2355.feature diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 2a3d6fe0c..22448ef74 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1735,6 +1735,16 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol { set(value) { underlyingIsIgnored = value } } var underlyingIsIgnored: Bool! + var powerLevel: Int { + get { return underlyingPowerLevel } + set(value) { underlyingPowerLevel = value } + } + var underlyingPowerLevel: Int! + var role: RoomMemberRole { + get { return underlyingRole } + set(value) { underlyingRole = value } + } + var underlyingRole: RoomMemberRole! var canInviteUsers: Bool { get { return underlyingCanInviteUsers } set(value) { underlyingCanInviteUsers = value } diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index 536299e0c..5e64c3d55 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -24,6 +24,8 @@ struct RoomMemberProxyMockConfiguration { var membership: MembershipState var isAccountOwner = false var isIgnored = false + var powerLevel = 0 + var role = RoomMemberRole.user var canInviteUsers = false var canSendStateEvent: (StateEventType) -> Bool = { _ in true } } @@ -37,6 +39,8 @@ extension RoomMemberProxyMock { membership = configuration.membership isAccountOwner = configuration.isAccountOwner isIgnored = configuration.isIgnored + powerLevel = configuration.powerLevel + role = configuration.role canInviteUsers = configuration.canInviteUsers canSendStateEventTypeClosure = configuration.canSendStateEvent } @@ -110,6 +114,24 @@ extension RoomMemberProxyMock { canInviteUsers: canInviteUsers, canSendStateEvent: { allowedStateEvents.contains($0) })) } + + static var mockAdmin: RoomMemberProxyMock { + RoomMemberProxyMock(with: .init(userID: "@admin:matrix.org", + displayName: "Arthur", + avatarURL: nil, + membership: .join, + powerLevel: 100, + role: .administrator)) + } + + static var mockModerator: RoomMemberProxyMock { + RoomMemberProxyMock(with: .init(userID: "@mod:matrix.org", + displayName: "Merlin", + avatarURL: nil, + membership: .join, + powerLevel: 50, + role: .moderator)) + } } extension Array where Element == RoomMemberProxyMock { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index f42a3f53d..c6ea4161d 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -83,6 +83,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe private func updateState(members: [RoomMemberProxyProtocol]) { Task { showLoader() + let members = members.sorted() let roomMembersDetails = await buildMembersDetails(members: members) self.members = members self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount, diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift index 85c97803b..2dbf8405d 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift @@ -77,7 +77,9 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview { let members: [RoomMemberProxyMock] = [ .mockAlice, .mockBob, - .mockCharlie + .mockCharlie, + .mockAdmin, + .mockModerator ] return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)), mediaProvider: MockMediaProvider(), diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift index 66e632638..05ee2f432 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift @@ -31,28 +31,50 @@ struct RoomMembersListScreenMemberCell: View { avatarSize: .user(on: .roomDetails), imageProvider: context.imageProvider) .accessibilityHidden(true) - VStack(alignment: .leading, spacing: 0) { - Text(member.name ?? "") - .font(.compound.bodyMDSemibold) - .foregroundColor(.compound.textPrimary) - .lineLimit(1) - Text(member.id) - .font(.compound.bodySM) - .foregroundColor(.compound.textSecondary) - .lineLimit(1) + + HStack(alignment: .firstTextBaseline, spacing: 4) { + VStack(alignment: .leading, spacing: 0) { + Text(member.name ?? "") + .font(.compound.bodyMDSemibold) + .foregroundColor(.compound.textPrimary) + .lineLimit(1) + Text(member.id) + .font(.compound.bodySM) + .foregroundColor(.compound.textSecondary) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + + if let role { + Text(role) + .font(.compound.bodyXS) + .foregroundStyle(.compound.textSecondary) + } } } .frame(maxWidth: .infinity, alignment: .leading) .accessibilityElement(children: .combine) } } + + var role: String? { + switch member.role { + case .administrator: + L10n.screenRoomMemberListRoleAdministrator + case .moderator: + L10n.screenRoomMemberListRoleModerator + case .user: + nil + } + } } struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview { static let members: [RoomMemberProxyMock] = [ .mockAlice, .mockBob, - .mockCharlie + .mockCharlie, + .mockModerator ] static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)), mediaProvider: MockMediaProvider(), diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift index 341026e1f..f52963724 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift @@ -43,6 +43,10 @@ final class RoomMemberProxy: RoomMemberProxyProtocol { lazy var isIgnored = member.isIgnored() + lazy var powerLevel = Int(member.powerLevel()) + + lazy var role = member.suggestedRoleForPowerLevel() + lazy var canInviteUsers = member.canInvite() func canSendStateEvent(type: StateEventType) -> Bool { diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift index e74c87977..64256d8f0 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift @@ -30,6 +30,8 @@ protocol RoomMemberProxyProtocol: AnyObject { var membership: MembershipState { get } var isAccountOwner: Bool { get } var isIgnored: Bool { get } + var powerLevel: Int { get } + var role: RoomMemberRole { get } var canInviteUsers: Bool { get } func ignoreUser() async -> Result @@ -42,4 +44,24 @@ extension RoomMemberProxyProtocol { try? PermalinkBuilder.permalinkTo(userIdentifier: userID, baseURL: ServiceLocator.shared.settings.permalinkBaseURL) } + + /// The name used for sorting the member alphabetically. This will be the displayname if, + /// it exists otherwise it will be the userID with the leading `@` removed. + var sortingName: String { + // If there isn't a displayname we sort by the userID without the @. + displayName ?? String(userID.dropFirst()) + } +} + +extension [RoomMemberProxyProtocol] { + /// The members, sorted first by power-level, and then alphabetically within each power-level. + func sorted() -> Self { + sorted { lhs, rhs in + if lhs.powerLevel != rhs.powerLevel { + lhs.powerLevel > rhs.powerLevel + } else { + lhs.sortingName < rhs.sortingName + } + } + } } diff --git a/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift b/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift index 6f45634f6..6a24d0e24 100644 --- a/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift +++ b/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift @@ -15,6 +15,7 @@ // import Foundation +import MatrixRustSDK struct RoomMemberDetails: Identifiable, Equatable { let id: String @@ -23,6 +24,9 @@ struct RoomMemberDetails: Identifiable, Equatable { let permalink: URL? let isAccountOwner: Bool var isIgnored: Bool + + enum Role { case administrator, moderator, user } + let role: Role init(withProxy proxy: RoomMemberProxyProtocol) { id = proxy.userID @@ -31,5 +35,16 @@ struct RoomMemberDetails: Identifiable, Equatable { permalink = proxy.permalink isAccountOwner = proxy.isAccountOwner isIgnored = proxy.isIgnored + role = .init(proxy.role) + } +} + +extension RoomMemberDetails.Role { + init(_ role: RoomMemberRole) { + self = switch role { + case .administrator: .administrator + case .moderator: .moderator + case .user: .user + } } } diff --git a/UnitTests/Sources/RoomMembersListViewModelTests.swift b/UnitTests/Sources/RoomMembersListViewModelTests.swift index b98d6460a..bfe15c525 100644 --- a/UnitTests/Sources/RoomMembersListViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListViewModelTests.swift @@ -39,6 +39,19 @@ class RoomMembersListScreenViewModelTests: XCTestCase { XCTAssertEqual(viewModel.state.visibleJoinedMembers.count, 2) } + func testSortingMembers() async throws { + setup(with: [.mockModerator, .mockDan, .mockAlice, .mockAdmin]) + + let deferred = deferFulfillment(context.$viewState) { state in + state.visibleJoinedMembers.count == 4 + } + + try await deferred.fulfill() + + let sortedMembers: [RoomMemberProxyMock] = [.mockAdmin, .mockModerator, .mockAlice, .mockDan] + XCTAssertEqual(viewModel.state.visibleJoinedMembers, sortedMembers.map(RoomMemberDetails.init)) + } + func testSearch() async throws { setup(with: [.mockAlice, .mockBob]) diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png index a3865bb26..dbadfde46 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92fa7ac1c4f12e359aafeae825f2c4f09e998da24f1ff27245153137f52d6b85 -size 87593 +oid sha256:56b6f461a25f610215bd0cf49ea17b6c8b26693d743b7c8d45e6f27ae7f05aa3 +size 100612 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png index 524ca762d..65b926256 100644 --- a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61657e5970894e22203a70d84208fc5608e533707b1a4bbe31016b92d92bc328 -size 103429 +oid sha256:2923bfecff677c082946428df17e9e584cefdbf91596fb6f16798638b9338d8e +size 129702 diff --git a/changelog.d/2355.feature b/changelog.d/2355.feature new file mode 100644 index 000000000..455b36ec9 --- /dev/null +++ b/changelog.d/2355.feature @@ -0,0 +1 @@ +Show admins and moderators in the room member list. \ No newline at end of file