mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(concord): let the owner dissolve a community
`dissolveConcordCommunity` existed with no UI caller, so an owner could only dissolve a community from amy. Adds an owner-only "Dissolve community" item beside Leave, with an irreversible-action confirmation. Verified: amy and a live member see the community read-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e6ee629c04
commit
c994027863
@@ -615,6 +615,11 @@
|
||||
<string name="concord_timer_notice_off">%1$s turned off disappearing messages</string>
|
||||
<string name="concord_timer_active">Messages disappear after %1$s</string>
|
||||
<string name="concord_dissolved_read_only">This community has been dissolved and is now read-only. You can still read its history, but no new messages can be posted.</string>
|
||||
<string name="concord_dissolve_community">Dissolve community</string>
|
||||
<string name="concord_dissolve_title">Dissolve %1$s?</string>
|
||||
<string name="concord_dissolve_message">This ends the community for everyone. It becomes read-only: members keep its history, but nobody can post, change it, or invite anyone again. This can't be undone.</string>
|
||||
<string name="concord_dissolve_confirm">Dissolve</string>
|
||||
<string name="concord_dissolve_failed">The community was not dissolved: only its owner can, from an account that can sign.</string>
|
||||
<string name="concord_private_channel_no_key">This is a private channel and you don't hold its key, so you can't read it or post here.</string>
|
||||
<string name="concord_private_channel_cut">You no longer have access to this private channel: its key was rotated without you, so you can't read new messages or post here.</string>
|
||||
<string name="concord_banned_notice">You're banned from this community, so you can't post here and your messages are hidden from everyone.</string>
|
||||
|
||||
+56
@@ -95,6 +95,10 @@ import com.vitorpamplona.amethyst.commons.resources.concord_channel_rename_save
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_channel_rotate_key
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_channels_empty
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_direct_invite_action
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_dissolve_community
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_dissolve_confirm
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_dissolve_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_dissolve_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_edit_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_invite_action
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_invite_links_action
|
||||
@@ -193,6 +197,7 @@ fun ConcordChannelListScreen(
|
||||
// Read once here (it is @Composable) so the post-leave navigation can use it from a callback.
|
||||
val canPop = nav.canPop()
|
||||
var showLeave by remember { mutableStateOf(false) }
|
||||
var showDissolve by remember { mutableStateOf(false) }
|
||||
var showDirectInvite by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDirectInvite) {
|
||||
@@ -217,6 +222,18 @@ fun ConcordChannelListScreen(
|
||||
)
|
||||
}
|
||||
|
||||
if (showDissolve) {
|
||||
ConcordDissolveDialog(
|
||||
communityName = communityName,
|
||||
onDismiss = { showDissolve = false },
|
||||
onConfirm = {
|
||||
showDissolve = false
|
||||
// Stay here: the dissolved community stays readable, and this screen shows it read-only.
|
||||
accountViewModel.dissolveConcordCommunity(communityId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Channel create/rename/delete are gated on MANAGE_CHANNELS (or owner) — the same predicate the
|
||||
// fold enforces, so an unauthorized action would be a silent no-op we shouldn't even offer.
|
||||
// Rank alone isn't enough on a split epoch: publishing any Control edition also takes the
|
||||
@@ -416,6 +433,21 @@ fun ConcordChannelListScreen(
|
||||
showLeave = true
|
||||
},
|
||||
)
|
||||
// Owner only, like the verifiers: anyone else's tombstone is ignored (CORD-02 §9).
|
||||
if (isOwner && state?.dissolved != true) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
stringRes(Res.string.concord_dissolve_community),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
menuOpen = false
|
||||
showDissolve = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -728,6 +760,30 @@ fun ConcordLeaveDialog(
|
||||
)
|
||||
}
|
||||
|
||||
/** Confirms dissolving a community (CORD-02 §9): irreversible, so the confirm button is the error color. */
|
||||
@Composable
|
||||
fun ConcordDissolveDialog(
|
||||
communityName: String,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(Res.string.concord_dissolve_title, communityName)) },
|
||||
text = { Text(stringRes(Res.string.concord_dissolve_message)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringRes(Res.string.concord_dissolve_confirm), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringRes(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** A pending channel create ([channelIdHex] null) or rename target. */
|
||||
private data class ConcordChannelEditor(
|
||||
val channelIdHex: String?,
|
||||
|
||||
+10
@@ -78,6 +78,8 @@ import com.vitorpamplona.amethyst.commons.resources.cashu_successful_redemption_
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_channel_rotate_key
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_channel_rotate_key_done
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_channel_rotate_key_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_dissolve_community
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_dissolve_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_kicked_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_kicked_message_unnamed
|
||||
import com.vitorpamplona.amethyst.commons.resources.concord_kicked_title
|
||||
@@ -652,6 +654,14 @@ class AccountViewModel(
|
||||
toastManager.toast(Res.string.concord_pin_failed_title, message)
|
||||
}
|
||||
|
||||
/** Dissolve [communityId] for good (CORD-02 §9, owner only); a refusal surfaces as a toast. */
|
||||
fun dissolveConcordCommunity(communityId: String) =
|
||||
launchSigner {
|
||||
if (!account.concord.dissolveConcordCommunity(communityId)) {
|
||||
toastManager.toast(Res.string.concord_dissolve_community, Res.string.concord_dissolve_failed)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate Private Channel [channelIdHex]'s key (CORD-06). The rotation publishes a chunk per
|
||||
* member and can take a while, so both outcomes are toasted: a silent menu item left the
|
||||
|
||||
Reference in New Issue
Block a user