mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
room members list redesign
This commit is contained in:
@@ -629,6 +629,8 @@
|
||||
"screen_room_details_requests_to_join_title" = "Requests to join";
|
||||
"screen_room_grouped_state_events_expand" = "Expand";
|
||||
"screen_room_grouped_state_events_reduce" = "Reduce";
|
||||
"screen_room_member_list_banned_header_title" = "%1$d Banned";
|
||||
"screen_room_member_list_pending_status" = "Pending";
|
||||
"screen_room_roles_and_permissions_space_details" = "Space details";
|
||||
"screen_room_timeline_tombstoned_room_action" = "Jump to new room";
|
||||
"screen_room_timeline_tombstoned_room_message" = "This room has been replaced and is no longer active";
|
||||
@@ -1069,7 +1071,7 @@
|
||||
"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited.";
|
||||
"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_pending_header_title" = "%1$d Invited";
|
||||
"screen_room_member_list_role_owner" = "Owner";
|
||||
"screen_room_member_list_room_members_header_title" = "Room members";
|
||||
"screen_room_member_list_unbanning_user" = "Unbanning %1$@";
|
||||
|
||||
@@ -629,6 +629,8 @@
|
||||
"screen_room_details_requests_to_join_title" = "Requests to join";
|
||||
"screen_room_grouped_state_events_expand" = "Expand";
|
||||
"screen_room_grouped_state_events_reduce" = "Reduce";
|
||||
"screen_room_member_list_banned_header_title" = "%1$d Banned";
|
||||
"screen_room_member_list_pending_status" = "Pending";
|
||||
"screen_room_roles_and_permissions_space_details" = "Space details";
|
||||
"screen_room_timeline_tombstoned_room_action" = "Jump to new room";
|
||||
"screen_room_timeline_tombstoned_room_message" = "This room has been replaced and is no longer active";
|
||||
@@ -1069,7 +1071,7 @@
|
||||
"screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited.";
|
||||
"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_pending_header_title" = "%1$d Invited";
|
||||
"screen_room_member_list_role_owner" = "Owner";
|
||||
"screen_room_member_list_room_members_header_title" = "Room members";
|
||||
"screen_room_member_list_unbanning_user" = "Unbanning %1$@";
|
||||
|
||||
@@ -317,9 +317,9 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%1$d person</string>
|
||||
<string>%1$d Person</string>
|
||||
<key>other</key>
|
||||
<string>%1$d people</string>
|
||||
<string>%1$d People</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>screen_room_multiple_knock_requests_title</key>
|
||||
|
||||
@@ -2478,6 +2478,10 @@ internal enum L10n {
|
||||
}
|
||||
/// There are no banned users.
|
||||
internal static var screenRoomMemberListBannedEmpty: String { return L10n.tr("Localizable", "screen_room_member_list_banned_empty") }
|
||||
/// %1$d Banned
|
||||
internal static func screenRoomMemberListBannedHeaderTitle(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_member_list_banned_header_title", p1)
|
||||
}
|
||||
/// Plural format key: "%#@COUNT@"
|
||||
internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_member_list_header_title", p1)
|
||||
@@ -2496,8 +2500,12 @@ internal enum L10n {
|
||||
internal static var screenRoomMemberListModeBanned: String { return L10n.tr("Localizable", "screen_room_member_list_mode_banned") }
|
||||
/// Members
|
||||
internal static var screenRoomMemberListModeMembers: String { return L10n.tr("Localizable", "screen_room_member_list_mode_members") }
|
||||
/// %1$d Invited
|
||||
internal static func screenRoomMemberListPendingHeaderTitle(_ p1: Int) -> String {
|
||||
return L10n.tr("Localizable", "screen_room_member_list_pending_header_title", p1)
|
||||
}
|
||||
/// Pending
|
||||
internal static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") }
|
||||
internal static var screenRoomMemberListPendingStatus: String { return L10n.tr("Localizable", "screen_room_member_list_pending_status") }
|
||||
/// Admin
|
||||
internal static var screenRoomMemberListRoleAdministrator: String { return L10n.tr("Localizable", "screen_room_member_list_role_administrator") }
|
||||
/// Moderator
|
||||
|
||||
@@ -65,6 +65,7 @@ enum UserAvatarSizeOnScreen {
|
||||
case timeline
|
||||
case settings
|
||||
case roomDetails
|
||||
case roomMembersList
|
||||
case dmDetails
|
||||
case startChat
|
||||
case memberDetails
|
||||
@@ -103,6 +104,8 @@ enum UserAvatarSizeOnScreen {
|
||||
return 52
|
||||
case .roomDetails:
|
||||
return 44
|
||||
case .roomMembersList:
|
||||
return 32
|
||||
case .startChat:
|
||||
return 36
|
||||
case .memberDetails:
|
||||
|
||||
@@ -19,7 +19,7 @@ struct RoomChangeRolesScreenRow: View {
|
||||
|
||||
var body: some View {
|
||||
ListRow(label: .avatar(title: member.name ?? member.id,
|
||||
status: member.isInvited ? L10n.screenRoomMemberListPendingHeaderTitle : nil,
|
||||
status: member.isInvited ? L10n.screenRoomMemberListPendingStatus : nil,
|
||||
description: member.name == nil ? nil : member.id,
|
||||
icon: avatar),
|
||||
kind: .multiSelection(isSelected: isSelected, action: action))
|
||||
|
||||
@@ -22,7 +22,7 @@ struct RoomMembersListScreen: View {
|
||||
.tag(RoomMembersListScreenMode.banned)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(ListRowPadding.insets)
|
||||
}
|
||||
|
||||
if context.mode == .members {
|
||||
@@ -38,15 +38,14 @@ struct RoomMembersListScreen: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.compound.bgCanvasDefault)
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.searchable(text: $context.searchQuery,
|
||||
placement: .navigationBarDrawer(displayMode: .always),
|
||||
prompt: L10n.commonSearchForSomeone)
|
||||
.compoundSearchField()
|
||||
.autocorrectionDisabled()
|
||||
.background(Color.compound.bgCanvasDefault.ignoresSafeArea())
|
||||
.navigationTitle(L10n.commonPeople)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.sheet(item: $context.manageMemeberViewModel) {
|
||||
@@ -59,41 +58,44 @@ struct RoomMembersListScreen: View {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@ViewBuilder
|
||||
var roomMembers: some View {
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
membersSection(entries: context.viewState.visibleInvitedMembers, sectionTitle: L10n.screenRoomMemberListPendingHeaderTitle)
|
||||
membersSection(entries: context.viewState.visibleJoinedMembers, sectionTitle: L10n.screenRoomMemberListHeaderTitle(Int(context.viewState.joinedMembersCount)))
|
||||
}
|
||||
membersSection(entries: context.viewState.visibleInvitedMembers, section: .invited)
|
||||
membersSection(entries: context.viewState.visibleJoinedMembers, section: .joined)
|
||||
}
|
||||
|
||||
var bannedUsers: some View {
|
||||
LazyVStack(alignment: .leading, spacing: 12) {
|
||||
membersSection(entries: context.viewState.visibleBannedMembers)
|
||||
}
|
||||
membersSection(entries: context.viewState.visibleBannedMembers, section: .banned)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func membersSection(entries: [RoomMemberListScreenEntry], sectionTitle: String? = nil) -> some View {
|
||||
private func membersSection(entries: [RoomMemberListScreenEntry], section: MembersSection) -> some View {
|
||||
if !entries.isEmpty {
|
||||
Section {
|
||||
ForEach(entries, id: \.member.id) { entry in
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, context: context)
|
||||
LazyVStack(alignment: .leading, spacing: 0) {
|
||||
ForEach(entries, id: \.member.id) { entry in
|
||||
ListRow(kind: .custom {
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, isLast: entries.last == entry, context: context)
|
||||
})
|
||||
}
|
||||
}
|
||||
.background(.compound.bgCanvasDefault)
|
||||
.clipShape(sectionShape)
|
||||
} 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)
|
||||
}
|
||||
section.header(count: entries.count)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
||||
private var sectionShape: AnyShape {
|
||||
if #available(iOS 26, *) {
|
||||
AnyShape(ConcentricRectangle(corners: .concentric(minimum: 26)))
|
||||
} else {
|
||||
AnyShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
@@ -106,6 +108,40 @@ struct RoomMembersListScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
private enum MembersSection {
|
||||
case joined
|
||||
case invited
|
||||
case banned
|
||||
|
||||
private func sectionTitle(count: Int) -> String {
|
||||
switch self {
|
||||
case .banned:
|
||||
L10n.screenRoomMemberListBannedHeaderTitle(count)
|
||||
case .invited:
|
||||
L10n.screenRoomMemberListHeaderTitle(count)
|
||||
case .joined:
|
||||
L10n.screenRoomMemberListPendingHeaderTitle(count)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func text(count: Int) -> some View {
|
||||
switch self {
|
||||
case .invited, .joined:
|
||||
Text(sectionTitle(count: count))
|
||||
case .banned:
|
||||
Text(sectionTitle(count: count))
|
||||
.foregroundStyle(.compound.bgCriticalPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
func header(count: Int) -> some View {
|
||||
text(count: count)
|
||||
.compoundListSectionHeader()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct RoomMembersListScreen_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
+18
-8
@@ -6,28 +6,30 @@
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct RoomMembersListScreenMemberCell: View {
|
||||
let listEntry: RoomMemberListScreenEntry
|
||||
let isLast: Bool
|
||||
let context: RoomMembersListScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
context.send(viewAction: .selectMember(listEntry.member))
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
HStack(spacing: 16) {
|
||||
LoadableAvatarImage(url: avatarURL,
|
||||
name: avatarName,
|
||||
contentID: listEntry.member.id,
|
||||
avatarSize: .user(on: .roomDetails),
|
||||
avatarSize: .user(on: .roomMembersList),
|
||||
mediaProvider: context.mediaProvider)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
.font(.compound.bodyMDSemibold)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.lineLimit(1)
|
||||
|
||||
@@ -44,11 +46,19 @@ struct RoomMembersListScreenMemberCell: View {
|
||||
|
||||
if let role {
|
||||
Text(role)
|
||||
.font(.compound.bodyXS)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
}
|
||||
}
|
||||
.overlay(alignment: .bottom) {
|
||||
if !isLast {
|
||||
ListRowColor.separatorTint
|
||||
.frame(height: 1)
|
||||
.offset(x: 0, y: ListRowPadding.vertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(ListRowPadding.insets)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
@@ -124,17 +134,17 @@ struct RoomMembersListMemberCell_Previews: PreviewProvider, TestablePreview {
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController,
|
||||
analytics: ServiceLocator.shared.analytics)
|
||||
static var previews: some View {
|
||||
VStack(spacing: 12) {
|
||||
VStack(spacing: 0) {
|
||||
Section("Invited/Joined") {
|
||||
ForEach(members, id: \.member.id) { entry in
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, context: viewModel.context)
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, isLast: members.last == entry, 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: \.member.id) { entry in
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, context: viewModel.context)
|
||||
RoomMembersListScreenMemberCell(listEntry: entry, isLast: bannedMembers.last == entry, context: viewModel.context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user