Sort members in the member list by power level and show mods and admins. (#2448)

This commit is contained in:
Doug 2024-02-12 10:39:06 +00:00 committed by GitHub
parent 316e351dbb
commit c1aaf331a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 127 additions and 15 deletions

View File

@ -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 }

View File

@ -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 {

View File

@ -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,

View File

@ -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(),

View File

@ -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(),

View File

@ -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 {

View File

@ -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<Void, RoomMemberProxyError>
@ -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
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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])

Binary file not shown.

1
changelog.d/2355.feature Normal file
View File

@ -0,0 +1 @@
Show admins and moderators in the room member list.