diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index b20534817..002fe0759 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -513,6 +513,8 @@ "screen_room_member_details_unblock_alert_action" = "Unblock"; "screen_room_member_details_unblock_alert_description" = "You'll be able to see all messages from them again."; "screen_room_member_details_unblock_user" = "Unblock user"; +"screen_room_member_list_mode_banned" = "Banned"; +"screen_room_member_list_mode_members" = "Members"; "screen_room_member_list_pending_header_title" = "Pending"; "screen_room_member_list_role_administrator" = "Admin"; "screen_room_member_list_role_moderator" = "Moderator"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index e7d2babc3..52bb9fd3b 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -1254,6 +1254,10 @@ public enum L10n { public static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String { return L10n.tr("Localizable", "screen_room_member_list_header_title", p1) } + /// Banned + public static var screenRoomMemberListModeBanned: String { return L10n.tr("Localizable", "screen_room_member_list_mode_banned") } + /// Members + public static var screenRoomMemberListModeMembers: String { return L10n.tr("Localizable", "screen_room_member_list_mode_members") } /// Pending public static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") } /// Admin diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 77c8711da..e70ff9ced 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1750,6 +1750,11 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol { set(value) { underlyingCanInviteUsers = value } } var underlyingCanInviteUsers: Bool! + var canBanUsers: Bool { + get { return underlyingCanBanUsers } + set(value) { underlyingCanBanUsers = value } + } + var underlyingCanBanUsers: Bool! //MARK: - ignoreUser diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index 5e64c3d55..736af3232 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -19,7 +19,7 @@ import MatrixRustSDK struct RoomMemberProxyMockConfiguration { var userID: String - var displayName: String + var displayName: String? var avatarURL: URL? var membership: MembershipState var isAccountOwner = false @@ -27,6 +27,7 @@ struct RoomMemberProxyMockConfiguration { var powerLevel = 0 var role = RoomMemberRole.user var canInviteUsers = false + var canBanUsers = false var canSendStateEvent: (StateEventType) -> Bool = { _ in true } } @@ -42,6 +43,7 @@ extension RoomMemberProxyMock { powerLevel = configuration.powerLevel role = configuration.role canInviteUsers = configuration.canInviteUsers + canBanUsers = configuration.canBanUsers canSendStateEventTypeClosure = configuration.canSendStateEvent } @@ -58,28 +60,24 @@ extension RoomMemberProxyMock { static var mockAlice: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@alice:matrix.org", displayName: "Alice", - avatarURL: nil, membership: .join)) } static var mockInvitedAlice: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@alice:matrix.org", displayName: "Alice", - avatarURL: nil, membership: .invite)) } static var mockBob: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@bob:matrix.org", displayName: "Bob", - avatarURL: nil, membership: .join)) } static var mockCharlie: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@charlie:matrix.org", displayName: "Charlie", - avatarURL: nil, membership: .join)) } @@ -93,7 +91,6 @@ extension RoomMemberProxyMock { static var mockInvited: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@invited:matrix.org", displayName: "Invited", - avatarURL: nil, membership: .invite, isIgnored: true)) } @@ -101,7 +98,6 @@ extension RoomMemberProxyMock { static var mockIgnored: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@ignored:matrix.org", displayName: "Ignored", - avatarURL: nil, membership: .join, isIgnored: true)) } @@ -118,7 +114,6 @@ extension RoomMemberProxyMock { static var mockAdmin: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@admin:matrix.org", displayName: "Arthur", - avatarURL: nil, membership: .join, powerLevel: 100, role: .administrator)) @@ -127,11 +122,24 @@ extension RoomMemberProxyMock { static var mockModerator: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@mod:matrix.org", displayName: "Merlin", - avatarURL: nil, membership: .join, powerLevel: 50, role: .moderator)) } + + static var mockBanned: [RoomMemberProxyMock] { + [ + RoomMemberProxyMock(with: .init(userID: "@mischief:matrix.org", + membership: .ban)), + RoomMemberProxyMock(with: .init(userID: "@spam:matrix.org", + membership: .ban)), + RoomMemberProxyMock(with: .init(userID: "@angry:matrix.org", + membership: .ban)), + RoomMemberProxyMock(with: .init(userID: "@fake:matrix.org", + displayName: "The President", + membership: .ban)) + ] + } } extension Array where Element == RoomMemberProxyMock { diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift index c4f46eaf6..ed09bfb69 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift @@ -21,41 +21,59 @@ enum RoomMembersListScreenViewModelAction { case invite } +/// The different modes that the screen can be in. +enum RoomMembersListScreenMode { + /// The screen is showing invited and joined members. + case members + /// The screen is showing banned members (to mods/admins) + case banned +} + struct RoomMembersListScreenViewState: BindableState { private var joinedMembers: [RoomMemberDetails] private var invitedMembers: [RoomMemberDetails] + private var bannedMembers: [RoomMemberDetails] let joinedMembersCount: Int + var bannedMembersCount: Int { bannedMembers.count } + var canInviteUsers = false + var canBanUsers = false + var bindings: RoomMembersListScreenViewStateBindings init(joinedMembersCount: Int, joinedMembers: [RoomMemberDetails] = [], invitedMembers: [RoomMemberDetails] = [], + bannedMembers: [RoomMemberDetails] = [], bindings: RoomMembersListScreenViewStateBindings) { self.joinedMembersCount = joinedMembersCount self.joinedMembers = joinedMembers self.invitedMembers = invitedMembers + self.bannedMembers = bannedMembers self.bindings = bindings } var visibleJoinedMembers: [RoomMemberDetails] { - joinedMembers.lazy - .filter { member in - member.matches(searchQuery: bindings.searchQuery) - } + joinedMembers + .filter { $0.matches(searchQuery: bindings.searchQuery) } } var visibleInvitedMembers: [RoomMemberDetails] { - invitedMembers.lazy - .filter { member in - member.matches(searchQuery: bindings.searchQuery) - } + invitedMembers + .filter { $0.matches(searchQuery: bindings.searchQuery) } + } + + var visibleBannedMembers: [RoomMemberDetails] { + bannedMembers + .filter { $0.matches(searchQuery: bindings.searchQuery) } } } struct RoomMembersListScreenViewStateBindings { var searchQuery = "" + /// The current mode the screen is in. + var mode: RoomMembersListScreenMode = .members /// Information describing the currently displayed alert. var alertInfo: AlertInfo? diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index c6ea4161d..907c122fc 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -31,13 +31,15 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe actionsSubject.eraseToAnyPublisher() } - init(roomProxy: RoomProxyProtocol, + init(initialMode: RoomMembersListScreenMode = .members, + roomProxy: RoomProxyProtocol, mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController - super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount, bindings: .init()), + super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount, + bindings: .init(mode: initialMode)), imageProvider: mediaProvider) setupMembers() @@ -89,8 +91,12 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount, joinedMembers: roomMembersDetails.joinedMembers, invitedMembers: roomMembersDetails.invitedMembers, + bannedMembers: roomMembersDetails.bannedMembers, bindings: state.bindings) - self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false + if let accountOwner = roomMembersDetails.accountOwner { + self.state.canInviteUsers = accountOwner.canInviteUsers + self.state.canBanUsers = accountOwner.canBanUsers + } hideLoader() } } @@ -100,6 +106,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe // accessing RoomMember's properties is very slow. We need to do it in a background thread. var invitedMembers: [RoomMemberDetails] = .init() var joinedMembers: [RoomMemberDetails] = .init() + var bannedMembers: [RoomMemberDetails] = .init() var accountOwner: RoomMemberProxyProtocol? for member in members { @@ -112,12 +119,17 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe invitedMembers.append(.init(withProxy: member)) case .join: joinedMembers.append(.init(withProxy: member)) + case .ban: + bannedMembers.append(.init(withProxy: member)) default: continue } } - return .init(invitedMembers: invitedMembers, joinedMembers: joinedMembers, accountOwner: accountOwner) + return .init(invitedMembers: invitedMembers, + joinedMembers: joinedMembers, + bannedMembers: bannedMembers.sorted { $0.id.localizedStandardCompare($1.id) == .orderedAscending }, // Re-sort ignoring display name. + accountOwner: accountOwner) } .value } @@ -140,5 +152,6 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe private struct RoomMembersDetails { var invitedMembers: [RoomMemberDetails] var joinedMembers: [RoomMemberDetails] + var bannedMembers: [RoomMemberDetails] var accountOwner: RoomMemberProxyProtocol? } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift index 2dbf8405d..704e2e93a 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreen.swift @@ -22,9 +22,23 @@ struct RoomMembersListScreen: View { var body: some View { ScrollView { - LazyVStack(alignment: .leading, spacing: 12) { - membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle) - membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount))) + if context.viewState.canBanUsers, + context.viewState.bannedMembersCount > 0 { + // Maybe this should go into the search bar if it can be pinned when not focussed? + Picker("", selection: $context.mode) { + Text(L10n.screenRoomMemberListModeMembers) + .tag(RoomMembersListScreenMode.members) + Text(L10n.screenRoomMemberListModeBanned) + .tag(RoomMembersListScreenMode.banned) + } + .pickerStyle(.segmented) + .padding(.horizontal, 16) + } + + if context.mode == .members { + roomMembers + } else { + bannedUsers } } .searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always)) @@ -42,20 +56,39 @@ struct RoomMembersListScreen: View { // MARK: - Private - private func membersSection(data: [RoomMemberDetails], sectionTitle: String) -> some View { - Section { - ForEach(data, id: \.id) { member in - RoomMembersListScreenMemberCell(member: member, context: context) - } - } header: { - if !data.isEmpty { - Text(sectionTitle) - .foregroundColor(.compound.textSecondary) - .font(.compound.bodyLG) - .padding(.top, 12) - } + var roomMembers: some View { + LazyVStack(alignment: .leading, spacing: 12) { + membersSection(data: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle) + membersSection(data: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount))) + } + } + + var bannedUsers: some View { + LazyVStack(alignment: .leading, spacing: 12) { + membersSection(data: context.viewState.visibleBannedMembers) + } + } + + @ViewBuilder + private func membersSection(data: [RoomMemberDetails], sectionTitle: String? = nil) -> some View { + if !data.isEmpty { + Section { + ForEach(data, id: \.id) { member in + RoomMembersListScreenMemberCell(member: member, context: context) + } + } header: { + if let sectionTitle { + Text(sectionTitle) + .foregroundColor(.compound.textSecondary) + .font(.compound.bodyLG) + .padding(.top, 12) + } else { + // Put something in here to maintain constant top padding. + Spacer().frame(height: 0) + } + } + .padding(.horizontal, 16) } - .padding(.horizontal) } @ViewBuilder @@ -73,23 +106,62 @@ struct RoomMembersListScreen: View { // MARK: - Previews struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = { - let members: [RoomMemberProxyMock] = [ - .mockAlice, - .mockBob, - .mockCharlie, - .mockAdmin, - .mockModerator - ] - return RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)), - mediaProvider: MockMediaProvider(), - userIndicatorController: ServiceLocator.shared.userIndicatorController) - }() + static let viewModel = makeViewModel() + static let invitesViewModel = makeViewModel(withInvites: true) + static let adminViewModel = makeViewModel(isAdmin: true, initialMode: .members) + static let bannedViewModel = makeViewModel(isAdmin: true, initialMode: .banned) static var previews: some View { NavigationStack { RoomMembersListScreen(context: viewModel.context) } .snapshot(delay: 1.0) + .previewDisplayName("Member") + + NavigationStack { + RoomMembersListScreen(context: invitesViewModel.context) + } + .snapshot(delay: 1.0) + .previewDisplayName("Invites") + + NavigationStack { + RoomMembersListScreen(context: adminViewModel.context) + } + .snapshot(delay: 1.0) + .previewDisplayName("Admin: Members") + + NavigationStack { + RoomMembersListScreen(context: bannedViewModel.context) + } + .snapshot(delay: 1.0) + .previewDisplayName("Admin: Banned") + } + + static func makeViewModel(withInvites: Bool = false, + isAdmin: Bool = false, + initialMode: RoomMembersListScreenMode = .members) -> RoomMembersListScreenViewModel { + let mockAdmin = RoomMemberProxyMock.mockAdmin + + if isAdmin { + mockAdmin.underlyingCanBanUsers = true + mockAdmin.underlyingIsAccountOwner = true + } + + var members: [RoomMemberProxyMock] = [ + .mockAlice, + .mockBob, + .mockCharlie, + mockAdmin, + .mockModerator + ] + RoomMemberProxyMock.mockBanned + + if withInvites { + members.append(.mockInvited) + } + + return RoomMembersListScreenViewModel(initialMode: initialMode, + roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)), + mediaProvider: MockMediaProvider(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) } } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift index 05ee2f432..56a798b86 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift @@ -25,8 +25,8 @@ struct RoomMembersListScreenMemberCell: View { context.send(viewAction: .selectMember(id: member.id)) } label: { HStack(spacing: 8) { - LoadableAvatarImage(url: member.avatarURL, - name: member.name ?? "", + LoadableAvatarImage(url: avatarURL, + name: avatarName, contentID: member.id, avatarSize: .user(on: .roomDetails), imageProvider: context.imageProvider) @@ -34,14 +34,17 @@ struct RoomMembersListScreenMemberCell: View { HStack(alignment: .firstTextBaseline, spacing: 4) { VStack(alignment: .leading, spacing: 0) { - Text(member.name ?? "") + Text(title) .font(.compound.bodyMDSemibold) .foregroundColor(.compound.textPrimary) .lineLimit(1) - Text(member.id) - .font(.compound.bodySM) - .foregroundColor(.compound.textSecondary) - .lineLimit(1) + + if let subtitle { + Text(subtitle) + .font(.compound.bodySM) + .foregroundColor(.compound.textSecondary) + .lineLimit(1) + } } .frame(maxWidth: .infinity, alignment: .leading) @@ -67,22 +70,59 @@ struct RoomMembersListScreenMemberCell: View { nil } } + + // Computed properties to hide the user's profile when banned. + + var title: String { + guard !member.isBanned else { return member.id } + return member.name ?? member.id + } + + var subtitle: String? { + member.isBanned ? nil : member.id + } + + var avatarName: String? { + member.isBanned ? nil : member.name + } + + var avatarURL: URL? { + member.isBanned ? nil : member.avatarURL + } } struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview { static let members: [RoomMemberProxyMock] = [ .mockAlice, - .mockBob, - .mockCharlie, - .mockModerator + .mockAdmin, + .mockModerator, + .init(with: .init(userID: "@nodisplayname:matrix.org", membership: .join)), + .init(with: .init(userID: "@avatar:matrix.org", displayName: "Avatar", avatarURL: .picturesDirectory, membership: .join)) ] - static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", members: members)), + + static let bannedMembers: [RoomMemberProxyMock] = [ + .init(with: .init(userID: "@nodisplayname:matrix.org", membership: .ban)), + .init(with: .init(userID: "@fake:matrix.org", displayName: "President", membership: .ban)), + .init(with: .init(userID: "@badavatar:matrix.org", avatarURL: .picturesDirectory, membership: .ban)) + ] + + static let viewModel = RoomMembersListScreenViewModel(roomProxy: RoomProxyMock(with: .init(displayName: "Some room", + members: members)), mediaProvider: MockMediaProvider(), userIndicatorController: ServiceLocator.shared.userIndicatorController) static var previews: some View { VStack(spacing: 12) { - ForEach(members, id: \.userID) { member in - RoomMembersListScreenMemberCell(member: .init(withProxy: member), context: viewModel.context) + Section("Invited/Joined") { + ForEach(members, id: \.userID) { member in + RoomMembersListScreenMemberCell(member: .init(withProxy: member), context: viewModel.context) + } + } + + // Banned members should have their profiles hidden and the avatar should use the first letter from their user ID. + Section("Banned") { + ForEach(bannedMembers, id: \.userID) { member in + RoomMembersListScreenMemberCell(member: .init(withProxy: member), context: viewModel.context) + } } } } diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift index f52963724..0165bdb0d 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift @@ -44,10 +44,9 @@ 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() + lazy var canBanUsers = member.canBan() func canSendStateEvent(type: StateEventType) -> Bool { member.canSendState(stateEvent: type) diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift index 64256d8f0..c957128f5 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift @@ -30,9 +30,11 @@ 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 } + var canBanUsers: Bool { get } func ignoreUser() async -> Result func unignoreUser() async -> Result @@ -49,7 +51,7 @@ extension RoomMemberProxyProtocol { /// 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()) + (displayName ?? String(userID.dropFirst())).lowercased() } } @@ -60,7 +62,7 @@ extension [RoomMemberProxyProtocol] { if lhs.powerLevel != rhs.powerLevel { lhs.powerLevel > rhs.powerLevel } else { - lhs.sortingName < rhs.sortingName + lhs.sortingName.localizedStandardCompare(rhs.sortingName) == .orderedAscending } } } diff --git a/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift b/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift index 6a24d0e24..e453357ff 100644 --- a/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift +++ b/ElementX/Sources/Services/RoomMember/RoomMemberDetails.swift @@ -24,17 +24,21 @@ struct RoomMemberDetails: Identifiable, Equatable { let permalink: URL? let isAccountOwner: Bool var isIgnored: Bool + var isBanned: Bool enum Role { case administrator, moderator, user } let role: Role +} +extension RoomMemberDetails { init(withProxy proxy: RoomMemberProxyProtocol) { id = proxy.userID - name = proxy.displayName ?? proxy.userID + name = proxy.displayName avatarURL = proxy.avatarURL permalink = proxy.permalink isAccountOwner = proxy.isAccountOwner isIgnored = proxy.isIgnored + isBanned = proxy.membership == .ban role = .init(proxy.role) } } diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListMemberCell.1.png index dbadfde46..0e4897c9e 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:56b6f461a25f610215bd0cf49ea17b6c8b26693d743b7c8d45e6f27ae7f05aa3 -size 100612 +oid sha256:4e3ca244d74c6362c0bbf1e3102e674c0a74ad83077535778f3998b7b4be933d +size 175572 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Admin-Banned.png b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Admin-Banned.png new file mode 100644 index 000000000..ec33cd60d --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Admin-Banned.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cc654312d9d1c771b828b9fff87681d5020d952a43fac53367c93cfb945815c +size 117702 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Admin-Members.png b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Admin-Members.png new file mode 100644 index 000000000..ec5993d4e --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Admin-Members.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a26fac69e9d20254891018a2443dd70a2df71fe0cf5899720f616551c245fc86 +size 139984 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Invites.png b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Invites.png new file mode 100644 index 000000000..a453b42b5 --- /dev/null +++ b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Invites.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4efa28d080b7ee425c212744a44b62873d973627186d8be6aabba6eefc78918e +size 144200 diff --git a/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png b/UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Member.png similarity index 100% rename from UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.1.png rename to UnitTests/__Snapshots__/PreviewTests/test_roomMembersListScreen.Member.png diff --git a/changelog.d/2355.feature b/changelog.d/2355.feature new file mode 100644 index 000000000..f91c13876 --- /dev/null +++ b/changelog.d/2355.feature @@ -0,0 +1 @@ +Add the list of banned users, shown to room members who have the power ban/unban. \ No newline at end of file