From aa9b147aa8ed1bd3cd34fe04bf5a46b35b7cf8cf Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:28:54 +0000 Subject: [PATCH] ui: show subsriber roles in list; contributor list for subscribers (#7126) --- .../Shared/Views/Chat/ChatInfoToolbar.swift | 9 ++++++++ .../Views/Chat/Group/ChannelMembersView.swift | 19 ++++++++++----- .../Views/Chat/Group/GroupChatInfoView.swift | 2 +- .../simplex/common/views/chat/ChatView.kt | 5 ++++ .../views/chat/group/ChannelMembersView.kt | 23 ++++++++++++++----- .../commonMain/resources/MR/base/strings.xml | 5 +++- 6 files changed, 49 insertions(+), 14 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 00c8d7070b..f825dbeca7 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -131,6 +131,15 @@ public func subscriberCountStr(_ count: Int64) -> String { : String.localizedStringWithFormat(NSLocalizedString("%d subscribers", comment: "channel subscriber count"), count) } +public func ownersContributorsCountStr(_ count: Int, withContributors: Bool) -> String { + if withContributors { + return String.localizedStringWithFormat(NSLocalizedString("%d owners & contributors", comment: "channel members count"), count) + } + return count == 1 + ? String.localizedStringWithFormat(NSLocalizedString("%d owner", comment: "channel owners count"), count) + : String.localizedStringWithFormat(NSLocalizedString("%d owners", comment: "channel owners count"), count) +} + struct ChatInfoToolbar_Previews: PreviewProvider { static var previews: some View { ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift index 44fc302aff..231054fd78 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift @@ -21,22 +21,29 @@ struct ChannelMembersView: View { let s = m.wrapped.memberStatus return s != .memLeft && s != .memRemoved && m.wrapped.memberRole != .relay } + .sorted { $0.wrapped.memberRole > $1.wrapped.memberRole } + let subscriberCount = groupInfo.groupSummary.publicMemberCount ?? Int64(members.count + 1) if groupInfo.isOwner { - let subscriberCount = groupInfo.groupSummary.publicMemberCount ?? Int64(members.count + 1) List { Section(header: Text(subscriberCountStr(subscriberCount)).foregroundColor(theme.colors.secondary)) { memberRow(GMember(groupInfo.membership), user: true, showRole: true) ForEach(members) { member in - memberRow(member, user: false, showRole: member.wrapped.memberRole >= .owner) + memberRow(member, user: false, showRole: member.wrapped.memberRole >= .member) } } } } else { - let owners = members.filter { $0.wrapped.memberRole >= .owner } + let contributors = members.filter { $0.wrapped.memberRole >= .member && $0.wrapped.memberStatus != .memUnknown } + let contributorCount = contributors.count + (groupInfo.membership.memberRole >= .member ? 1 : 0) + let withContributors = contributors.contains { $0.wrapped.memberRole < .owner } + || groupInfo.membership.memberRole >= .member List { - Section(header: Text("Owners").foregroundColor(theme.colors.secondary)) { - ForEach(owners) { member in - memberRow(member, user: false, showRole: false) + Section(header: Text(ownersContributorsCountStr(contributorCount, withContributors: withContributors)).foregroundColor(theme.colors.secondary)) { + if groupInfo.membership.memberRole >= .member { + memberRow(GMember(groupInfo.membership), user: true, showRole: true) + } + ForEach(contributors) { member in + memberRow(member, user: false, showRole: member.wrapped.memberRole >= .moderator) } } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 5563d79e61..41e24a6ced 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -691,7 +691,7 @@ struct GroupChatInfoView: View { } private func channelMembersButton() -> some View { - let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners" + let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners & contributors" return NavigationLink { ChannelMembersView(chat: chat, groupInfo: groupInfo) .navigationTitle(label) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 6f7c746691..cc9e71354c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1547,6 +1547,11 @@ fun subscriberCountStr(count: Long): String = if (count == 1L) String.format(generalGetString(MR.strings.channel_subscriber_count_singular), count) else String.format(generalGetString(MR.strings.channel_subscriber_count_plural), count) +fun ownersContributorsCountStr(count: Int, withContributors: Boolean): String = + if (withContributors) String.format(generalGetString(MR.strings.channel_owners_contributors_count), count) + else if (count == 1) String.format(generalGetString(MR.strings.channel_owner_count_singular), count) + else String.format(generalGetString(MR.strings.channel_owner_count_plural), count) + @Composable fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f)) { Row( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt index 9f13cf2b19..ef3d8805f4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/ChannelMembersView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ownersContributorsCountStr import chat.simplex.common.views.chat.subscriberCountStr import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -33,6 +34,7 @@ fun ChannelMembersView( && m.memberStatus != GroupMemberStatus.MemRemoved && m.memberRole != GroupMemberRole.Relay } + .sortedByDescending { it.memberRole } ColumnWithScrollBar { val title = if (groupInfo.isOwner) { @@ -42,8 +44,8 @@ fun ChannelMembersView( } AppBarTitle(title) + val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong() if (groupInfo.isOwner) { - val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong() SectionView(title = subscriberCountStr(subscriberCount)) { SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { ChannelMemberRow(groupInfo.membership, user = true, showRole = true, isChannel = groupInfo.isChannel) @@ -55,14 +57,23 @@ fun ChannelMembersView( minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING) ) { - ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Owner, isChannel = groupInfo.isChannel) + ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Member, isChannel = groupInfo.isChannel) } } } } else { - val owners = members.filter { it.memberRole >= GroupMemberRole.Owner } - SectionView(title = generalGetString(MR.strings.channel_members_section_owners)) { - owners.forEachIndexed { index, member -> + val contributors = members.filter { it.memberRole >= GroupMemberRole.Member && it.memberStatus != GroupMemberStatus.MemUnknown } + val contributorCount = contributors.size + if (groupInfo.membership.memberRole >= GroupMemberRole.Member) 1 else 0 + val withContributors = contributors.any { it.memberRole < GroupMemberRole.Owner } || + groupInfo.membership.memberRole >= GroupMemberRole.Member + SectionView(title = ownersContributorsCountStr(contributorCount, withContributors)) { + if (groupInfo.membership.memberRole >= GroupMemberRole.Member) { + SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + ChannelMemberRow(groupInfo.membership, user = true, showRole = true, isChannel = groupInfo.isChannel) + } + Divider() + } + contributors.forEachIndexed { index, member -> if (index > 0) { Divider() } @@ -71,7 +82,7 @@ fun ChannelMembersView( minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING) ) { - ChannelMemberRow(member, user = false, showRole = false, isChannel = groupInfo.isChannel) + ChannelMemberRow(member, user = false, showRole = member.memberRole >= GroupMemberRole.Moderator, isChannel = groupInfo.isChannel) } } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 71c0bf98a4..9262bd9dac 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -2976,9 +2976,12 @@ Subscribers - Owners + Owners & contributors %1$d subscriber %1$d subscribers + %1$d owner + %1$d owners + %1$d owners & contributors you