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) ) {