mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Sort members in the member list by power level and show mods and admins. (#2448)
This commit is contained in:
@@ -1735,6 +1735,16 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
|
|||||||
set(value) { underlyingIsIgnored = value }
|
set(value) { underlyingIsIgnored = value }
|
||||||
}
|
}
|
||||||
var underlyingIsIgnored: Bool!
|
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 {
|
var canInviteUsers: Bool {
|
||||||
get { return underlyingCanInviteUsers }
|
get { return underlyingCanInviteUsers }
|
||||||
set(value) { underlyingCanInviteUsers = value }
|
set(value) { underlyingCanInviteUsers = value }
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ struct RoomMemberProxyMockConfiguration {
|
|||||||
var membership: MembershipState
|
var membership: MembershipState
|
||||||
var isAccountOwner = false
|
var isAccountOwner = false
|
||||||
var isIgnored = false
|
var isIgnored = false
|
||||||
|
var powerLevel = 0
|
||||||
|
var role = RoomMemberRole.user
|
||||||
var canInviteUsers = false
|
var canInviteUsers = false
|
||||||
var canSendStateEvent: (StateEventType) -> Bool = { _ in true }
|
var canSendStateEvent: (StateEventType) -> Bool = { _ in true }
|
||||||
}
|
}
|
||||||
@@ -37,6 +39,8 @@ extension RoomMemberProxyMock {
|
|||||||
membership = configuration.membership
|
membership = configuration.membership
|
||||||
isAccountOwner = configuration.isAccountOwner
|
isAccountOwner = configuration.isAccountOwner
|
||||||
isIgnored = configuration.isIgnored
|
isIgnored = configuration.isIgnored
|
||||||
|
powerLevel = configuration.powerLevel
|
||||||
|
role = configuration.role
|
||||||
canInviteUsers = configuration.canInviteUsers
|
canInviteUsers = configuration.canInviteUsers
|
||||||
canSendStateEventTypeClosure = configuration.canSendStateEvent
|
canSendStateEventTypeClosure = configuration.canSendStateEvent
|
||||||
}
|
}
|
||||||
@@ -110,6 +114,24 @@ extension RoomMemberProxyMock {
|
|||||||
canInviteUsers: canInviteUsers,
|
canInviteUsers: canInviteUsers,
|
||||||
canSendStateEvent: { allowedStateEvents.contains($0) }))
|
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 {
|
extension Array where Element == RoomMemberProxyMock {
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
|
|||||||
private func updateState(members: [RoomMemberProxyProtocol]) {
|
private func updateState(members: [RoomMemberProxyProtocol]) {
|
||||||
Task {
|
Task {
|
||||||
showLoader()
|
showLoader()
|
||||||
|
let members = members.sorted()
|
||||||
let roomMembersDetails = await buildMembersDetails(members: members)
|
let roomMembersDetails = await buildMembersDetails(members: members)
|
||||||
self.members = members
|
self.members = members
|
||||||
self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount,
|
self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount,
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
|||||||
let members: [RoomMemberProxyMock] = [
|
let members: [RoomMemberProxyMock] = [
|
||||||
.mockAlice,
|
.mockAlice,
|
||||||
.mockBob,
|
.mockBob,
|
||||||
.mockCharlie
|
.mockCharlie,
|
||||||
|
.mockAdmin,
|
||||||
|
.mockModerator
|
||||||
]
|
]
|
||||||
return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
|
return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
|
|||||||
+32
-10
@@ -31,28 +31,50 @@ struct RoomMembersListScreenMemberCell: View {
|
|||||||
avatarSize: .user(on: .roomDetails),
|
avatarSize: .user(on: .roomDetails),
|
||||||
imageProvider: context.imageProvider)
|
imageProvider: context.imageProvider)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
Text(member.name ?? "")
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||||
.font(.compound.bodyMDSemibold)
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
.foregroundColor(.compound.textPrimary)
|
Text(member.name ?? "")
|
||||||
.lineLimit(1)
|
.font(.compound.bodyMDSemibold)
|
||||||
Text(member.id)
|
.foregroundColor(.compound.textPrimary)
|
||||||
.font(.compound.bodySM)
|
.lineLimit(1)
|
||||||
.foregroundColor(.compound.textSecondary)
|
Text(member.id)
|
||||||
.lineLimit(1)
|
.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)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.accessibilityElement(children: .combine)
|
.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 {
|
struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
|
||||||
static let members: [RoomMemberProxyMock] = [
|
static let members: [RoomMemberProxyMock] = [
|
||||||
.mockAlice,
|
.mockAlice,
|
||||||
.mockBob,
|
.mockBob,
|
||||||
.mockCharlie
|
.mockCharlie,
|
||||||
|
.mockModerator
|
||||||
]
|
]
|
||||||
static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
|
static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)),
|
||||||
mediaProvider: MockMediaProvider(),
|
mediaProvider: MockMediaProvider(),
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
|
|||||||
|
|
||||||
lazy var isIgnored = member.isIgnored()
|
lazy var isIgnored = member.isIgnored()
|
||||||
|
|
||||||
|
lazy var powerLevel = Int(member.powerLevel())
|
||||||
|
|
||||||
|
lazy var role = member.suggestedRoleForPowerLevel()
|
||||||
|
|
||||||
lazy var canInviteUsers = member.canInvite()
|
lazy var canInviteUsers = member.canInvite()
|
||||||
|
|
||||||
func canSendStateEvent(type: StateEventType) -> Bool {
|
func canSendStateEvent(type: StateEventType) -> Bool {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ protocol RoomMemberProxyProtocol: AnyObject {
|
|||||||
var membership: MembershipState { get }
|
var membership: MembershipState { get }
|
||||||
var isAccountOwner: Bool { get }
|
var isAccountOwner: Bool { get }
|
||||||
var isIgnored: Bool { get }
|
var isIgnored: Bool { get }
|
||||||
|
var powerLevel: Int { get }
|
||||||
|
var role: RoomMemberRole { get }
|
||||||
var canInviteUsers: Bool { get }
|
var canInviteUsers: Bool { get }
|
||||||
|
|
||||||
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
func ignoreUser() async -> Result<Void, RoomMemberProxyError>
|
||||||
@@ -42,4 +44,24 @@ extension RoomMemberProxyProtocol {
|
|||||||
try? PermalinkBuilder.permalinkTo(userIdentifier: userID,
|
try? PermalinkBuilder.permalinkTo(userIdentifier: userID,
|
||||||
baseURL: ServiceLocator.shared.settings.permalinkBaseURL)
|
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 Foundation
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct RoomMemberDetails: Identifiable, Equatable {
|
struct RoomMemberDetails: Identifiable, Equatable {
|
||||||
let id: String
|
let id: String
|
||||||
@@ -24,6 +25,9 @@ struct RoomMemberDetails: Identifiable, Equatable {
|
|||||||
let isAccountOwner: Bool
|
let isAccountOwner: Bool
|
||||||
var isIgnored: Bool
|
var isIgnored: Bool
|
||||||
|
|
||||||
|
enum Role { case administrator, moderator, user }
|
||||||
|
let role: Role
|
||||||
|
|
||||||
init(withProxy proxy: RoomMemberProxyProtocol) {
|
init(withProxy proxy: RoomMemberProxyProtocol) {
|
||||||
id = proxy.userID
|
id = proxy.userID
|
||||||
name = proxy.displayName ?? proxy.userID
|
name = proxy.displayName ?? proxy.userID
|
||||||
@@ -31,5 +35,16 @@ struct RoomMemberDetails: Identifiable, Equatable {
|
|||||||
permalink = proxy.permalink
|
permalink = proxy.permalink
|
||||||
isAccountOwner = proxy.isAccountOwner
|
isAccountOwner = proxy.isAccountOwner
|
||||||
isIgnored = proxy.isIgnored
|
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)
|
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 {
|
func testSearch() async throws {
|
||||||
setup(with: [.mockAlice, .mockBob])
|
setup(with: [.mockAlice, .mockBob])
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:92fa7ac1c4f12e359aafeae825f2c4f09e998da24f1ff27245153137f52d6b85
|
oid sha256:56b6f461a25f610215bd0cf49ea17b6c8b26693d743b7c8d45e6f27ae7f05aa3
|
||||||
size 87593
|
size 100612
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:61657e5970894e22203a70d84208fc5608e533707b1a4bbe31016b92d92bc328
|
oid sha256:2923bfecff677c082946428df17e9e584cefdbf91596fb6f16798638b9338d8e
|
||||||
size 103429
|
size 129702
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Show admins and moderators in the room member list.
|
||||||
Reference in New Issue
Block a user