feat: geohash composer polish + hide the room's own cell in bubbles

- Center the composer identity avatar vertically instead of bottom-aligning
  it (the "post as me" picture was sinking to the bottom of the edit row).
- Halve the gap between the avatar and the text field (drop the avatar's end
  padding and nudge the field left into its fixed inset).
- Drop the divider line between the feed and the composer — the other chat
  screens don't use one, and the rounded field is separation enough.
- In a location room, suppress the room's own geohash in each message's
  footer/time row (every message repeats the room cell, so showing it is
  noise). Done via a new LocalChatSuppressGeohash composition-local, factored
  into both the footer-has-metadata gate and the footer render; any other
  geohash still shows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172JoMccseEKenyWan6txWV
This commit is contained in:
Claude
2026-07-17 01:21:21 +00:00
parent 4c9f599a79
commit d07bac7489
3 changed files with 32 additions and 10 deletions
@@ -249,8 +249,11 @@ fun NormalChatNote(
}
// The footer shows on the last message of a run (for the time) and on any message
// carrying per-message metadata (expiration, geohash, PoW, legacy-DM marker).
val footerHasMeta = remember(note.event) { chatFooterHasMeta(note) }
// carrying per-message metadata (expiration, geohash, PoW, legacy-DM marker). In a geohash
// room every message repeats the room's own cell, so that geohash is suppressed and doesn't,
// by itself, force a footer row (see LocalChatSuppressGeohash).
val suppressGeohash = LocalChatSuppressGeohash.current
val footerHasMeta = remember(note.event, suppressGeohash) { chatFooterHasMeta(note, suppressGeohash) }
// Only mount the reaction/zap chip row when the message actually has engagement.
// The chips overlap the bubble's bottom edge, so the bubble reserves space beneath
@@ -458,6 +461,13 @@ val LocalChatDisplayNameResolver = compositionLocalOf<((Note) -> String?)?> { nu
*/
val LocalChatShowSelfAuthorName = compositionLocalOf { false }
/**
* The geohash of the location room currently open, or null outside one. Every message in that room
* repeats the room's own cell in its `g` tag, so the bubble footer suppresses this one geohash —
* showing it on every message is redundant noise. Any other geohash still renders.
*/
val LocalChatSuppressGeohash = compositionLocalOf<String?> { null }
@Composable
fun RenderReplyRow(
note: Note,
@@ -74,11 +74,17 @@ fun ChatTimeAgo(baseNote: Note) {
* row) while still surfacing these per-message details that used to live in the
* tap-to-expand "complete UI" detail row.
*/
fun chatFooterHasMeta(note: Note): Boolean {
fun chatFooterHasMeta(
note: Note,
// The location room's own cell, when open. A message whose only metadata is this geohash carries
// no footer-worthy detail (the room header already says where we are), so it doesn't force a row.
suppressGeohash: String? = null,
): Boolean {
val event = note.event ?: return false
val geo = event.geoHashOrScope()
return event is PrivateDmEvent ||
event.expiration() != null ||
event.geoHashOrScope() != null ||
(geo != null && geo != suppressGeohash) ||
event.strongPoWOrNull() != null ||
note.isPinnedInRelayGroup()
}
@@ -102,7 +108,10 @@ fun ChatMessageFooter(
nav: INav,
) {
val event = note.event
val geo = remember(event) { event?.geoHashOrScope() }
val geoRaw = remember(event) { event?.geoHashOrScope() }
// Hide the room's own cell (repeated on every message here); keep any other geohash.
val suppressGeohash = LocalChatSuppressGeohash.current
val geo = if (geoRaw != null && geoRaw != suppressGeohash) geoRaw else null
val pow = remember(event) { event?.strongPoWOrNull() }
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
@@ -39,7 +40,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
@@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatActingI
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatDisplayNameResolver
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatReactOverride
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatShowSelfAuthorName
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.LocalChatSuppressGeohash
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
@@ -203,6 +204,7 @@ private fun GeohashChatRoom(
Box(Modifier.weight(1f).fillMaxWidth()) {
CompositionLocalProvider(
LocalChatShowSelfAuthorName provides true,
LocalChatSuppressGeohash provides geohash,
LocalChatActingIdentities provides myPubKeys,
LocalChatReactOverride provides { note, reaction ->
identity.react(note, reaction, newMessageModel.geohashPostAsSelf)
@@ -233,20 +235,21 @@ private fun GeohashChatRoom(
}
}
HorizontalDivider()
if (newMessageModel.geohashTeleported) {
TeleportIndicatorRow()
}
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Bottom,
verticalAlignment = Alignment.CenterVertically,
) {
GeohashIdentityAvatar(
model = newMessageModel,
accountViewModel = accountViewModel,
onRequestPostAsSelf = { showPostAsSelfWarning = true },
)
Box(Modifier.weight(1f)) {
// Nudge the field left into the composer's fixed 10dp inset so the picture sits close
// to the field rather than across a wide gap.
Box(Modifier.weight(1f).offset(x = (-4).dp)) {
EditFieldRow(
channelScreenModel = newMessageModel,
accountViewModel = accountViewModel,
@@ -288,7 +291,7 @@ private fun GeohashIdentityAvatar(
)
Column(
modifier = Modifier.padding(start = 8.dp, end = 4.dp, bottom = 4.dp),
modifier = Modifier.padding(start = 8.dp, end = 0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (postAsSelf) {