@@ -624,28 +624,34 @@ private fun UserBubble(
624624 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
625625 // Crossfade the send-status slot so SENDING → FAILED → null transitions
626626 // don't snap; a subtle fade reads as a polish detail rather than a pop.
627− androidx.compose.animation.AnimatedContent(
628− targetState = sendStatus,
629− transitionSpec = {
630− androidx.compose.animation.fadeIn() togetherWith
631− androidx.compose.animation.fadeOut()
632− },
633− label = "send_status",
634− ) { status ->
635− when (status) {
636− MessageSendStatus.SENDING -> {
637− CircularProgressIndicator(
638− Modifier.size(12.dp),
639− strokeWidth = 1.5.dp,
640− color = MaterialTheme.colorScheme.onSurfaceVariant,
641− )
642− Text(
643− stringResource(R.string.message_sending),
644− style = MaterialTheme.typography.labelSmall,
645− color = MaterialTheme.colorScheme.onSurfaceVariant,
646− )
647− }
648− MessageSendStatus.FAILED -> {
627+ // Only mounted when there's an actual status to show: a null status (the
628+ // common case for any historical/confirmed message) renders nothing at all.
629+ // Passing null into AnimatedContent crashes its measure policy (an NPE in
630+ // the SubcomposeLayout's ChildData lookup when the null-state content is
631+ // empty), so gate the whole composable on sendStatus != null.
632+ sendStatus?.let { status ->
633+ androidx.compose.animation.AnimatedContent(
634+ targetState = status,
635+ transitionSpec = {
636+ androidx.compose.animation.fadeIn() togetherWith
637+ androidx.compose.animation.fadeOut()
638+ },
639+ label = "send_status",
640+ ) { current ->
641+ when (current) {
642+ MessageSendStatus.SENDING -> {
643+ CircularProgressIndicator(
644+ Modifier.size(12.dp),
645+ strokeWidth = 1.5.dp,
646+ color = MaterialTheme.colorScheme.onSurfaceVariant,
647+ )
648+ Text(
649+ stringResource(R.string.message_sending),
650+ style = MaterialTheme.typography.labelSmall,
651+ color = MaterialTheme.colorScheme.onSurfaceVariant,
652+ )
653+ }
654+ MessageSendStatus.FAILED -> {
649655 // Tap-to-retry: the whole failed label is tappable to re-send (far more
650656 // discoverable than relying on the transient snackbar). A dismiss (×) button
651657 // removes the abandoned message entirely, since its text will never match a
@@ -704,7 +710,7 @@ private fun UserBubble(
704710 }
705711 }
706712 }
707− null -> {}
713+ }
708714 }
709715 }
710716 if (isEdited && sendStatus == null) {