From dd3a77e2f70883b3465aacc3fa509968f937257d Mon Sep 17 00:00:00 2001 From: andrekir Date: Tue, 3 Dec 2024 16:36:28 -0300 Subject: [PATCH] feat: add reaction dialog with grouped emojis and user list --- .../mesh/ui/components/BottomSheetDialog.kt | 52 ++++++++++++++ .../mesh/ui/components/EmojiPicker.kt | 15 +++- .../mesh/ui/map/EditWaypointDialog.kt | 4 +- .../mesh/ui/message/components/MessageList.kt | 9 ++- .../mesh/ui/message/components/Reaction.kt | 71 ++++++++++++++++--- 5 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt new file mode 100644 index 000000000..86aded6c1 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/components/BottomSheetDialog.kt @@ -0,0 +1,52 @@ +package com.geeksville.mesh.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties + +@Composable +fun BottomSheetDialog( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) = Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), +) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Transparent) + .clickable( + onClick = onDismiss, + indication = null, + interactionSource = remember { MutableInteractionSource() } + ) + ) { + Column( + modifier = modifier + .align(Alignment.BottomCenter) + .background( + color = MaterialTheme.colors.surface.copy(alpha = 1f), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + ) + .padding(16.dp), + content = content + ) + } +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/components/EmojiPicker.kt b/app/src/main/java/com/geeksville/mesh/ui/components/EmojiPicker.kt index 87336bfff..0861dc405 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/components/EmojiPicker.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/components/EmojiPicker.kt @@ -56,8 +56,21 @@ fun EmojiPicker( }, modifier = Modifier .fillMaxWidth() - .fillMaxHeight(fraction = 0.4f) .background(MaterialTheme.colors.background) ) } } + +@Composable +fun EmojiPickerDialog( + onDismiss: () -> Unit = {}, + onConfirm: (String) -> Unit +) = BottomSheetDialog( + onDismiss = onDismiss, + modifier = Modifier.fillMaxHeight(fraction = .4f), +) { + EmojiPicker( + onConfirm = onConfirm, + onDismiss = onDismiss, + ) +} diff --git a/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt b/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt index b1a035c3a..95ee1f39d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/map/EditWaypointDialog.kt @@ -60,7 +60,7 @@ import com.geeksville.mesh.MeshProtos.Waypoint import com.geeksville.mesh.R import com.geeksville.mesh.copy import com.geeksville.mesh.ui.components.EditTextPreference -import com.geeksville.mesh.ui.components.EmojiPicker +import com.geeksville.mesh.ui.components.EmojiPickerDialog import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.waypoint @@ -179,7 +179,7 @@ internal fun EditWaypointDialog( } }, ) else { - EmojiPicker(onDismiss = { showEmojiPickerView = false }) { + EmojiPickerDialog(onDismiss = { showEmojiPickerView = false }) { showEmojiPickerView = false waypointInput = waypointInput.copy { icon = it.codePointAt(0) } } 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 63a511c02..42c30588d 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 @@ -34,6 +34,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import com.geeksville.mesh.DataPacket +import com.geeksville.mesh.database.entity.Reaction import com.geeksville.mesh.model.Message import com.geeksville.mesh.ui.components.SimpleAlertDialog import kotlinx.coroutines.FlowPreview @@ -63,6 +64,12 @@ internal fun MessageList( SimpleAlertDialog(title = title, text = text) { showStatusDialog = null } } + var showReactionDialog by remember { mutableStateOf?>(null) } + if (showReactionDialog != null) { + val reactions = showReactionDialog ?: return + ReactionDialog(reactions) { showReactionDialog = null } + } + fun toggle(uuid: Long) = if (selectedIds.value.contains(uuid)) { selectedIds.value -= uuid } else { @@ -79,7 +86,7 @@ internal fun MessageList( val fromLocal = msg.user.id == DataPacket.ID_LOCAL val selected by remember { derivedStateOf { selectedIds.value.contains(msg.uuid) } } - ReactionRow(fromLocal, msg.emojis) {} // TODO + ReactionRow(fromLocal, msg.emojis) { showReactionDialog = msg.emojis } MessageItem( shortName = msg.user.shortName.takeIf { !fromLocal }, messageText = msg.text, 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 60fbc7cfe..53268dff1 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 @@ -23,14 +23,20 @@ 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.Row +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.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 import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme @@ -43,16 +49,18 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +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.window.Dialog import com.geeksville.mesh.MeshProtos import com.geeksville.mesh.database.entity.Reaction -import com.geeksville.mesh.ui.components.EmojiPicker +import com.geeksville.mesh.ui.components.BottomSheetDialog +import com.geeksville.mesh.ui.components.EmojiPickerDialog import com.geeksville.mesh.ui.theme.AppTheme @Composable @@ -159,17 +167,58 @@ fun ReactionRow( fun reduceEmojis(emojis: List): Map = emojis.groupingBy { it }.eachCount() @Composable -fun EmojiPickerDialog( - onConfirm: (String) -> Unit, - onDismiss: () -> Unit = {}, +fun ReactionDialog( + reactions: List, + onDismiss: () -> Unit = {} +) = BottomSheetDialog( + onDismiss = onDismiss, + modifier = Modifier.fillMaxHeight(fraction = .3f), ) { - Dialog( - onDismissRequest = onDismiss, + val groupedEmojis = reactions.groupBy { it.emoji } + var selectedEmoji by remember { mutableStateOf(null) } + val filteredReactions = selectedEmoji?.let { groupedEmojis[it] ?: emptyList() } ?: reactions + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() ) { - EmojiPicker( - onConfirm = onConfirm, - onDismiss = onDismiss, - ) + items(groupedEmojis.entries.toList()) { (emoji, reactions) -> + Text( + text = "$emoji${reactions.size}", + modifier = Modifier + .clip(CircleShape) + .background(if (selectedEmoji == emoji) Color.Gray else Color.Transparent) + .padding(8.dp) + .clickable { + selectedEmoji = if (selectedEmoji == emoji) null else emoji + }, + style = MaterialTheme.typography.body2 + ) + } + } + + Divider(Modifier.padding(vertical = 8.dp)) + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(filteredReactions) { reaction -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = reaction.user.longName, + style = MaterialTheme.typography.subtitle1 + ) + Text( + text = reaction.emoji, + style = MaterialTheme.typography.h6 + ) + } + } } }