From b0aba715477261dee0c38cedf4f4a373b488bd48 Mon Sep 17 00:00:00 2001 From: spacedandy-agents-simplex <342056592+spacedandy-agents-simplex@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:42:55 +0000 Subject: [PATCH] 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 --- apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift | 2 +- apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift | 4 ++-- apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift | 2 ++ apps/ios/SimpleXChat/ChatTypes.swift | 5 +---- .../commonMain/kotlin/chat/simplex/common/model/ChatModel.kt | 5 +---- .../simplex/common/views/chat/group/ChannelMembersView.kt | 4 ++-- .../simplex/common/views/chat/group/GroupChatInfoView.kt | 4 ++-- .../simplex/common/views/chat/group/GroupMemberInfoView.kt | 2 ++ .../common/src/commonMain/moko-resources/base/strings.xml | 2 ++ 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift index 6b7f5b65fc..e4d788b47c 100644 --- a/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/ChannelMembersView.swift @@ -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) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 1be8070259..d2644e600f 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -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) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index ebbdc6b6a3..d529bc4e76 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -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 { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 68300193d5..60d8ca1da8 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -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 } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 094615eee1..8a0f18d5de 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -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 } } 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 ea841acc86..3ad2906759 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 @@ -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)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 973b684d38..8cbbeb18c7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index ac1c6915b4..dc06d15ea6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -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 { diff --git a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml index 793d72244b..b4d12e357f 100644 --- a/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml @@ -3090,6 +3090,8 @@ Relay Owner + Moderator + Contributor Subscriber Channel Relay link