fix(cordn): name the attachment in the inbox row, and drop the bech32 above the QR

Two things the Messages list and the group info page were showing that nobody
could use.

**A voice note read as an empty message.** The inbox row is built from
`envelope.content`, and a message whose whole content is an attachment has
none — so the row rendered "Dr. Edo Paz: " and trailed off, which looks like
somebody sent nothing. It now names what was sent: Voice note, Photo, Video or
File, chosen off the first attachment's MIME. The first one decides because a
row is one line, and a message with a photo and a file in it is, to someone
skimming an inbox, a message with a photo in it. A withdrawn message says so
too, rather than falling through to the attachment label.

**The share block printed the bech32 above the QR.** Three lines of characters
that are not readable and not meant to be retyped, pushing the QR itself below
the fold. Both things a person actually does with that string are still on the
page — point a camera at the code, or press Copy link.

Verified on the tablet: the row reads "Dr. Edo Paz: Voice note", and the share
section is explainer, QR, Copy link.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-09-25 10:13:56 -04:00
co-authored by Claude Opus 5
parent e26629f33e
commit 9ecb38df24
3 changed files with 44 additions and 6 deletions
@@ -610,11 +610,10 @@ private fun ShareGroup(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
SelectionContainer {
Text(ref, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(vertical = 8.dp))
}
QrCodeDrawer(ref, Modifier.size(220.dp))
// The bech32 itself is not shown. It is three lines of characters nobody
// reads, and both things a person actually does with it are already here:
// point a camera at the QR, or press Copy link.
QrCodeDrawer(ref, Modifier.padding(top = 8.dp).size(220.dp))
OutlinedButton(
onClick = {
@@ -82,6 +82,11 @@ import com.vitorpamplona.amethyst.commons.resources.concord_server_label
import com.vitorpamplona.amethyst.commons.resources.cordn_group
import com.vitorpamplona.amethyst.commons.resources.cordn_group_no_messages_yet
import com.vitorpamplona.amethyst.commons.resources.cordn_group_untitled
import com.vitorpamplona.amethyst.commons.resources.cordn_preview_deleted
import com.vitorpamplona.amethyst.commons.resources.cordn_preview_file
import com.vitorpamplona.amethyst.commons.resources.cordn_preview_photo
import com.vitorpamplona.amethyst.commons.resources.cordn_preview_video
import com.vitorpamplona.amethyst.commons.resources.cordn_preview_voice_note
import com.vitorpamplona.amethyst.commons.resources.could_not_decrypt_the_message
import com.vitorpamplona.amethyst.commons.resources.ephemeral_relay_chat
import com.vitorpamplona.amethyst.commons.resources.geohash_chat
@@ -146,8 +151,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayG
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.ConcordServerRoomNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.dal.RelayGroupServerRoomNote
import com.vitorpamplona.quartz.buzz.notifications.MemberAddedNotificationEvent
import com.vitorpamplona.quartz.cordn.appEncryptedMedia.CordnMediaTag
import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChatEvent
import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
@@ -545,6 +552,23 @@ private fun MarmotGroupRoomCompose(
)
}
/**
* What to call a message whose whole content is an attachment.
*
* The first attachment decides, which is what the eye does too: a row is one
* line and a message with a photo and a file in it is, to a reader skimming the
* inbox, a message with a photo in it.
*/
private fun attachmentLabelFor(tags: TagArray): StringResource {
val first = CordnMediaTag.parseAll(tags).firstOrNull() ?: return Res.string.cordn_preview_file
return when {
first.isAudio -> Res.string.cordn_preview_voice_note
first.isImage -> Res.string.cordn_preview_photo
first.mimeType.startsWith("video/") -> Res.string.cordn_preview_video
else -> Res.string.cordn_preview_file
}
}
/**
* One cordn room in the Messages list.
*
@@ -573,7 +597,17 @@ private fun CordnGroupRoomCompose(
// inbox while the room shows the edit is the kind of mismatch that
// reads as a sync bug.
val text = annotations.contentOf(message.envelope.id) ?: message.envelope.content
"$authorName: ${text.take(200)}"
// A voice note or a photo carries no text, so the row used to read
// "Someone: " and trail off — an attachment looked like an empty
// message. Name what was sent instead, the way the bubble does.
val preview =
text.takeIf { it.isNotBlank() }
?: if (annotations.isDeleted(message.envelope.id)) {
stringRes(Res.string.cordn_preview_deleted)
} else {
stringRes(attachmentLabelFor(message.envelope.tags))
}
"$authorName: ${preview.take(200)}"
} ?: stringRes(Res.string.cordn_group_no_messages_yet)
ChannelName(
@@ -5381,6 +5381,11 @@
and the new-conversation picker. -->
<string name="cordn_group_untitled">Group %1$s</string>
<string name="cordn_group_no_messages_yet">No messages yet</string>
<string name="cordn_preview_deleted">Message deleted</string>
<string name="cordn_preview_voice_note">Voice note</string>
<string name="cordn_preview_photo">Photo</string>
<string name="cordn_preview_video">Video</string>
<string name="cordn_preview_file">File</string>
<string name="cordn_group">cordn</string>
<string name="new_conversation_cordn_title">cordn group</string>
<string name="new_conversation_cordn_tagline">Encrypted group chat ordered by a coordinator you pick.</string>