From 8ccb9d615e39928229c7e9cc2c4f9b1a535a9e5c Mon Sep 17 00:00:00 2001 From: jamesarich Date: Fri, 13 Dec 2024 08:44:04 -0600 Subject: [PATCH 1/4] Refactor: Improve Reaction UI in MessageList- Moves the ReactionRow below the MessageItem within a Column, providing better visual separation. - Updates ReactionItem UI to a more modern style with borders and rounded corners. - Implements an ellipsis indicator for overflowing reactions, allowing users to expand and view more. - Changes the reaction button to a plus icon, indicating the ability to add a reaction. --- .../mesh/ui/message/components/MessageList.kt | 69 ++++++----- .../mesh/ui/message/components/Reaction.kt | 110 +++++++++++++----- 2 files changed, 119 insertions(+), 60 deletions(-) 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 21ad8525a..5ac539f30 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 @@ -17,9 +17,12 @@ package com.geeksville.mesh.ui.message.components +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column 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 @@ -38,6 +41,7 @@ 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.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, @@ -95,35 +99,40 @@ internal fun MessageList( 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 expandedNodeMenu by remember { mutableStateOf(false) } - MessageItem( - node = msg.node, - messageText = msg.text, - messageTime = msg.time, - messageStatus = msg.status, - selected = selected, - onClick = { if (inSelectionMode) selectedIds.toggle(msg.uuid) }, - onLongClick = { - selectedIds.toggle(msg.uuid) - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - }, - onChipClick = { - if (msg.node.num != 0) { - expandedNodeMenu = true - } - }, - onStatusClick = { showStatusDialog = msg }, - onSendReaction = { onSendReaction(it, msg.packetId) }, - ) - NodeMenu( - node = msg.node, - showFullMenu = true, - onDismissRequest = { expandedNodeMenu = false }, - expanded = expandedNodeMenu, - onAction = onNodeMenuAction - ) + Column( + modifier = Modifier.padding(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy((-12).dp) + ) { + Box(Modifier.wrapContentSize(Alignment.TopStart)) { + var expandedNodeMenu by remember { mutableStateOf(false) } + MessageItem( + node = msg.node, + messageText = msg.text, + messageTime = msg.time, + messageStatus = msg.status, + selected = selected, + onClick = { if (inSelectionMode) selectedIds.toggle(msg.uuid) }, + onLongClick = { + selectedIds.toggle(msg.uuid) + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onChipClick = { + if (msg.node.num != 0) { + expandedNodeMenu = true + } + }, + onStatusClick = { showStatusDialog = msg }, + onSendReaction = { onSendReaction(it, msg.packetId) }, + ) + NodeMenu( + node = msg.node, + showFullMenu = true, + onDismissRequest = { expandedNodeMenu = false }, + expanded = expandedNodeMenu, + onAction = onNodeMenuAction + ) + } + ReactionRow(fromLocal, 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 53268dff1..e0fd2b430 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.onSurface.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, + ) + } + } } } } @@ -146,11 +168,21 @@ fun ReactionRow( ) } + var maxLines by remember { mutableStateOf(1) } FlowRow( 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 +196,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 +280,7 @@ fun ReactionItemPreview() { ) { ReactionItem(emoji = "\uD83D\uDE42") ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2) + ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 222) ReactionButton() } } From fe44aed3e61b88fc70b935d8ae71313df7c76f84 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:41:33 -0600 Subject: [PATCH 2/4] Refactor: Update reaction button border color Changed the reaction button border color from `onSurface` to `secondaryVariant` to improve visual clarity. --- .../java/com/geeksville/mesh/ui/message/components/Reaction.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e0fd2b430..e92828cda 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 @@ -106,7 +106,7 @@ private fun ReactionItem( .padding(2.dp) .border( 1.dp, - MaterialTheme.colors.onSurface.copy(ContentAlpha.medium), + MaterialTheme.colors.secondaryVariant.copy(ContentAlpha.medium), RoundedCornerShape(8.dp) ) .clickable { onClick() }, From da5248b18387ce50b908543e6629ce184a8db9c0 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Fri, 3 Jan 2025 07:25:18 -0600 Subject: [PATCH 3/4] Refactor MessageList and ReactionRow composables - Adds modifier parameter to ReactionRow for styling flexibility. - Removes Column and adjusts layout in MessageList for better message spacing and reaction positioning. - Adds zIndex and offset to reaction row for improved visual layering. --- .../mesh/ui/message/components/MessageList.kt | 72 +++++++++---------- .../mesh/ui/message/components/Reaction.kt | 3 +- 2 files changed, 37 insertions(+), 38 deletions(-) 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 5ac539f30..8fd74c787 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 @@ -17,12 +17,10 @@ package com.geeksville.mesh.ui.message.components -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -42,6 +40,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import com.geeksville.mesh.DataPacket import com.geeksville.mesh.database.entity.Reaction import com.geeksville.mesh.model.Message @@ -99,41 +98,40 @@ internal fun MessageList( val fromLocal = msg.node.user.id == DataPacket.ID_LOCAL val selected by remember { derivedStateOf { selectedIds.value.contains(msg.uuid) } } - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy((-12).dp) - ) { - Box(Modifier.wrapContentSize(Alignment.TopStart)) { - var expandedNodeMenu by remember { mutableStateOf(false) } - MessageItem( - node = msg.node, - messageText = msg.text, - messageTime = msg.time, - messageStatus = msg.status, - selected = selected, - onClick = { if (inSelectionMode) selectedIds.toggle(msg.uuid) }, - onLongClick = { - selectedIds.toggle(msg.uuid) - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - }, - onChipClick = { - if (msg.node.num != 0) { - expandedNodeMenu = true - } - }, - onStatusClick = { showStatusDialog = msg }, - onSendReaction = { onSendReaction(it, msg.packetId) }, - ) - NodeMenu( - node = msg.node, - showFullMenu = true, - onDismissRequest = { expandedNodeMenu = false }, - expanded = expandedNodeMenu, - onAction = onNodeMenuAction - ) - } - ReactionRow(fromLocal, msg.emojis) { showReactionDialog = msg.emojis } + Box(Modifier.wrapContentSize(Alignment.TopStart)) { + var expandedNodeMenu by remember { mutableStateOf(false) } + MessageItem( + node = msg.node, + messageText = msg.text, + messageTime = msg.time, + messageStatus = msg.status, + selected = selected, + onClick = { if (inSelectionMode) selectedIds.toggle(msg.uuid) }, + onLongClick = { + selectedIds.toggle(msg.uuid) + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onChipClick = { + if (msg.node.num != 0) { + expandedNodeMenu = true + } + }, + onStatusClick = { showStatusDialog = msg }, + onSendReaction = { onSendReaction(it, msg.packetId) }, + ) + NodeMenu( + node = msg.node, + showFullMenu = true, + onDismissRequest = { expandedNodeMenu = false }, + expanded = expandedNodeMenu, + onAction = onNodeMenuAction + ) } + ReactionRow( + modifier = Modifier.zIndex(1F).offset(y = (-8).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 e92828cda..39fc4651a 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 @@ -152,6 +152,7 @@ private fun ReactionItem( @OptIn(ExperimentalLayoutApi::class) @Composable fun ReactionRow( + modifier: Modifier = Modifier, fromLocal: Boolean, reactions: List = emptyList(), onSendReaction: (String) -> Unit = {} @@ -170,7 +171,7 @@ 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, From 875e94bb2530481e6c2e867cae1910688e409510 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Sat, 4 Jan 2025 10:58:32 -0600 Subject: [PATCH 4/4] refactor: use calculated message height as padding --- .../mesh/ui/message/components/MessageList.kt | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) 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 f5af33fa0..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,7 +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.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -38,9 +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 androidx.compose.ui.zIndex import com.geeksville.mesh.DataPacket import com.geeksville.mesh.database.entity.Reaction import com.geeksville.mesh.model.Message @@ -97,10 +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) } } - - 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, @@ -126,12 +129,14 @@ 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 } } - ReactionRow( - modifier = Modifier.zIndex(1F).offset(y = (-8).dp), - fromLocal = fromLocal, - reactions = msg.emojis - ) { showReactionDialog = msg.emojis } } } }