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