mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
feat(chat): anchor delivery checks to the bubble's bottom-end corner
Instead of trailing the timestamp mid-line, the delivery checks now park at the bubble's bottom-end corner like classic messengers, with the body text reserving a small end inset so the last line never collides with them. The checks keep their constant-width grey-to-green behaviour, colour tween, and scale pop (shared DeliveryStatusIcon). Matrix mode is unchanged.
This commit is contained in:
parent
7247d0ad5b
commit
6eafa5932d
@ -193,42 +193,6 @@ private fun appendMutedTimestamp(
|
||||
builder.pop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-check colours for the inline delivery marker trailing the timestamp inside a bubble.
|
||||
*
|
||||
* Both checks always render — grey until an acknowledgement turns them on — so a status change
|
||||
* recolours in place and can never reflow the message text.
|
||||
*/
|
||||
data class MessageStatusGlyph(
|
||||
val firstColor: Color,
|
||||
val secondColor: Color,
|
||||
)
|
||||
|
||||
private fun appendStatusGlyph(
|
||||
builder: AnnotatedString.Builder,
|
||||
glyph: MessageStatusGlyph,
|
||||
) {
|
||||
builder.append(" ")
|
||||
builder.pushStyle(
|
||||
SpanStyle(
|
||||
color = glyph.firstColor,
|
||||
fontSize = ChatVisualTokens.SystemTimeFontSize,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
)
|
||||
builder.append("✓")
|
||||
builder.pop()
|
||||
builder.pushStyle(
|
||||
SpanStyle(
|
||||
color = glyph.secondColor,
|
||||
fontSize = ChatVisualTokens.SystemTimeFontSize,
|
||||
fontWeight = FontWeight.Normal,
|
||||
)
|
||||
)
|
||||
builder.append("✓")
|
||||
builder.pop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message body: neutral text with mention/URL/geohash accents, followed by an inline
|
||||
* trailing timestamp.
|
||||
@ -244,8 +208,7 @@ fun formatTextMessageBody(
|
||||
linkColor: Color,
|
||||
mentionPeerIdentities: Map<String, PeerIdentity> = emptyMap(),
|
||||
timeFormatter: SimpleDateFormat = SimpleDateFormat(CHAT_TIMESTAMP_PATTERN, Locale.getDefault()),
|
||||
includeTimestamp: Boolean = true,
|
||||
statusGlyph: MessageStatusGlyph? = null
|
||||
includeTimestamp: Boolean = true
|
||||
): AnnotatedString {
|
||||
val builder = AnnotatedString.Builder()
|
||||
|
||||
@ -262,9 +225,6 @@ fun formatTextMessageBody(
|
||||
if (includeTimestamp) {
|
||||
appendBodyTimestamp(builder, message, palette, timeFormatter)
|
||||
}
|
||||
if (statusGlyph != null) {
|
||||
appendStatusGlyph(builder, statusGlyph)
|
||||
}
|
||||
return builder.toAnnotatedString()
|
||||
}
|
||||
|
||||
|
||||
@ -760,30 +760,6 @@ internal fun TextMessageLayout(
|
||||
onMessageLongPress?.invoke(message)
|
||||
}
|
||||
|
||||
// Bubble mode pulls the delivery marker into the bubble, trailing the timestamp. Both
|
||||
// checks render from the start — grey until an acknowledgement turns them green — so a
|
||||
// status change recolours in place and never reflows the text. The colour transition is
|
||||
// animated, which reads as the checks lighting up rather than popping in.
|
||||
val checkTargets = deliveryCheckColors(
|
||||
status = if (bubbles && isSelf && message.isPrivate) message.deliveryStatus else null,
|
||||
colorScheme = colorScheme,
|
||||
)
|
||||
val firstCheck by animateColorAsState(
|
||||
targetValue = checkTargets.first,
|
||||
animationSpec = tween(BitchatMotion.QUICK_MS),
|
||||
label = "firstCheckColor",
|
||||
)
|
||||
val secondCheck by animateColorAsState(
|
||||
targetValue = checkTargets.second,
|
||||
animationSpec = tween(BitchatMotion.QUICK_MS),
|
||||
label = "secondCheckColor",
|
||||
)
|
||||
val statusGlyph = if (bubbles && isSelf && message.isPrivate && message.deliveryStatus != null) {
|
||||
MessageStatusGlyph(firstColor = firstCheck, secondColor = secondCheck)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// The timestamp trails the body rather than occupying its own column, so a short message
|
||||
// no longer reserves a full-width row for eight grey characters.
|
||||
val bodyText = remember(
|
||||
@ -793,8 +769,7 @@ internal fun TextMessageLayout(
|
||||
colorScheme.onSurface,
|
||||
colorScheme.secondary,
|
||||
mentionPeerIdentities,
|
||||
timeFormatter,
|
||||
statusGlyph
|
||||
timeFormatter
|
||||
) {
|
||||
formatTextMessageBody(
|
||||
message = displayMessage,
|
||||
@ -804,7 +779,6 @@ internal fun TextMessageLayout(
|
||||
linkColor = colorScheme.secondary,
|
||||
mentionPeerIdentities = mentionPeerIdentities,
|
||||
timeFormatter = timeFormatter,
|
||||
statusGlyph = statusGlyph,
|
||||
)
|
||||
}
|
||||
|
||||
@ -966,32 +940,52 @@ private fun BubbleTextMessageLayout(
|
||||
)
|
||||
}
|
||||
|
||||
AnnotatedClickableText(
|
||||
text = bodyText,
|
||||
annotationTags = listOf("geohash_click", "url_click"),
|
||||
onAnnotationClick = { tag, item ->
|
||||
when (tag) {
|
||||
"geohash_click" -> {
|
||||
navigateToGeohash(context, item)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
true
|
||||
}
|
||||
Box {
|
||||
AnnotatedClickableText(
|
||||
text = bodyText,
|
||||
annotationTags = listOf("geohash_click", "url_click"),
|
||||
onAnnotationClick = { tag, item ->
|
||||
when (tag) {
|
||||
"geohash_click" -> {
|
||||
navigateToGeohash(context, item)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
true
|
||||
}
|
||||
|
||||
"url_click" -> {
|
||||
openMessageUrl(context, item)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
true
|
||||
}
|
||||
"url_click" -> {
|
||||
openMessageUrl(context, item)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
else -> false
|
||||
}
|
||||
},
|
||||
onLongPress = onLongPress,
|
||||
// Keep the last line clear of the checks parked at the bubble's corner.
|
||||
modifier = Modifier.padding(
|
||||
end = if (isSelf && message.isPrivate && message.deliveryStatus != null) {
|
||||
ChatVisualTokens.BubbleStatusInset
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
),
|
||||
fontFamily = BitchatFontFamily,
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible,
|
||||
style = MessageBodyTextStyle.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
|
||||
// Delivery checks anchor the bubble's bottom-end corner, like classic
|
||||
// messengers, instead of trailing the timestamp mid-line.
|
||||
if (isSelf && message.isPrivate) {
|
||||
message.deliveryStatus?.let { status ->
|
||||
Box(modifier = Modifier.align(Alignment.BottomEnd)) {
|
||||
DeliveryStatusIcon(status = status)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongPress = onLongPress,
|
||||
fontFamily = BitchatFontFamily,
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible,
|
||||
style = MessageBodyTextStyle.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +61,9 @@ internal object ChatVisualTokens {
|
||||
/** Author-colour hairline around a bubble; stronger than the fill so the shape reads. */
|
||||
const val BubbleBorderAlpha: Float = 0.38f
|
||||
|
||||
/** End inset reserving room for the delivery checks parked at a bubble's bottom-end corner. */
|
||||
val BubbleStatusInset: Dp = 18.dp
|
||||
|
||||
const val SenderSuffixAlpha: Float = 0.60f
|
||||
const val HighlightAlpha: Float = 0.20f
|
||||
const val MutedTextAlpha: Float = 0.50f
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user