|
|
|
|
@ -101,16 +101,31 @@ private fun detectEmojiOnly(text: String): Int {
|
|
|
|
|
private fun deliveryLabel(message: Message): String = |
|
|
|
|
if (message.deliveryStatusRes == 0) "" else stringResource(message.deliveryStatusRes) |
|
|
|
|
|
|
|
|
|
/* Footer получает ширину пузыря после измерения содержимого, включая медиа. */ |
|
|
|
|
/* Одна нижняя строка внутри пузыря: реакции слева, время справа. Ширина учитывает оба блока. */ |
|
|
|
|
@Composable |
|
|
|
|
internal fun MessageWithFooter(footer: @Composable () -> Unit, content: @Composable () -> Unit) { |
|
|
|
|
Layout(content = { Box { content() }; Box { footer() } }) { children, constraints -> |
|
|
|
|
internal fun MessageWithFooter(message: Message, footer: @Composable () -> Unit, |
|
|
|
|
modifier: Modifier = Modifier, content: @Composable () -> Unit) { |
|
|
|
|
Layout(modifier = modifier, content = { |
|
|
|
|
Box { content() } |
|
|
|
|
Box { footer() } |
|
|
|
|
Text(listOf(formatTime(message.ts), deliveryLabel(message)).filter { it.isNotEmpty() }.joinToString(" · "), |
|
|
|
|
fontSize = 11.sp, color = ChatColors.Secondary, maxLines = 1, overflow = TextOverflow.Ellipsis) |
|
|
|
|
}) { children, constraints -> |
|
|
|
|
val childConstraints = constraints.copy(minWidth = 0, minHeight = 0) |
|
|
|
|
val body = children[0].measure(childConstraints) |
|
|
|
|
val bottom = children[1].measure(childConstraints.copy(minWidth = body.width, maxWidth = body.width)) |
|
|
|
|
layout(body.width, body.height + bottom.height) { |
|
|
|
|
val preferred = children[1].maxIntrinsicWidth(Int.MAX_VALUE).coerceAtMost(constraints.maxWidth) |
|
|
|
|
val gap = if (preferred > 0) 6.dp.roundToPx() else 0 |
|
|
|
|
val time = children[2].measure(childConstraints.copy( |
|
|
|
|
maxWidth = (constraints.maxWidth - gap - if (preferred > 0) 32.dp.roundToPx() else 0).coerceAtLeast(0))) |
|
|
|
|
val width = maxOf(constraints.minWidth, body.width, minOf(constraints.maxWidth, preferred + gap + time.width)) |
|
|
|
|
val reactions = children[1].measure(childConstraints.copy(maxWidth = (width - time.width - gap).coerceAtLeast(0))) |
|
|
|
|
val rowHeight = maxOf(reactions.height, time.height) |
|
|
|
|
val lastLineHeight = maxOf(time.height, minOf(reactions.height, 32.dp.roundToPx())) |
|
|
|
|
val top = body.height + 4.dp.roundToPx() |
|
|
|
|
layout(width, top + rowHeight) { |
|
|
|
|
body.placeRelative(0, 0) |
|
|
|
|
bottom.placeRelative(0, body.height) |
|
|
|
|
reactions.placeRelative(0, top) |
|
|
|
|
time.placeRelative(width - time.width, top + rowHeight - lastLineHeight + (lastLineHeight - time.height) / 2) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -157,8 +172,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
|
|
|
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp), |
|
|
|
|
horizontalArrangement = alignment |
|
|
|
|
) { |
|
|
|
|
MessageWithFooter(footer) { |
|
|
|
|
Column( |
|
|
|
|
MessageWithFooter(message, footer, |
|
|
|
|
modifier = Modifier |
|
|
|
|
.widthIn(min = minBubbleWidth, max = if (isVoice) voiceMaxWidth else 300.dp) |
|
|
|
|
// Тень рисуем до clip, чтобы выступающие справа и снизу края оставались видны. |
|
|
|
|
@ -172,6 +186,7 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
|
|
|
|
.background(bgColor) |
|
|
|
|
.padding(12.dp) |
|
|
|
|
) { |
|
|
|
|
Column { |
|
|
|
|
if (!isVoice) Text(if (message.isOutgoing) stringResource(R.string.you) else message.author, |
|
|
|
|
fontSize = 13.sp, fontWeight = FontWeight.Bold, color = ChatColors.Secondary) |
|
|
|
|
Spacer(Modifier.height(2.dp)) |
|
|
|
|
@ -203,15 +218,6 @@ fun MessageBubble(message: Message, playbackState: ChatViewModel.PlaybackState =
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!isVoice) { |
|
|
|
|
Spacer(Modifier.height(2.dp)) |
|
|
|
|
Text( |
|
|
|
|
listOf(formatTime(message.ts), deliveryLabel(message)).filter { it.isNotEmpty() }.joinToString(" · "), |
|
|
|
|
fontSize = 11.sp, |
|
|
|
|
color = ChatColors.Secondary, |
|
|
|
|
modifier = Modifier.align(Alignment.End) |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
@ -314,12 +320,6 @@ private fun VoiceBubble(message: Message, textColor: Color, isActive: Boolean,
|
|
|
|
|
fontSize = 12.sp, |
|
|
|
|
color = ChatColors.Secondary |
|
|
|
|
) |
|
|
|
|
Text( |
|
|
|
|
listOf(formatTime(message.ts), deliveryLabel(message)).filter { it.isNotEmpty() }.joinToString(" · "), |
|
|
|
|
fontSize = 11.sp, |
|
|
|
|
color = ChatColors.Secondary, |
|
|
|
|
modifier = Modifier.align(Alignment.CenterVertically).padding(start = 8.dp) |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|