mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
Merge pull request #860 from permissionlesstech/feat/bubble-media-messages
feat(ui): wrap image and voice messages in bubble shells
This commit is contained in:
commit
49753ccb88
@ -461,9 +461,9 @@ fun MessageItem(
|
||||
}
|
||||
}
|
||||
|
||||
// Bubble mode: text messages carry the marker inline, trailing the timestamp. Media
|
||||
// rows have no inline text, so their marker stays beneath the end-aligned card.
|
||||
if (bubbles && message.type != BitchatMessageType.Message &&
|
||||
// Bubble mode: text and media bubbles carry the marker inline, trailing the timestamp.
|
||||
// File rows have no bubble shell, so their marker stays beneath the end-aligned row.
|
||||
if (bubbles && message.type == BitchatMessageType.File &&
|
||||
message.isPrivate && message.sender == currentUserNickname
|
||||
) {
|
||||
message.deliveryStatus?.let { status ->
|
||||
|
||||
@ -24,7 +24,6 @@ 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
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@ -48,8 +47,6 @@ fun AudioMessageItem(
|
||||
.getInstance(context).liveMessageIDs.collectAsState()
|
||||
val isLive = message.id in liveMessageIDs
|
||||
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 -> {
|
||||
@ -59,9 +56,55 @@ fun AudioMessageItem(
|
||||
}
|
||||
else -> null to null
|
||||
}
|
||||
// Bubble mode wraps the voice note in the same tinted shell as text bubbles; Matrix mode
|
||||
// keeps the flat header-plus-player layout.
|
||||
if (bubbles) {
|
||||
MediaBubbleShell(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
myPeerID = meshService.myPeerID,
|
||||
showSender = showSender,
|
||||
timeFormatter = timeFormatter,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (isLive) {
|
||||
androidx.compose.material3.Text(
|
||||
text = "LIVE",
|
||||
color = Color(0xFFFFB300),
|
||||
style = androidx.compose.material3.MaterialTheme.typography.labelSmall,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
}
|
||||
VoiceNotePlayer(
|
||||
path = path,
|
||||
progressOverride = overrideProgress,
|
||||
progressColor = overrideColor,
|
||||
modifier = Modifier.widthIn(max = 260.dp)
|
||||
)
|
||||
val showCancel = message.sender == currentUserNickname && (message.deliveryStatus is com.bitchat.android.model.DeliveryStatus.PartiallyDelivered)
|
||||
if (showCancel) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(26.dp)
|
||||
.background(Color.Gray.copy(alpha = 0.6f), CircleShape)
|
||||
.clickable { onCancelTransfer?.invoke(message) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(R.string.cd_cancel), tint = Color.White, modifier = Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = if (isSelfInBubbles) Alignment.End else Alignment.Start,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
// Header: nickname + timestamp line above the audio note, identical styling to text messages
|
||||
val headerText = com.bitchat.android.ui.formatMessageHeaderAnnotatedString(
|
||||
@ -104,9 +147,6 @@ fun AudioMessageItem(
|
||||
path = path,
|
||||
progressOverride = overrideProgress,
|
||||
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) {
|
||||
|
||||
@ -2,9 +2,11 @@ package com.bitchat.android.ui.media
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
@ -28,7 +30,6 @@ 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
|
||||
@ -49,17 +50,53 @@ fun ImageMessageItem(
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val path = message.content.trim()
|
||||
// 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)
|
||||
// Bubble mode wraps the image in the same tinted shell as text bubbles; Matrix mode keeps
|
||||
// the flat header-plus-card layout.
|
||||
val bubblesMode = bubbles
|
||||
val imageShape = androidx.compose.foundation.shape.RoundedCornerShape(10.dp)
|
||||
|
||||
val context = LocalContext.current
|
||||
val bmp = remember(path) { try { android.graphics.BitmapFactory.decodeFile(path) } catch (_: Exception) { null } }
|
||||
|
||||
// Collect all image paths from messages for swipe navigation
|
||||
val imagePaths = remember(messages) {
|
||||
messages.filter { it.type == BitchatMessageType.Image }
|
||||
.map { it.content.trim() }
|
||||
}
|
||||
val haptic = LocalHapticFeedback.current
|
||||
|
||||
if (bubblesMode) {
|
||||
MediaBubbleShell(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
myPeerID = meshService.myPeerID,
|
||||
showSender = showSender,
|
||||
timeFormatter = timeFormatter,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onLongPress = { onMessageLongPress?.invoke(message) },
|
||||
modifier = modifier,
|
||||
) {
|
||||
ImageMessageCard(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
path = path,
|
||||
bmp = bmp,
|
||||
imagePaths = imagePaths,
|
||||
imageShape = imageShape,
|
||||
onImageClick = onImageClick,
|
||||
onCancelTransfer = onCancelTransfer,
|
||||
onLongPress = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onMessageLongPress?.invoke(message)
|
||||
},
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = if (isSelfInBubbles) Alignment.End else Alignment.Start,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
val headerText = com.bitchat.android.ui.formatMessageHeaderAnnotatedString(
|
||||
message = message,
|
||||
@ -70,7 +107,6 @@ fun ImageMessageItem(
|
||||
timeFormatter = timeFormatter,
|
||||
includeSender = showSender
|
||||
)
|
||||
val haptic = LocalHapticFeedback.current
|
||||
AnnotatedClickableText(
|
||||
text = headerText,
|
||||
annotationTags = listOf("nickname_click"),
|
||||
@ -88,78 +124,93 @@ fun ImageMessageItem(
|
||||
color = colorScheme.onSurface,
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
val bmp = remember(path) { try { android.graphics.BitmapFactory.decodeFile(path) } catch (_: Exception) { null } }
|
||||
|
||||
// Collect all image paths from messages for swipe navigation
|
||||
val imagePaths = remember(messages) {
|
||||
messages.filter { it.type == BitchatMessageType.Image }
|
||||
.map { it.content.trim() }
|
||||
}
|
||||
|
||||
if (bmp != null) {
|
||||
val img = bmp.asImageBitmap()
|
||||
val aspect = (bmp.width.toFloat() / bmp.height.toFloat()).takeIf { it.isFinite() && it > 0 } ?: 1f
|
||||
val progressFraction: Float? = when (val st = message.deliveryStatus) {
|
||||
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 = if (isSelfInBubbles) Arrangement.End else Arrangement.Start
|
||||
) {
|
||||
Box {
|
||||
if (progressFraction != null && progressFraction < 1f && message.sender == currentUserNickname) {
|
||||
// Cyberpunk block-reveal while sending
|
||||
BlockRevealImage(
|
||||
bitmap = img,
|
||||
progress = progressFraction,
|
||||
blocksX = 24,
|
||||
blocksY = 16,
|
||||
modifier = Modifier
|
||||
.widthIn(max = 300.dp)
|
||||
.aspectRatio(aspect)
|
||||
.clip(imageShape)
|
||||
.clickable {
|
||||
val currentIndex = imagePaths.indexOf(path)
|
||||
onImageClick?.invoke(path, imagePaths, currentIndex)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Fully revealed image
|
||||
Image(
|
||||
bitmap = img,
|
||||
contentDescription = stringResource(com.bitchat.android.R.string.cd_image),
|
||||
modifier = Modifier
|
||||
.widthIn(max = 300.dp)
|
||||
.aspectRatio(aspect)
|
||||
.clip(imageShape)
|
||||
.clickable {
|
||||
val currentIndex = imagePaths.indexOf(path)
|
||||
onImageClick?.invoke(path, imagePaths, currentIndex)
|
||||
},
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
// Cancel button overlay during sending
|
||||
val showCancel = message.sender == currentUserNickname && (message.deliveryStatus is com.bitchat.android.model.DeliveryStatus.PartiallyDelivered)
|
||||
if (showCancel) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(4.dp)
|
||||
.size(22.dp)
|
||||
.background(Color.Gray.copy(alpha = 0.6f), CircleShape)
|
||||
.clickable { onCancelTransfer?.invoke(message) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(com.bitchat.android.R.string.cd_cancel), tint = Color.White, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Start) {
|
||||
ImageMessageCard(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
path = path,
|
||||
bmp = bmp,
|
||||
imagePaths = imagePaths,
|
||||
imageShape = imageShape,
|
||||
onImageClick = onImageClick,
|
||||
onCancelTransfer = onCancelTransfer,
|
||||
onLongPress = null,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(text = stringResource(com.bitchat.android.R.string.image_unavailable), fontFamily = BitchatFontFamily, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ImageMessageCard(
|
||||
message: BitchatMessage,
|
||||
currentUserNickname: String,
|
||||
path: String,
|
||||
bmp: android.graphics.Bitmap?,
|
||||
imagePaths: List<String>,
|
||||
imageShape: androidx.compose.foundation.shape.RoundedCornerShape,
|
||||
onImageClick: ((String, List<String>, Int) -> Unit)?,
|
||||
onCancelTransfer: ((BitchatMessage) -> Unit)?,
|
||||
onLongPress: (() -> Unit)?,
|
||||
) {
|
||||
if (bmp == null) {
|
||||
Text(text = stringResource(com.bitchat.android.R.string.image_unavailable), fontFamily = BitchatFontFamily, color = Color.Gray)
|
||||
return
|
||||
}
|
||||
val img = bmp.asImageBitmap()
|
||||
val aspect = (bmp.width.toFloat() / bmp.height.toFloat()).takeIf { it.isFinite() && it > 0 } ?: 1f
|
||||
val progressFraction: Float? = when (val st = message.deliveryStatus) {
|
||||
is com.bitchat.android.model.DeliveryStatus.PartiallyDelivered -> if (st.total > 0) st.reached.toFloat() / st.total.toFloat() else 0f
|
||||
else -> null
|
||||
}
|
||||
Box {
|
||||
val imageModifier = Modifier
|
||||
.widthIn(max = 300.dp)
|
||||
.aspectRatio(aspect)
|
||||
.clip(imageShape)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
val currentIndex = imagePaths.indexOf(path)
|
||||
onImageClick?.invoke(path, imagePaths, currentIndex)
|
||||
},
|
||||
onLongClick = { onLongPress?.invoke() },
|
||||
)
|
||||
if (progressFraction != null && progressFraction < 1f && message.sender == currentUserNickname) {
|
||||
// Cyberpunk block-reveal while sending
|
||||
BlockRevealImage(
|
||||
bitmap = img,
|
||||
progress = progressFraction,
|
||||
blocksX = 24,
|
||||
blocksY = 16,
|
||||
modifier = imageModifier
|
||||
)
|
||||
} else {
|
||||
// Fully revealed image
|
||||
Image(
|
||||
bitmap = img,
|
||||
contentDescription = stringResource(com.bitchat.android.R.string.cd_image),
|
||||
modifier = imageModifier,
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
// Cancel button overlay during sending
|
||||
val showCancel = message.sender == currentUserNickname && (message.deliveryStatus is com.bitchat.android.model.DeliveryStatus.PartiallyDelivered)
|
||||
if (showCancel) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(4.dp)
|
||||
.size(22.dp)
|
||||
.background(Color.Gray.copy(alpha = 0.6f), CircleShape)
|
||||
.clickable { onCancelTransfer?.invoke(message) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(com.bitchat.android.R.string.cd_cancel), tint = Color.White, modifier = Modifier.size(14.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,155 @@
|
||||
package com.bitchat.android.ui.media
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitchat.android.core.ui.component.text.AnnotatedClickableText
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.ui.DeliveryStatusIcon
|
||||
import com.bitchat.android.ui.formatTextMessageMetadata
|
||||
import com.bitchat.android.ui.formatTextMessageSender
|
||||
import com.bitchat.android.ui.isFromSelf
|
||||
import com.bitchat.android.ui.peerIdentityForMessage
|
||||
import com.bitchat.android.ui.theme.BitchatFontFamily
|
||||
import com.bitchat.android.ui.theme.ChatVisualTokens
|
||||
import com.bitchat.android.ui.theme.LocalBitchatPalette
|
||||
import com.bitchat.android.ui.theme.MessageSenderTextStyle
|
||||
import com.bitchat.android.ui.theme.colorForPeer
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
/**
|
||||
* Classic messenger shell for media messages (images, voice notes) in `ChatUiMode.Bubbles`,
|
||||
* mirroring the text-bubble treatment: the author's identity colour washes the fill and
|
||||
* hairline, the speaker-side corner tightens into a tail, the sender's name heads the first
|
||||
* bubble of a run, and the timestamp (plus delivery ticks for own private messages) rides
|
||||
* flush-right beneath the media. The shell itself is long-clickable so media bubbles always
|
||||
* open the message action sheet even when no sender label is shown and the media consumes
|
||||
* touches (voice-note player controls).
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MediaBubbleShell(
|
||||
message: BitchatMessage,
|
||||
currentUserNickname: String,
|
||||
myPeerID: String,
|
||||
showSender: Boolean,
|
||||
timeFormatter: SimpleDateFormat,
|
||||
onNicknameClick: ((String) -> Unit)?,
|
||||
onLongPress: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val palette = LocalBitchatPalette.current
|
||||
val haptic = LocalHapticFeedback.current
|
||||
val isSelf = message.isFromSelf(currentUserNickname, myPeerID)
|
||||
|
||||
val authorColor = remember(message, isSelf, palette) {
|
||||
if (isSelf) palette.accentOrange else colorForPeer(peerIdentityForMessage(message), palette)
|
||||
}
|
||||
|
||||
val corner = ChatVisualTokens.BubbleCornerRadius
|
||||
val tail = ChatVisualTokens.BubbleTailRadius
|
||||
val bubbleShape = if (isSelf) {
|
||||
RoundedCornerShape(topStart = corner, topEnd = corner, bottomEnd = tail, bottomStart = corner)
|
||||
} else {
|
||||
RoundedCornerShape(topStart = corner, topEnd = corner, bottomEnd = corner, bottomStart = tail)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = if (isSelf) Alignment.End else Alignment.Start,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = authorColor.copy(alpha = ChatVisualTokens.BubbleBorderAlpha),
|
||||
shape = bubbleShape
|
||||
)
|
||||
.background(
|
||||
color = authorColor.copy(alpha = ChatVisualTokens.BubbleBackgroundAlpha),
|
||||
shape = bubbleShape
|
||||
)
|
||||
.combinedClickable(
|
||||
enabled = onLongPress != null,
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLongPress?.invoke()
|
||||
},
|
||||
)
|
||||
.padding(
|
||||
horizontal = ChatVisualTokens.BubblePaddingHorizontal,
|
||||
vertical = ChatVisualTokens.BubblePaddingVertical,
|
||||
)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (showSender && !isSelf) {
|
||||
val senderText = remember(message, currentUserNickname, myPeerID, palette) {
|
||||
formatTextMessageSender(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
myPeerID = myPeerID,
|
||||
palette = palette,
|
||||
)
|
||||
}
|
||||
AnnotatedClickableText(
|
||||
text = senderText,
|
||||
annotationTags = listOf("nickname_click"),
|
||||
onAnnotationClick = { tag, item ->
|
||||
if (tag == "nickname_click" && onNicknameClick != null) {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
onNicknameClick.invoke(item)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
onLongPress = { onLongPress?.invoke() },
|
||||
fontFamily = BitchatFontFamily,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MessageSenderTextStyle,
|
||||
)
|
||||
}
|
||||
|
||||
content()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
) {
|
||||
Text(
|
||||
text = formatTextMessageMetadata(message, timeFormatter),
|
||||
fontFamily = BitchatFontFamily,
|
||||
)
|
||||
if (isSelf && message.isPrivate) {
|
||||
message.deliveryStatus?.let { status ->
|
||||
Spacer(Modifier.width(4.dp))
|
||||
DeliveryStatusIcon(status = status)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user