mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Sort members in the member list by power level and show mods and admins. (#2448)
This commit is contained in:
parent
316e351dbb
commit
c1aaf331a3
@ -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 }
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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(),
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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])
|
||||
|
||||
|
BIN
UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png
(Stored with Git LFS)
BIN
UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png
(Stored with Git LFS)
BIN
UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png
(Stored with Git LFS)
Binary file not shown.
1
changelog.d/2355.feature
Normal file
1
changelog.d/2355.feature
Normal file
@ -0,0 +1 @@
|
||||
Show admins and moderators in the room member list.
|
Loading…
x
Reference in New Issue
Block a user