diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt index df8726e13..ce855f116 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageList.kt @@ -20,6 +20,7 @@ package com.geeksville.mesh.ui.message.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -37,7 +38,10 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.dp import com.geeksville.mesh.DataPacket import com.geeksville.mesh.database.entity.Reaction import com.geeksville.mesh.model.Message @@ -48,7 +52,7 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce -@Suppress("LongMethod") +@Suppress("LongMethod", "MagicNumber") @Composable internal fun MessageList( messages: List, @@ -94,11 +98,12 @@ internal fun MessageList( items(messages, key = { it.uuid }) { msg -> val fromLocal = msg.node.user.id == DataPacket.ID_LOCAL val selected by remember { derivedStateOf { selectedIds.value.contains(msg.uuid) } } - - ReactionRow(fromLocal, msg.emojis) { showReactionDialog = msg.emojis } - Box(Modifier.wrapContentSize(Alignment.TopStart)) { + var messageItemHeight by remember { mutableStateOf(0) } + val density = LocalDensity.current + Box(Modifier.wrapContentSize(Alignment.TopStart).padding(vertical = 8.dp)) { var expandedNodeMenu by remember { mutableStateOf(false) } MessageItem( + modifier = Modifier.onGloballyPositioned { messageItemHeight = it.size.height }, node = msg.node, messageText = msg.text, messageTime = msg.time, @@ -124,6 +129,13 @@ internal fun MessageList( expanded = expandedNodeMenu, onAction = onNodeMenuAction ) + ReactionRow( + modifier = Modifier.padding( + top = with(density) { messageItemHeight.toDp() } + 4.dp + ), + fromLocal = fromLocal, + reactions = msg.emojis + ) { showReactionDialog = msg.emojis } } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt index b2fcb5963..1db68cf7b 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/Reaction.kt @@ -17,24 +17,30 @@ package com.geeksville.mesh.ui.message.components +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.FlowRowOverflow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Badge -import androidx.compose.material.BadgedBox import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider import androidx.compose.material.Icon @@ -43,7 +49,7 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.EmojiEmotions +import androidx.compose.material.icons.filled.AddReaction import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -53,10 +59,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.database.entity.Reaction import com.geeksville.mesh.ui.components.BottomSheetDialog @@ -80,7 +86,7 @@ fun ReactionButton( } IconButton(onClick = { showEmojiPickerDialog = true }) { Icon( - imageVector = Icons.Default.EmojiEmotions, + imageVector = Icons.Default.AddReaction, contentDescription = "emoji", modifier = modifier.size(16.dp), tint = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium), @@ -94,35 +100,51 @@ private fun ReactionItem( emojiCount: Int = 1, onClick: () -> Unit = {}, ) { - BadgedBox( - modifier = Modifier.padding(start = 2.dp, top = 8.dp, end = 2.dp, bottom = 4.dp), - badge = { - if (emojiCount > 1) { - Badge( - backgroundColor = MaterialTheme.colors.onBackground, - contentColor = MaterialTheme.colors.background, - ) { - Text( - fontWeight = FontWeight.Bold, - text = emojiCount.toString() - ) - } - } - } + + Surface( + modifier = Modifier + .padding(2.dp) + .border( + 1.dp, + MaterialTheme.colors.secondaryVariant.copy(ContentAlpha.medium), + RoundedCornerShape(8.dp) + ) + .clickable { onClick() }, + color = MaterialTheme.colors.surface.copy(alpha = ContentAlpha.medium), + contentColor = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium), + shape = RoundedCornerShape(8.dp), + elevation = 4.dp, ) { - Surface( + Row( modifier = Modifier - .clickable { onClick() }, - color = MaterialTheme.colors.surface, - shape = RoundedCornerShape(32.dp), - elevation = 4.dp, + .background(MaterialTheme.colors.surface) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically ) { Text( - text = emoji, - modifier = Modifier - .padding(8.dp) - .clip(CircleShape), + style = MaterialTheme.typography.h6, + text = emoji ) + if (emojiCount > 0) { + Spacer( + modifier = Modifier.width(2.dp) + ) + AnimatedContent( + targetState = emojiCount, + transitionSpec = { + if (targetState > initialState) { + slideInVertically { -it } togetherWith slideOutVertically { it } + } else { + slideInVertically { it } togetherWith slideOutVertically { -it } + } + } + ) { + Text( + text = "$it", + style = MaterialTheme.typography.body2, + ) + } + } } } } @@ -130,6 +152,7 @@ private fun ReactionItem( @OptIn(ExperimentalLayoutApi::class) @Composable fun ReactionRow( + modifier: Modifier = Modifier, fromLocal: Boolean, reactions: List = emptyList(), onSendReaction: (String) -> Unit = {} @@ -146,11 +169,21 @@ fun ReactionRow( ) } + var maxLines by remember { mutableStateOf(1) } FlowRow( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(horizontal = 16.dp), - horizontalArrangement = if (fromLocal) Arrangement.End else Arrangement.Start + horizontalArrangement = if (fromLocal) Arrangement.End else Arrangement.Start, + maxLines = maxLines, + overflow = FlowRowOverflow.expandIndicator { + ReactionItem( + emoji = "...", + emojiCount = 0 + ) { + maxLines += 1 + } + } ) { emojiList.forEach { entry -> ReactionItem( @@ -164,6 +197,23 @@ fun ReactionRow( } } +@Composable +internal fun Ellipsis(text: String, onClick: () -> Unit) { + Surface( + color = MaterialTheme.colors.surface, + contentColor = MaterialTheme.colors.onSurface, + modifier = Modifier + .clickable(onClick = onClick) + ) { + Text( + modifier = Modifier + .padding(3.dp), + text = text, + fontSize = 18.sp + ) + } +} + fun reduceEmojis(emojis: List): Map = emojis.groupingBy { it }.eachCount() @Composable @@ -231,6 +281,7 @@ fun ReactionItemPreview() { ) { ReactionItem(emoji = "\uD83D\uDE42") ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2) + ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 222) ReactionButton() } }