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.
This commit is contained in:
callebtc 2026-08-01 13:57:40 +02:00
parent 94d5fa3084
commit 7569fba7b4
4 changed files with 55 additions and 11 deletions

View File

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

View File

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

View File

@ -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<String>, 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)

View File

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