From 07a6a6d40829107665dba581eed9cc12474ef1df Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 29 Sep 2026 20:41:28 -0400 Subject: [PATCH] fix(concord): ban dialog tells the truth in Private communities; amy refuses to post while banned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In a Private community a ban also Refounds (CORD-05 §5), so the banned member loses access to new messages — but the confirm dialog said they could keep reading until removed. It now says the keys rotate when the ban will Refound. amy `send` also refuses when the fold shows this account banned, instead of publishing a message every reader drops. Co-Authored-By: Claude Opus 5.5 --- .../amethyst/cli/commands/ConcordChannelCommands.kt | 4 ++++ .../src/commonMain/composeResources/values/strings.xml | 1 + .../chats/publicChannels/concord/ConcordMembersScreen.kt | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ConcordChannelCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ConcordChannelCommands.kt index 1e0fedccc2..a99513d2d1 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ConcordChannelCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/ConcordChannelCommands.kt @@ -92,6 +92,10 @@ object ConcordChannelCommands { if (state.dissolved) { return Output.error("dissolved", "community '$handle' has been dissolved and is read-only (CORD-02 §9)") } + // CORD-04 §4: every reader drops a banned author's messages, so a post would vanish unseen. + if (state.authority.isBanned(ctx.signer.pubKey)) { + return Output.error("banned", "this account is banned from '$handle' (CORD-04 §4); its messages are hidden from everyone") + } val channelId = resolve(ctx, sc, channelRef) ?: return Output.error("not_found", "no channel '$channelRef'") // The channel's own plane (CORD-03 §1): root-derived when Public, its held key when // Private — and a refusal, never the root plane, for a Private Channel we hold no key for. diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index db3cfefd5e..8c402b5bff 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -3713,6 +3713,7 @@ Ban Ban member? They can no longer post, and their messages are hidden from everyone. Other apps may remove the community from their list. They can still read new messages until you also remove them from the community. You can unban them later. + They can no longer post, and their messages are hidden from everyone. This community is Private, so banning also rotates its keys: they lose access to new messages. You can unban them later, but they will need a new invite to read again. Make admin Remove admin Could not update this member's roles. diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt index 27b70f9428..d29dfcf49f 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMembersScreen.kt @@ -64,6 +64,7 @@ import com.vitorpamplona.amethyst.commons.resources.back import com.vitorpamplona.amethyst.commons.resources.cancel import com.vitorpamplona.amethyst.commons.resources.concord_members_ban import com.vitorpamplona.amethyst.commons.resources.concord_members_ban_message +import com.vitorpamplona.amethyst.commons.resources.concord_members_ban_message_refound import com.vitorpamplona.amethyst.commons.resources.concord_members_ban_title import com.vitorpamplona.amethyst.commons.resources.concord_members_empty import com.vitorpamplona.amethyst.commons.resources.concord_members_kick @@ -238,6 +239,9 @@ fun ConcordMembersScreen( state?.authority?.canActOn(myPubKey, entry.pubkey, ConcordPermissions.BAN) == true, // A Kick needs KICK and a strict outrank of the target (CORD-04 §6), the same rank rule. canKickTarget = iCanKick && state?.authority?.canActOn(myPubKey, entry.pubkey, ConcordPermissions.KICK) == true, + // In a Private community a ban also Refounds (CORD-05 §5), so the dialog must not + // promise they keep reading. + banRotatesKeys = state?.banRequiresRefounding(listOf(entry.pubkey)) == true, viewerCanManageRoles = iCanManageRoles, // canActOn folds the whole rank rule for us: we hold MANAGE_ROLES, we're not // banned, the target isn't the owner (unremovable), and we strictly outrank @@ -263,6 +267,7 @@ private fun ConcordMemberRow( viewerCanBan: Boolean, canBanTarget: Boolean, canKickTarget: Boolean, + banRotatesKeys: Boolean, viewerCanManageRoles: Boolean, canManageRolesOnTarget: Boolean, assignableRoles: List, @@ -326,7 +331,7 @@ private fun ConcordMemberRow( if (confirmBan) { ConcordConfirmMemberActionDialog( title = Res.string.concord_members_ban_title, - message = Res.string.concord_members_ban_message, + message = if (banRotatesKeys) Res.string.concord_members_ban_message_refound else Res.string.concord_members_ban_message, confirm = Res.string.concord_members_ban, onConfirm = { accountViewModel.setConcordBan(communityId, entry.pubkey, ban = true)