From 7569fba7b4f2a6f8458ad1b46e3550885bbe1fc1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:57:40 +0200 Subject: [PATCH] feat(chat): align self media rows with bubbles chat UI mode In bubbles mode, self-authored image, voice-note, and file rows now align to the end side with the same tail-corner cue as text bubbles, instead of sitting on the received side where they read as someone else's content. Grouped self voice notes also keep the run on the correct side. VoiceNotePlayer gains an optional modifier so the player can hug the end side at a capped width. Received media and matrix mode are unchanged. --- .../bitchat/android/ui/MessageComponents.kt | 21 +++++++++++++-- .../android/ui/media/AudioMessageItem.kt | 16 +++++++++--- .../android/ui/media/ImageMessageItem.kt | 26 +++++++++++++++---- .../android/ui/media/VoiceNotePlayer.kt | 3 ++- 4 files changed, 55 insertions(+), 11 deletions(-) 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 35166672..b9e51f75 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -506,6 +506,7 @@ fun MessageItem( colorScheme = colorScheme, timeFormatter = timeFormatter, showSender = showSender, + bubbles = bubbles, onNicknameClick = onNicknameClick, onMessageLongPress = onMessageLongPress, onCancelTransfer = onCancelTransfer, @@ -524,6 +525,7 @@ fun MessageItem( colorScheme = colorScheme, timeFormatter = timeFormatter, showSender = showSender, + bubbles = bubbles, onNicknameClick = onNicknameClick, onMessageLongPress = onMessageLongPress, onCancelTransfer = onCancelTransfer, @@ -544,7 +546,15 @@ fun MessageItem( } else -> null to null } - Column(modifier = modifier.fillMaxWidth()) { + Column( + modifier = modifier.fillMaxWidth(), + // Bubble mode aligns self-authored file rows to the end side, mirroring text bubbles. + horizontalAlignment = if (bubbles && message.isFromSelf(currentUserNickname, meshService.myPeerID)) { + Alignment.End + } else { + Alignment.Start + }, + ) { // Header: nickname + timestamp line above the file, identical styling to text messages val headerText = formatMessageHeaderAnnotatedString( message = message, @@ -590,7 +600,14 @@ fun MessageItem( null } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (bubbles && message.isFromSelf(currentUserNickname, meshService.myPeerID)) { + Arrangement.End + } else { + Arrangement.Start + } + ) { Box { if (packet != null) { if (overrideProgress != null) { diff --git a/app/src/main/java/com/bitchat/android/ui/media/AudioMessageItem.kt b/app/src/main/java/com/bitchat/android/ui/media/AudioMessageItem.kt index c646d570..9252123c 100644 --- a/app/src/main/java/com/bitchat/android/ui/media/AudioMessageItem.kt +++ b/app/src/main/java/com/bitchat/android/ui/media/AudioMessageItem.kt @@ -22,6 +22,7 @@ import com.bitchat.android.mesh.MeshService import com.bitchat.android.model.BitchatMessage import androidx.compose.material3.ColorScheme import com.bitchat.android.ui.theme.LocalBitchatPalette +import com.bitchat.android.ui.isFromSelf import java.text.SimpleDateFormat @Composable @@ -35,10 +36,13 @@ fun AudioMessageItem( onMessageLongPress: ((BitchatMessage) -> Unit)?, onCancelTransfer: ((BitchatMessage) -> Unit)?, modifier: Modifier = Modifier, - showSender: Boolean = true + showSender: Boolean = true, + bubbles: Boolean = false ) { val palette = LocalBitchatPalette.current val path = message.content.trim() + // Bubble mode aligns self-authored voice notes to the end side, mirroring text bubbles. + val isSelfInBubbles = bubbles && message.isFromSelf(currentUserNickname, meshService.myPeerID) // Derive sending progress if applicable val (overrideProgress, overrideColor) = when (val st = message.deliveryStatus) { is com.bitchat.android.model.DeliveryStatus.PartiallyDelivered -> { @@ -48,7 +52,10 @@ fun AudioMessageItem( } else -> null to null } - Column(modifier = modifier.fillMaxWidth()) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = if (isSelfInBubbles) Alignment.End else Alignment.Start, + ) { // Header: nickname + timestamp line above the audio note, identical styling to text messages val headerText = com.bitchat.android.ui.formatMessageHeaderAnnotatedString( message = message, @@ -81,7 +88,10 @@ fun AudioMessageItem( VoiceNotePlayer( path = path, progressOverride = overrideProgress, - progressColor = overrideColor + progressColor = overrideColor, + // Self voice notes hug the end side like other self content instead of + // spanning the full row. + modifier = if (isSelfInBubbles) Modifier.widthIn(max = 300.dp) else Modifier ) val showCancel = message.sender == currentUserNickname && (message.deliveryStatus is com.bitchat.android.model.DeliveryStatus.PartiallyDelivered) if (showCancel) { diff --git a/app/src/main/java/com/bitchat/android/ui/media/ImageMessageItem.kt b/app/src/main/java/com/bitchat/android/ui/media/ImageMessageItem.kt index 5d9908fc..ce7ba470 100644 --- a/app/src/main/java/com/bitchat/android/ui/media/ImageMessageItem.kt +++ b/app/src/main/java/com/bitchat/android/ui/media/ImageMessageItem.kt @@ -28,6 +28,7 @@ import com.bitchat.android.model.BitchatMessageType import androidx.compose.material3.ColorScheme import com.bitchat.android.core.ui.component.text.AnnotatedClickableText import com.bitchat.android.ui.theme.LocalBitchatPalette +import com.bitchat.android.ui.isFromSelf import java.text.SimpleDateFormat @Composable @@ -43,11 +44,23 @@ fun ImageMessageItem( onCancelTransfer: ((BitchatMessage) -> Unit)?, onImageClick: ((String, List, Int) -> Unit)?, modifier: Modifier = Modifier, - showSender: Boolean = true + showSender: Boolean = true, + bubbles: Boolean = false ) { val palette = LocalBitchatPalette.current val path = message.content.trim() - Column(modifier = modifier.fillMaxWidth()) { + // Bubble mode aligns self-authored media to the end side, mirroring text bubbles; the + // corner on the speaker's side is tightened into the same subtle tail. + val isSelfInBubbles = bubbles && message.isFromSelf(currentUserNickname, meshService.myPeerID) + val imageShape = when { + !bubbles -> androidx.compose.foundation.shape.RoundedCornerShape(10.dp) + isSelfInBubbles -> androidx.compose.foundation.shape.RoundedCornerShape(10.dp, 10.dp, 3.dp, 10.dp) + else -> androidx.compose.foundation.shape.RoundedCornerShape(10.dp, 10.dp, 10.dp, 3.dp) + } + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = if (isSelfInBubbles) Alignment.End else Alignment.Start, + ) { val headerText = com.bitchat.android.ui.formatMessageHeaderAnnotatedString( message = message, currentUserNickname = currentUserNickname, @@ -91,7 +104,10 @@ fun ImageMessageItem( is com.bitchat.android.model.DeliveryStatus.PartiallyDelivered -> if (st.total > 0) st.reached.toFloat() / st.total.toFloat() else 0f else -> null } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = if (isSelfInBubbles) Arrangement.End else Arrangement.Start + ) { Box { if (progressFraction != null && progressFraction < 1f && message.sender == currentUserNickname) { // Cyberpunk block-reveal while sending @@ -103,7 +119,7 @@ fun ImageMessageItem( modifier = Modifier .widthIn(max = 300.dp) .aspectRatio(aspect) - .clip(androidx.compose.foundation.shape.RoundedCornerShape(10.dp)) + .clip(imageShape) .clickable { val currentIndex = imagePaths.indexOf(path) onImageClick?.invoke(path, imagePaths, currentIndex) @@ -117,7 +133,7 @@ fun ImageMessageItem( modifier = Modifier .widthIn(max = 300.dp) .aspectRatio(aspect) - .clip(androidx.compose.foundation.shape.RoundedCornerShape(10.dp)) + .clip(imageShape) .clickable { val currentIndex = imagePaths.indexOf(path) onImageClick?.invoke(path, imagePaths, currentIndex) diff --git a/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt b/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt index 10109194..1f3f567e 100644 --- a/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt +++ b/app/src/main/java/com/bitchat/android/ui/media/VoiceNotePlayer.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.unit.sp @Composable fun VoiceNotePlayer( path: String, + modifier: Modifier = Modifier, progressOverride: Float? = null, progressColor: Color? = null ) { @@ -86,7 +87,7 @@ fun VoiceNotePlayer( DisposableEffect(Unit) { onDispose { try { player.release() } catch (_: Exception) {} } } Row( - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) {