fix(chat): timestamp tap opens delivery (not format toggle); relays for Concord

The chat timestamp tap toggled relative/absolute time instead of opening
the relay dialog, because ToggleableTimeAgoText has its own inner click.
Give it a `toggleable` flag and turn it off for the chat time, so the
enclosing tap target opens the delivery dialog; the absolute time now shows
as a header inside that dialog so nothing is lost.

Also:
- Encrypted-channel messages (Concord/Armada) decrypt locally with no
  per-relay attribution, so the dialog showed "no relay information". Fall
  back to the channel's own relays as "where this message lives".
- Move the "Message Delivery" sheet tile out of the always-visible row into
  the Show More section — it's a secondary action.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
This commit is contained in:
Claude
2026-07-15 19:47:38 +00:00
parent bf9fb48205
commit c740a346ae
4 changed files with 53 additions and 12 deletions
@@ -89,6 +89,9 @@ fun ToggleableTimeAgoText(
fontSize: TextUnit = TextUnit.Unspecified,
maxLines: Int = 1,
overflow: TextOverflow = TextOverflow.Clip,
// When false, the text is not itself clickable, so an enclosing tap target (e.g. the
// chat timestamp opening the relay/delivery dialog) receives the tap instead.
toggleable: Boolean = true,
) {
val context = LocalContext.current
val nowState = LocalNowSeconds.current
@@ -125,10 +128,14 @@ fun ToggleableTimeAgoText(
maxLines = maxLines,
overflow = overflow,
modifier =
modifier.clickable(
interactionSource = interactionSource,
indication = null,
) { showAbsolute = !showAbsolute },
if (toggleable) {
modifier.clickable(
interactionSource = interactionSource,
indication = null,
) { showAbsolute = !showAbsolute }
} else {
modifier
},
)
}
@@ -41,12 +41,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.Channel
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.chatDelivery.ChatDelivery
import com.vitorpamplona.amethyst.service.relayClient.chatDelivery.RecipientDelivery
@@ -54,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.timeAbsolute
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
import com.vitorpamplona.amethyst.ui.stringRes
@@ -175,6 +178,23 @@ private fun ChatDeliveryDetailDialog(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
// Absolute timestamp header (the chat time no longer toggles to absolute
// on tap — it opens this dialog instead).
val context = LocalContext.current
Text(
text = timeAbsolute(baseNote.createdAt(), context, prefix = "").trim(),
color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font12SP,
)
// Encrypted-channel messages (Concord/Armada) are decrypted locally with no
// per-relay attribution, so seen-on is empty — fall back to the channel's own
// relays as "where this message lives".
val channelRelays =
remember(baseNote) {
baseNote.inGatherers?.firstNotNullOfOrNull { (it as? Channel)?.relays()?.takeIf { r -> r.isNotEmpty() } } ?: emptySet()
}
val recipients = delivery?.recipients
when {
!recipients.isNullOrEmpty() ->
@@ -196,6 +216,11 @@ private fun ChatDeliveryDetailDialog(
RelayDeliveryRow(relay = relay, accepted = true)
}
channelRelays.isNotEmpty() ->
channelRelays.sortedBy { it.url }.forEach { relay ->
RelayDeliveryRow(relay = relay, accepted = true)
}
else ->
// Nothing to list: our own just-sent message no relay has acknowledged
// yet (pending), or a received/old message with no recorded relays.
@@ -261,7 +261,7 @@ fun ChatMessageActionSheet(
}
// Stage one: the primary chat action (reply / edit draft) is always shown.
ChatOnlyRow(note, state, onWantsToReply, onWantsToEditDraft, { showDeliveryDialog = true }, onDismiss)
ChatOnlyRow(note, state, onWantsToReply, onWantsToEditDraft, onDismiss)
val handlers =
NoteActionHandlers(
@@ -312,6 +312,17 @@ fun ChatMessageActionSheet(
}
}
}
// Chat-specific: which relays carry this message (and, for our own,
// their acceptance). A secondary action, so it lives under Show More.
if (!note.isDraft()) {
SectionDivider()
TileRow {
ActionTile(MaterialSymbols.DoneAll, stringRes(R.string.chat_delivery_details_title)) {
showDeliveryDialog = true
}
}
}
}
}
@@ -357,7 +368,7 @@ private fun MoreActionsToggle(
// ---------- Action tile sections ----------
/** Chat-specific tiles (reply, message delivery, edit draft) that have no 3-dot menu equivalent. */
/** Chat-specific tiles (reply, edit draft) that have no 3-dot menu equivalent. */
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun ChatOnlyRow(
@@ -365,7 +376,6 @@ private fun ChatOnlyRow(
state: DropDownParams,
onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (Note) -> Unit,
onShowDelivery: () -> Unit,
onDismiss: () -> Unit,
) {
if (note.isDraft() && !state.isLoggedUser) return
@@ -384,11 +394,6 @@ private fun ChatOnlyRow(
onWantsToReply(note)
onDismiss()
}
// Which relays carry this message (and, for our own, their acceptance) — the
// reliable way to reach the relay list now that the detail row is gone.
ActionTile(MaterialSymbols.DoneAll, stringRes(R.string.chat_delivery_details_title)) {
onShowDelivery()
}
}
}
@@ -59,6 +59,10 @@ fun ChatTimeAgo(baseNote: Note) {
style = TimeAgoStyle.Short,
color = MaterialTheme.colorScheme.placeholderText,
fontSize = Font12SP,
// The chat time is wrapped in a tap target that opens the relay/delivery dialog,
// so it must not steal the tap to toggle relative/absolute. The absolute time is
// shown in that dialog instead.
toggleable = false,
)
}