From 7247d0ad5b9122e4f7bf2c2e2dd9d984c784acdb Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:06:37 +0200 Subject: [PATCH] feat(chat): constant-width two-check delivery marker that lights up green Delivery/read markers previously swapped glyphs as acknowledgements arrived, reflowing the text around them. Both checks now render from the start in a disabled grey and simply recolour as the state advances (delivered lights the first check, read lights both), so nothing ever pushes message text around. Read receipts use the app's primary green instead of the blue accent, a quick colour tween lights the checks up, and the standalone marker adds a snappy scale pop when the state advances. Applies to both matrix and bubbles modes. --- .../com/bitchat/android/ui/ChatUIUtils.kt | 26 +++- .../bitchat/android/ui/MessageComponents.kt | 144 +++++++++++------- app/src/main/res/values-fil/strings.xml | 6 - app/src/main/res/values-fr/strings.xml | 6 - app/src/main/res/values-he/strings.xml | 6 - app/src/main/res/values-it/strings.xml | 6 - app/src/main/res/values-mg/strings.xml | 6 - app/src/main/res/values-ms/strings.xml | 6 - app/src/main/res/values-ne/strings.xml | 6 - app/src/main/res/values-nl/strings.xml | 6 - app/src/main/res/values-pl/strings.xml | 6 - app/src/main/res/values-ru/strings.xml | 7 - app/src/main/res/values-sv/strings.xml | 7 - app/src/main/res/values-ta/strings.xml | 6 - app/src/main/res/values-tr/strings.xml | 7 - app/src/main/res/values-uk/strings.xml | 6 - app/src/main/res/values-zh-rCN/strings.xml | 6 - app/src/main/res/values-zh-rTW/strings.xml | 6 - app/src/main/res/values-zh/strings.xml | 6 - app/src/main/res/values/strings.xml | 8 - 20 files changed, 106 insertions(+), 177 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt index a67f6a04..485fb9b0 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatUIUtils.kt @@ -194,28 +194,38 @@ private fun appendMutedTimestamp( } /** - * A delivery-status glyph rendered inline, trailing the timestamp inside a bubble. + * Per-check colours for the inline delivery marker trailing the timestamp inside a bubble. * - * Mirrors the mapping used by the standalone delivery marker so the two never disagree. + * 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 text: String, - val color: Color, - val bold: Boolean, + val firstColor: Color, + val secondColor: Color, ) private fun appendStatusGlyph( builder: AnnotatedString.Builder, glyph: MessageStatusGlyph, ) { + builder.append(" ") builder.pushStyle( SpanStyle( - color = glyph.color, + color = glyph.firstColor, fontSize = ChatVisualTokens.SystemTimeFontSize, - fontWeight = if (glyph.bold) FontWeight.Bold else FontWeight.Normal, + fontWeight = FontWeight.Normal, ) ) - builder.append(" ${glyph.text}") + builder.append("✓") + builder.pop() + builder.pushStyle( + SpanStyle( + color = glyph.secondColor, + fontSize = ChatVisualTokens.SystemTimeFontSize, + fontWeight = FontWeight.Normal, + ) + ) + builder.append("✓") builder.pop() } diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index ce127b8c..6b3b52af 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -8,7 +8,7 @@ import android.content.Intent import android.net.Uri import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FiniteAnimationSpec @@ -16,9 +16,6 @@ import androidx.compose.animation.core.Spring import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -763,25 +760,26 @@ internal fun TextMessageLayout( onMessageLongPress?.invoke(message) } - // Bubble mode pulls the delivery marker into the bubble, trailing the timestamp, so the - // whole message reads as one unit. Same glyph mapping as the standalone marker. - val statusGlyph = if (bubbles && isSelf && message.isPrivate) { - message.deliveryStatus?.let { status -> - when (status) { - is DeliveryStatus.Sending -> - MessageStatusGlyph(stringResource(R.string.status_sending), colorScheme.primary.copy(alpha = 0.6f), bold = false) - is DeliveryStatus.Sent -> - MessageStatusGlyph(stringResource(R.string.status_pending), colorScheme.primary.copy(alpha = 0.6f), bold = false) - is DeliveryStatus.Delivered -> - MessageStatusGlyph(stringResource(R.string.status_sent), colorScheme.primary.copy(alpha = 0.8f), bold = false) - is DeliveryStatus.Read -> - MessageStatusGlyph(stringResource(R.string.status_delivered), colorScheme.secondary, bold = true) - is DeliveryStatus.Failed -> - MessageStatusGlyph(stringResource(R.string.status_failed), colorScheme.error, bold = false) - is DeliveryStatus.PartiallyDelivered -> - MessageStatusGlyph(stringResource(R.string.status_sent), colorScheme.primary.copy(alpha = 0.6f), bold = false) - } - } + // 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 } @@ -1145,43 +1143,77 @@ private fun redeemCashu(context: Context, token: String, preferWallet: Boolean) runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(web))) } } +/** + * Per-check target colours for the delivery marker. + * + * Both checks always render — grey (disabled) until an acknowledgement turns them on — so a + * status change recolours in place and never reflows text around it. Read receipts use the + * app's primary green rather than a separate accent. [status] == null yields the all-grey + * baseline used while a message is still being sent. + */ +private fun deliveryCheckColors(status: DeliveryStatus?, colorScheme: ColorScheme): Pair { + val grey = colorScheme.onSurface.copy(alpha = 0.35f) + val green = colorScheme.primary + return when (status) { + is DeliveryStatus.Read -> green to green + is DeliveryStatus.Delivered -> green to grey + is DeliveryStatus.PartiallyDelivered -> green to grey + is DeliveryStatus.Failed -> colorScheme.error to colorScheme.error + else -> grey to grey + } +} + +/** Acknowledgement progress ordering, used to fire the pop only when the state advances. */ +private fun deliveryCheckRank(status: DeliveryStatus): Int = when (status) { + is DeliveryStatus.Read -> 3 + is DeliveryStatus.Delivered -> 2 + is DeliveryStatus.PartiallyDelivered -> 2 + is DeliveryStatus.Failed -> 1 + else -> 0 +} + @Composable fun DeliveryStatusIcon(status: DeliveryStatus) { val colorScheme = MaterialTheme.colorScheme + val (firstTarget, secondTarget) = deliveryCheckColors(status, colorScheme) + val first by animateColorAsState( + targetValue = firstTarget, + animationSpec = tween(BitchatMotion.QUICK_MS), + label = "firstCheckColor", + ) + val second by animateColorAsState( + targetValue = secondTarget, + animationSpec = tween(BitchatMotion.QUICK_MS), + label = "secondCheckColor", + ) - // Status advances on its own as acks come back, so a hard glyph swap reads as a flicker. - // Keyed on the status *type* rather than the instance, because Delivered/Read carry a - // timestamp that would otherwise retrigger the transition on every identical update. - AnimatedContent( - targetState = status::class, - transitionSpec = { - fadeIn(tween(BitchatMotion.STANDARD_MS)) togetherWith - fadeOut(tween(BitchatMotion.QUICK_MS)) - }, - label = "deliveryStatus" - ) { statusClass -> - val (text, color, weight) = when (statusClass) { - DeliveryStatus.Sending::class -> - Triple(R.string.status_sending, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal) - // Subtle hollow marker for Sent; a single check is reserved for Delivered (iOS parity). - DeliveryStatus.Sent::class -> - Triple(R.string.status_pending, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal) - DeliveryStatus.Delivered::class -> - Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.8f), FontWeight.Normal) - DeliveryStatus.Read::class -> - Triple(R.string.status_delivered, colorScheme.secondary, FontWeight.Bold) - DeliveryStatus.Failed::class -> - Triple(R.string.status_failed, colorScheme.error, FontWeight.Normal) - // A single subdued check, without the numeric label. - else -> - Triple(R.string.status_sent, colorScheme.primary.copy(alpha = 0.6f), FontWeight.Normal) + // Snappy micro pop when the state advances to (more) acknowledged. Keyed on the rank, not + // the instance, because Delivered/Read carry timestamps that would retrigger it otherwise. + val scale = remember { Animatable(1f) } + LaunchedEffect(deliveryCheckRank(status)) { + if (deliveryCheckRank(status) >= 2) { + scale.snapTo(1.3f) + scale.animateTo(1f, spring(dampingRatio = 0.55f, stiffness = 900f)) } - - Text( - text = stringResource(text), - fontSize = 10.sp, - color = color, - fontWeight = weight - ) } + + val text = remember(first, second) { + androidx.compose.ui.text.buildAnnotatedString { + pushStyle(androidx.compose.ui.text.SpanStyle(color = first)) + append("✓") + pop() + pushStyle(androidx.compose.ui.text.SpanStyle(color = second)) + append("✓") + pop() + } + } + Text( + text = text, + fontSize = 10.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.graphicsLayer { + scaleX = scale.value + scaleY = scale.value + } + ) } diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index 66cd93bd..fa4aea0c 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -293,12 +293,6 @@ File - - - - ✓✓ - - 📷 nagpadala ng larawan diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index aa857f44..a884d35e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -294,12 +294,6 @@ Fichier - - - - ✓✓ - - 📷 a envoyé une image diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 86e4bbcb..f742042e 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -349,12 +349,6 @@ image/* תמונה קובץ - - - - ✓✓ - - 📷 שלח/ה תמונה 🎤 שלח/ה הודעה קולית 📎 שלח/ה קובץ diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a8ecb688..af59fad3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -338,12 +338,6 @@ ⚠️ - - - - ✓✓ - - 📄 diff --git a/app/src/main/res/values-mg/strings.xml b/app/src/main/res/values-mg/strings.xml index 52617e72..b7182e09 100644 --- a/app/src/main/res/values-mg/strings.xml +++ b/app/src/main/res/values-mg/strings.xml @@ -302,12 +302,6 @@ Rakitra - - - - ✓✓ - - 📷 nandefasa sary diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index c6391e6f..79bf5b1c 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -374,12 +374,6 @@ Fail - - - - ✓✓ - - 📷 menghantar imej diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 7a3da0bd..59d52853 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -293,12 +293,6 @@ फाइल - - - - ✓✓ - - 📷 तस्वीर पठाइयो diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 64326ae5..1e46f196 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -339,12 +339,6 @@ ? - - - - ✓✓ - - 📄 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 0d2f2689..3ec84dd4 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -347,12 +347,6 @@ image/* Obraz Plik - - - - ✓✓ - - 📷 wysłał(a) obraz 🎤 wysłał(a) wiadomość głosową 📎 wysłał(a) plik diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 613b008d..02b1175d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -276,13 +276,6 @@ Изображение Файл - - - - ✓✓ - - - 📷 отправил изображение 🎤 отправил голосовое 📎 отправил файл diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index c0457997..b958faad 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -276,13 +276,6 @@ Bild Fil - - - - ✓✓ - - - 📷 skickade en bild 🎤 skickade ett röstmeddelande 📎 skickade en fil diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 5e7f8bdd..243f2112 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -352,12 +352,6 @@ image/* படம் கோப்பு - - - - ✓✓ - - 📷 ஒரு படத்தை அனுப்பியது 🎤 ஒரு குரல் செய்தியை அனுப்பியது 📎 ஒரு கோப்பை அனுப்பியது diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 533e3252..251d6a46 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -276,13 +276,6 @@ Görsel Dosya - - - - ✓✓ - - - 📷 görsel gönderdi 🎤 sesli mesaj gönderdi 📎 dosya gönderdi diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cdc73c91..f248ff71 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -341,12 +341,6 @@ image/* Зображення Файл - - - - ✓✓ - - 📷 надіслав(-ла) зображення 🎤 надіслав(-ла) голосове повідомлення 📎 надіслав(-ла) файл diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b9e35372..fca6b07b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -347,12 +347,6 @@ image/* 图片 文件 - - - - ✓✓ - - 📷 发送了一张图片 🎤 发送了一条语音消息 📎 发送了一个文件 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 36b6bbdd..f427dbaf 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -347,12 +347,6 @@ image/* 圖片 檔案 - - - - ✓✓ - - 📷 傳送了一張圖片 🎤 傳送了一則語音訊息 📎 傳送了一個檔案 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 751580a6..35f7477a 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -293,12 +293,6 @@ 文件 - - - - ✓✓ - - 📷 发送了图片 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 690db90b..1c20d534 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -559,14 +559,6 @@ Image File - - - - - ✓✓ - - - 📷 sent an image 🎤 sent a voice message