mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-02-18 14:48:53 -05:00
wip
This commit is contained in:
@@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
@@ -35,6 +36,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cloud
|
||||
import androidx.compose.material.icons.filled.FormatQuote
|
||||
import androidx.compose.material.icons.rounded.FormatQuote
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardColors
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -56,11 +58,14 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.room.util.newStringBuilder
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.MessageStatus
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import com.geeksville.mesh.model.Message
|
||||
import com.geeksville.mesh.model.Node
|
||||
import com.geeksville.mesh.model.getStringResFrom
|
||||
import com.geeksville.mesh.ui.common.components.EmojiPickerDialog
|
||||
import com.geeksville.mesh.ui.common.components.MDText
|
||||
import com.geeksville.mesh.ui.common.preview.NodePreviewParameterProvider
|
||||
@@ -159,7 +164,7 @@ internal fun MessageItem(
|
||||
onLongClick = { showMessageActionsDialog = true },
|
||||
onClick = {},
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
.padding(start = 8.dp, end = 8.dp, bottom = 16.dp),
|
||||
) {
|
||||
@Suppress("MagicNumber")
|
||||
Column(
|
||||
@@ -168,68 +173,85 @@ internal fun MessageItem(
|
||||
.align(if (message.fromLocal) Alignment.CenterEnd else Alignment.CenterStart),
|
||||
horizontalAlignment = if (message.fromLocal) Alignment.End else Alignment.Start,
|
||||
) {
|
||||
Card(
|
||||
modifier =
|
||||
Modifier.wrapContentSize()
|
||||
.then(
|
||||
if (containsBel) {
|
||||
Modifier.border(2.dp, MessageItemColors.Red, shape = MaterialTheme.shapes.medium)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = cardColors,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
OriginalMessageSnippet(
|
||||
message = message,
|
||||
ourNode = ourNode,
|
||||
cardColors = cardColors,
|
||||
onNavigateToOriginalMessage = onNavigateToOriginalMessage,
|
||||
)
|
||||
|
||||
Column {
|
||||
MDText(
|
||||
text = message.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = cardColors.contentColor,
|
||||
Box {
|
||||
Card(
|
||||
modifier =
|
||||
Modifier.wrapContentSize()
|
||||
.then(
|
||||
if (containsBel) {
|
||||
Modifier.border(
|
||||
2.dp,
|
||||
MessageItemColors.Red,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.align(if (message.fromLocal) Alignment.CenterEnd else Alignment.CenterStart),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = cardColors,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||
OriginalMessageSnippet(
|
||||
message = message,
|
||||
ourNode = ourNode,
|
||||
cardColors = cardColors,
|
||||
onNavigateToOriginalMessage = onNavigateToOriginalMessage,
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 4.dp).align(Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
if (message.viaMqtt) {
|
||||
Icon(
|
||||
Icons.Default.Cloud,
|
||||
contentDescription = stringResource(R.string.via_mqtt),
|
||||
modifier = Modifier.size(12.dp),
|
||||
MDText(
|
||||
text = message.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = cardColors.contentColor,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 4.dp).align(Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
if (message.viaMqtt) {
|
||||
Icon(
|
||||
Icons.Default.Cloud,
|
||||
contentDescription = stringResource(R.string.via_mqtt),
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReactionRow(
|
||||
reactions = emojis,
|
||||
onShowReactions = onShowReactions,
|
||||
modifier =
|
||||
Modifier.align(if (message.fromLocal) Alignment.TopStart else Alignment.TopEnd)
|
||||
.offset(y = (-14).dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (containsBel) {
|
||||
Text(text = "\uD83D\uDD14", modifier = Modifier.padding(end = 4.dp))
|
||||
val bottomLabel = buildString {
|
||||
if (containsBel) {
|
||||
append("\uD83D\uDD14")
|
||||
append(" · ")
|
||||
}
|
||||
|
||||
if(message.fromLocal) {
|
||||
append(stringResource(message.getStatusStringRes().second))
|
||||
append(" · ")
|
||||
}
|
||||
|
||||
append(message.time)
|
||||
}
|
||||
Text(text = message.time, style = MaterialTheme.typography.labelSmall)
|
||||
|
||||
Text(text = bottomLabel, style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReactionRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
reactions = emojis,
|
||||
onSendReaction = sendReaction,
|
||||
onShowReactions = onShowReactions,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -255,7 +277,7 @@ private fun OriginalMessageSnippet(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.FormatQuote,
|
||||
Icons.Rounded.FormatQuote,
|
||||
contentDescription = stringResource(R.string.reply), // Add to strings.xml
|
||||
)
|
||||
Text(
|
||||
@@ -314,9 +336,23 @@ private fun MessageItemPreview() {
|
||||
read = false,
|
||||
routingError = 0,
|
||||
packetId = 4545,
|
||||
emojis = listOf(),
|
||||
emojis =
|
||||
listOf(
|
||||
Reaction(
|
||||
replyId = 1,
|
||||
user = MeshProtos.User.getDefaultInstance(),
|
||||
emoji = "\uD83D\uDE42",
|
||||
timestamp = 1L,
|
||||
),
|
||||
Reaction(
|
||||
replyId = 1,
|
||||
user = MeshProtos.User.getDefaultInstance(),
|
||||
emoji = "\uD83E\uDEE0",
|
||||
timestamp = 1L,
|
||||
),
|
||||
),
|
||||
replyId = null,
|
||||
viaMqtt = true,
|
||||
viaMqtt = false,
|
||||
)
|
||||
val receivedWithOriginalMessage =
|
||||
Message(
|
||||
|
||||
@@ -17,26 +17,25 @@
|
||||
|
||||
package com.geeksville.mesh.ui.message.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
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.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -47,64 +46,75 @@ 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.layout.Layout
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.database.entity.Reaction
|
||||
import com.geeksville.mesh.ui.common.components.BottomSheetDialog
|
||||
import com.geeksville.mesh.ui.common.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
private fun ReactionItem(emoji: String, emojiCount: Int = 1, onClick: () -> Unit = {}, onLongClick: () -> Unit = {}) {
|
||||
BadgedBox(
|
||||
badge = {
|
||||
if (emojiCount > 1) {
|
||||
Badge { Text(fontWeight = FontWeight.Bold, text = emojiCount.toString()) }
|
||||
}
|
||||
},
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.combinedClickable(onClick = onClick, onLongClick = onLongClick),
|
||||
color = MaterialTheme.colorScheme.primaryContainer,
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Text(text = emoji, modifier = Modifier.padding(4.dp).clip(CircleShape))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ReactionRow(
|
||||
modifier: Modifier = Modifier,
|
||||
reactions: List<Reaction> = emptyList(),
|
||||
onSendReaction: (String) -> Unit = {},
|
||||
onShowReactions: () -> Unit = {},
|
||||
) {
|
||||
val emojiList = reduceEmojis(reactions.reversed().map { it.emoji }).entries
|
||||
val emojiList = reactions.map { it.emoji }.distinct()
|
||||
|
||||
AnimatedVisibility(emojiList.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = modifier.padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Box(modifier = modifier) {
|
||||
EmojiStack(
|
||||
offset = 12.dp,
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
interactionSource = null,
|
||||
indication = null,
|
||||
onLongClick = onShowReactions,
|
||||
onClick = {},
|
||||
),
|
||||
) {
|
||||
items(emojiList.size) { index ->
|
||||
val entry = emojiList.elementAt(index)
|
||||
ReactionItem(
|
||||
emoji = entry.key,
|
||||
emojiCount = entry.value,
|
||||
onClick = { onSendReaction(entry.key) },
|
||||
onLongClick = onShowReactions,
|
||||
)
|
||||
emojiList.forEach { emoji ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier.size(24.dp)
|
||||
.aspectRatio(1f)
|
||||
.background(color = MaterialTheme.colorScheme.primaryContainer, shape = CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = emoji,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.wrapContentSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reduceEmojis(emojis: List<String>): Map<String, Int> = emojis.groupingBy { it }.eachCount()
|
||||
@Composable
|
||||
fun EmojiStack(modifier: Modifier = Modifier, offset: Dp, content: @Composable () -> Unit) {
|
||||
Layout(content, modifier) { measurables, constraints ->
|
||||
val placeables = measurables.map { measurable -> measurable.measure(constraints) }
|
||||
|
||||
val height = if (placeables.isNotEmpty()) placeables.first().height else 0
|
||||
|
||||
val width =
|
||||
if (placeables.isNotEmpty()) {
|
||||
placeables.first().width + (offset.toPx().toInt() * (placeables.size - 1))
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
layout(width = width, height = height) {
|
||||
placeables.mapIndexed { index, placeable -> placeable.place(x = offset.toPx().toInt() * index, y = 0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReactionDialog(reactions: List<Reaction>, onDismiss: () -> Unit = {}) =
|
||||
@@ -143,17 +153,6 @@ fun ReactionDialog(reactions: List<Reaction>, onDismiss: () -> Unit = {}) =
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun ReactionItemPreview() {
|
||||
AppTheme {
|
||||
Column(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
|
||||
ReactionItem(emoji = "\uD83D\uDE42")
|
||||
ReactionItem(emoji = "\uD83D\uDE42", emojiCount = 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ReactionRowPreview() {
|
||||
@@ -173,6 +172,18 @@ fun ReactionRowPreview() {
|
||||
emoji = "\uD83D\uDE42",
|
||||
timestamp = 1L,
|
||||
),
|
||||
Reaction(
|
||||
replyId = 1,
|
||||
user = MeshProtos.User.getDefaultInstance(),
|
||||
emoji = "\uD83E\uDEE0",
|
||||
timestamp = 1L,
|
||||
),
|
||||
Reaction(
|
||||
replyId = 1,
|
||||
user = MeshProtos.User.getDefaultInstance(),
|
||||
emoji = "\uD83D\uDD12",
|
||||
timestamp = 1L,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user