Move Android reactions inside bubbles beside the timestamp
This commit is contained in:
1 parent
2cf0f9c928
commit
1f4266f8b1
2 files changed
+35
-32
No files matched your search
+23
-23
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ fun PollMessageBubble(message: Message, onVote: (Message, List<Int>, String) ->
|
||||
val poll = message.feedback?.poll
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = if (message.isOutgoing) Arrangement.End else Arrangement.Start) {
|
||||
MessageWithFooter(footer) {
|
||||
Card(onClick = { if (poll != null) showVote = true }, modifier = Modifier.widthIn(max = 300.dp),
|
||||
Card(onClick = { if (poll != null) showVote = true }, modifier = Modifier.widthIn(max = 300.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = if (message.isOutgoing) MaterialTheme.colorScheme.primaryContainer
|
||||
else MaterialTheme.colorScheme.surfaceVariant)) {
|
||||
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
MessageWithFooter(message, footer, Modifier.padding(12.dp)) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
if (!message.isOutgoing) Text(message.author, style = MaterialTheme.typography.labelMedium)
|
||||
if (poll == null) Text(stringResource(R.string.poll_pending))
|
||||
else {
|
||||
@@ -82,8 +82,8 @@ fun PollMessageBubble(message: Message, onVote: (Message, List<Int>, String) ->
|
||||
TextButton(onClick = { showVote = true }) { Text(stringResource(R.string.poll_vote_results)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showVote && poll != null) PollVoteDialog(message, poll, { showVote = false }, onVote, authorName)
|
||||
}
|
||||
@@ -134,25 +134,28 @@ private fun PollVoteDialog(message: Message, poll: PollState, onDismiss: () -> U
|
||||
} })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MessageReactions(message: Message, catalog: List<ReactionState>, onReact: (Message, Int?) -> Unit) {
|
||||
var picker by rememberSaveable(message.messageId) { mutableStateOf(false) }
|
||||
FlowRow(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
||||
FlowRow(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
IconButton(onClick = {
|
||||
LogManager.addLog("DEBUG", "POLL", "reaction picker opened message=${message.messageId} type=${message.contentType}")
|
||||
picker = true
|
||||
}) {
|
||||
}, modifier = Modifier.size(32.dp)) {
|
||||
ReactionHeart(stringResource(R.string.poll_react))
|
||||
}
|
||||
for (reaction in message.feedback?.reactions.orEmpty()) {
|
||||
FilterChip(selected = reaction.mine, onClick = {
|
||||
FilterChip(selected = reaction.mine, modifier = Modifier.height(32.dp), onClick = {
|
||||
LogManager.addLog("DEBUG", "POLL", "reaction toggled message=${message.messageId} emoji=${reaction.id} mine=${reaction.mine}")
|
||||
onReact(message, reaction.id)
|
||||
},
|
||||
label = { Text("${reaction.emoji} ${reaction.count}") })
|
||||
}
|
||||
}
|
||||
}
|
||||
if (picker) AlertDialog(onDismissRequest = { picker = false }, title = { Text(stringResource(R.string.poll_react)) },
|
||||
text = { FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
for (reaction in catalog) TextButton(onClick = { onReact(message, reaction.id); picker = false }) { Text(reaction.emoji) }
|
||||
@@ -176,7 +179,7 @@ private fun ReactionHeart(description: String) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
Canvas(Modifier.size(24.dp).semantics { contentDescription = description }) {
|
||||
Canvas(Modifier.size(20.dp).semantics { contentDescription = description }) {
|
||||
val factor = size.width / 24f
|
||||
scale(factor, factor, pivot = Offset.Zero) {
|
||||
drawPath(path, Color.Black.copy(alpha = 0.6f), style = Stroke(width = 1.8f + 2f / factor, join = StrokeJoin.Round))
|
||||
|
||||
Reference in new issue
Block a user