ui: channel owners can make subscribers moderators, and moderators see the subscriber list

Core already lets a channel owner make a subscriber a moderator; the role picker offered only subscriber and contributor. It now offers moderator too (not admin, by decision). Moderators see the full member list the owner sees, so they can reach the subscribers they may block (subscribers can't post, so there was no other way to reach them), and member info is headed Moderator or Contributor instead of Subscriber for those roles.

Run: 001-channel-signing-moderators, slice 4
This commit is contained in:
spacedandy-agents-simplex committed 2026-10-23 10:42:55 +00:00
1 parent ff09510b09
commit b0aba71547
9 files changed
+15 -15

No files matched your search

@@ -26,7 +26,7 @@ struct ChannelMembersView: View {
.sorted { $0.wrapped.memberRole > $1.wrapped.memberRole }
let subscriberCount = groupInfo.groupSummary.publicMemberCount ?? Int64(members.count + 1)
let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase
if groupInfo.isOwner {
if groupInfo.canModerate {
List {
Section(header: Text(subscriberCountStr(subscriberCount)).foregroundColor(theme.colors.secondary)) {
searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary)
@@ -125,7 +125,7 @@ struct GroupChatInfoView: View {
Label("Share via chat", systemImage: "arrowshape.turn.up.forward")
}
}
if groupInfo.isOwner || members.contains(where: { $0.wrapped.memberRole >= .owner }) {
if groupInfo.canModerate || members.contains(where: { $0.wrapped.memberRole >= .owner }) {
channelMembersButton()
}
if groupInfo.membership.memberRole >= .moderator {
@@ -734,7 +734,7 @@ struct GroupChatInfoView: View {
}
private func channelMembersButton() -> some View {
let label: LocalizedStringKey = groupInfo.isOwner ? "Subscribers" : "Owners & contributors"
let label: LocalizedStringKey = groupInfo.canModerate ? "Subscribers" : "Owners & contributors"
return NavigationLink {
ChannelMembersView(chat: chat, groupInfo: groupInfo)
.navigationTitle(label)
@@ -38,6 +38,8 @@ struct GroupMemberInfoView: View {
switch groupMember.wrapped.memberRole {
case .relay: "Relay"
case .owner: "Owner"
case .moderator: "Moderator"
case .member: "Contributor"
default: "Subscriber"
}
} else {
+1 -4
View File
@@ -3414,10 +3414,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
if groupInfo.useRelays && !groupInfo.isOwner { return nil }
let userRole = groupInfo.membership.memberRole
if groupInfo.useRelays {
// TODO [relays]: for now owners can only set observer/member in channels.
// Restore the full Owner-excluded picker when moderator/admin promotion is supported:
// return GroupMemberRole.supportedRoles.filter { $0 <= userRole && $0 != .owner }
return [.observer, .member]
return [.observer, .member, .moderator]
} else {
return GroupMemberRole.supportedRoles.filter { $0 <= userRole }
}
@@ -2987,10 +2987,7 @@ data class GroupMember (
else if (groupInfo.useRelays && !groupInfo.isOwner) null
else groupInfo.membership.memberRole.let { userRole ->
if (groupInfo.useRelays)
// TODO [relays]: for now owners can only set observer/member in channels.
// Restore the full Owner-excluded picker when moderator/admin promotion is supported:
// GroupMemberRole.selectableRoles.filter { it <= userRole && it != GroupMemberRole.Owner }
listOf(GroupMemberRole.Observer, GroupMemberRole.Member)
listOf(GroupMemberRole.Observer, GroupMemberRole.Member, GroupMemberRole.Moderator)
else
GroupMemberRole.selectableRoles.filter { it <= userRole }
}
@@ -44,7 +44,7 @@ fun ModalData.ChannelMembersView(
val s = searchText.value.text.trim().lowercase()
val subscriberCount = groupInfo.groupSummary.publicMemberCount ?: (members.size + 1).toLong()
val oneHandUI = remember { ChatController.appPrefs.oneHandUI.state }
val title = if (groupInfo.isOwner) {
val title = if (groupInfo.canModerate) {
generalGetString(MR.strings.channel_members_title_subscribers)
} else {
generalGetString(MR.strings.channel_members_section_owners)
@@ -57,7 +57,7 @@ fun ModalData.ChannelMembersView(
item {
AppBarTitle(title)
}
if (groupInfo.isOwner) {
if (groupInfo.canModerate) {
val showSearch = members.size > 8
item {
SectionView(title = subscriberCountStr(subscriberCount)) {
@@ -651,7 +651,7 @@ fun ModalData.GroupChatInfoLayout(
ModalManager.closeAllModalsEverywhere()
}
}
if (groupInfo.isOwner || activeSortedMembers.any { it.memberRole >= GroupMemberRole.Owner }) {
if (groupInfo.canModerate || activeSortedMembers.any { it.memberRole >= GroupMemberRole.Owner }) {
anyTopSectionRowShow = true
ChannelMembersButton(chat.remoteHostId, groupInfo, showMemberInfo)
}
@@ -1295,7 +1295,7 @@ private fun ShareViaChatButton(onClick: () -> Unit) {
@Composable
private fun ChannelMembersButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) {
val title = if (groupInfo.isOwner) {
val title = if (groupInfo.canModerate) {
stringResource(MR.strings.channel_members_title_subscribers)
} else {
stringResource(MR.strings.channel_members_section_owners)
@@ -594,6 +594,8 @@ fun GroupMemberInfoLayout(
when (member.memberRole) {
GroupMemberRole.Relay -> stringResource(MR.strings.member_info_section_title_relay)
GroupMemberRole.Owner -> stringResource(MR.strings.member_info_section_title_owner)
GroupMemberRole.Moderator -> stringResource(MR.strings.member_info_section_title_moderator)
GroupMemberRole.Member -> stringResource(MR.strings.member_info_section_title_contributor)
else -> stringResource(MR.strings.member_info_section_title_subscriber)
}
} else {
@@ -3090,6 +3090,8 @@
<!-- GroupMemberInfoView.kt channel-related -->
<string name="member_info_section_title_relay">Relay</string>
<string name="member_info_section_title_owner">Owner</string>
<string name="member_info_section_title_moderator">Moderator</string>
<string name="member_info_section_title_contributor">Contributor</string>
<string name="member_info_section_title_subscriber">Subscriber</string>
<string name="info_row_channel">Channel</string>
<string name="info_row_relay_link">Relay link</string>