From 88149f66f3d45032ab70cd39e50ab80b28bb4a56 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 29 Sep 2026 20:32:10 -0400 Subject: [PATCH] fix(concord): show the 64-byte name cap instead of silently dropping the edit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Community, channel and access-role names are capped at 64 UTF-8 bytes (CORD-02 §6). The actions refused a longer name, but the forms took any length, so a long rename or channel create simply did nothing. The name fields now show a "used / 64 bytes" counter that turns red past the cap, and Create / Save / Rename stay disabled until the name fits. Co-Authored-By: Claude Opus 5.5 --- .../composeResources/values/strings.xml | 1 + .../concord/ConcordChannelListScreen.kt | 9 +++++++-- .../concord/ConcordCreateScreen.kt | 2 +- .../concord/ConcordEditScreen.kt | 2 +- .../concord/ConcordMetadataForm.kt | 19 +++++++++++++++++++ 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index 83998fff6c..db3cfefd5e 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -591,6 +591,7 @@ Anyone still holding this link will no longer be able to join. People who already joined with it stay in the community. This can't be undone. Revoke Revoking… + %1$d / %2$d bytes This is the community's last live invite link. Revoking it makes the community Private and rotates its keys, so only current members keep access. Link revoked. The community is now Private and its keys were rotated. Link revoked. The community is now Private, but its keys were not rotated: an admin who can ban members needs to rotate them. diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt index 7b09389ac0..64d6723d32 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordChannelListScreen.kt @@ -851,6 +851,8 @@ private fun ConcordChannelEditDialog( onValueChange = { name = it }, singleLine = true, label = { Text(stringRes(Res.string.concord_channel_name_label)) }, + isError = !concordNameFits(name), + supportingText = { ConcordNameBudget(name) }, modifier = Modifier.fillMaxWidth(), ) // A new channel may be Private (CORD-03): its own key, and an access Role — the @@ -872,6 +874,8 @@ private fun ConcordChannelEditDialog( singleLine = true, placeholder = { Text(name.trim()) }, label = { Text(stringRes(Res.string.concord_channel_access_role_label)) }, + isError = !concordNameFits(roleName), + supportingText = { if (roleName.isNotBlank()) ConcordNameBudget(roleName) }, modifier = Modifier.fillMaxWidth(), ) } @@ -880,8 +884,9 @@ private fun ConcordChannelEditDialog( }, confirmButton = { TextButton( - enabled = name.isNotBlank(), - onClick = { if (name.isNotBlank()) onConfirm(name.trim(), makePrivate, roleName.trim().ifBlank { null }) }, + // Past the 64-byte cap every reader drops the edition, so the action would silently no-op. + enabled = name.isNotBlank() && concordNameFits(name) && concordNameFits(roleName), + onClick = { if (name.isNotBlank() && concordNameFits(name) && concordNameFits(roleName)) onConfirm(name.trim(), makePrivate, roleName.trim().ifBlank { null }) }, ) { Text( stringRes( diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt index 3355c41e4d..73849afed8 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordCreateScreen.kt @@ -147,7 +147,7 @@ fun ConcordCreateScreen( } } }, - enabled = name.value.isNotBlank() && !working, + enabled = name.value.isNotBlank() && concordNameFits(name.value) && !working, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), ) { Text(stringRes(Res.string.concord_create_action)) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt index ca2e82a4fe..f24df96875 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordEditScreen.kt @@ -202,7 +202,7 @@ fun ConcordEditScreen( if (ok) nav.popBack() } }, - enabled = name.value.isNotBlank() && !working, + enabled = name.value.isNotBlank() && concordNameFits(name.value) && !working, modifier = Modifier.fillMaxWidth().padding(top = 8.dp), ) { Text(stringRes(Res.string.concord_edit_save)) diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt index 5a69e72520..ccb4361bf3 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/chats/publicChannels/concord/ConcordMetadataForm.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.commons.resources.concord_create_icon_hint import com.vitorpamplona.amethyst.commons.resources.concord_create_name import com.vitorpamplona.amethyst.commons.resources.concord_create_title import com.vitorpamplona.amethyst.commons.resources.concord_edit_banner_hint +import com.vitorpamplona.amethyst.commons.resources.concord_name_budget import com.vitorpamplona.amethyst.commons.resources.failed_to_upload_media_no_details import com.vitorpamplona.amethyst.commons.resources.remove import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle @@ -85,6 +86,7 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.MediumRelayIconModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.quartz.concord.cord02Community.ImagePointer +import com.vitorpamplona.quartz.concord.cord04Roles.ConcordLimits import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.utils.Log @@ -130,6 +132,8 @@ fun ConcordMetadataFields( modifier = Modifier.fillMaxWidth(), singleLine = true, label = { Text(stringRes(Res.string.concord_create_name)) }, + isError = !concordNameFits(name.value), + supportingText = { ConcordNameBudget(name.value) }, ) OutlinedTextField( value = about.value, @@ -142,6 +146,21 @@ fun ConcordMetadataFields( } } +/** + * Whether [name] fits the protocol-wide name cap (CORD-02 §6: 64 UTF-8 bytes, for the Community, + * Channels and Roles). Every reader drops an edition past it, so a form must not submit one. + */ +fun concordNameFits(name: String): Boolean = ConcordLimits.nameFits(name.trim()) + +/** A text field's `used / 64` byte counter for a Concord name, in the error color once over the cap. */ +@Composable +fun ConcordNameBudget(name: String) { + Text( + stringRes(Res.string.concord_name_budget, ConcordLimits.utf8Size(name.trim()), ConcordLimits.NAME_MAX_BYTES), + color = if (concordNameFits(name)) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.error, + ) +} + /** * The circular community-icon hero: shows the current (decrypted) icon over a stable robohash * placeholder, and on tap opens the photo picker → encrypts + uploads the chosen image and updates