ui: improve Compose stability and message UI
- Add minute/second abbreviations and kotlin.time-based relative time formatter - Introduce FastPreview and update previews to use AppTheme with dark/dynamic colors - Refactor attachments preview grid & waveform to use ImmutableList and reduce recompositions - Tweak message bubble reply styling and swipe-to-reply animation/haptics - Add Compose Stability Analyzer plugin and enable it in debug builds - Cache shared images by sha256 and improve share intent/chooser text - Minor UX polish (e.g., “No views”) and immutability annotations
This commit is contained in:
+41
-25
@@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory.presentation
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
@@ -13,6 +14,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
@@ -30,7 +32,6 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.domain.util.annotated
|
||||
import dev.meloda.fast.messageshistory.presentation.attachments.Attachments
|
||||
@@ -38,14 +39,12 @@ import dev.meloda.fast.messageshistory.presentation.attachments.Reply
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkStickerDomain
|
||||
import dev.meloda.fast.model.api.domain.VkVideoMessageDomain
|
||||
import dev.meloda.fast.ui.common.FastPreview
|
||||
import dev.meloda.fast.ui.model.vk.SendingStatus
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.darken
|
||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||
import dev.meloda.fast.ui.util.isDark
|
||||
import dev.meloda.fast.ui.util.lighten
|
||||
|
||||
@Composable
|
||||
fun MessageBubble(
|
||||
@@ -120,20 +119,22 @@ fun MessageBubble(
|
||||
if (replyTitle != null) {
|
||||
Reply(
|
||||
modifier = Modifier
|
||||
.padding(if (attachments == null || text != null) 0.dp else 4.dp)
|
||||
.padding(if (attachments == null || text != null) 0.dp else 0.dp)
|
||||
.width(with(density) { containerWidth.toDp() }),
|
||||
bottomPadding = if (attachments == null || text != null) 0.dp else 4.dp,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 16.dp,
|
||||
topEnd = 16.dp,
|
||||
bottomStart = if (attachments == null || text != null) 0.dp else 16.dp,
|
||||
bottomEnd = if (attachments == null || text != null) 0.dp else 16.dp
|
||||
bottomStart = if (attachments == null || text != null) 0.dp else 0.dp,
|
||||
bottomEnd = if (attachments == null || text != null) 0.dp else 0.dp
|
||||
),
|
||||
onClick = onReplyClick,
|
||||
title = replyTitle,
|
||||
summary = replySummary,
|
||||
backgroundColor = colors.container,
|
||||
innerBackgroundColor = colors.replyContainer
|
||||
backgroundColor = colors.replyContainer,
|
||||
innerBackgroundColor = colors.replyInnerContainer,
|
||||
titleColor = colors.replyTitle,
|
||||
textColor = colors.replyText
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,7 +160,7 @@ fun MessageBubble(
|
||||
.padding(
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = if (replyTitle != null) 0.dp else 6.dp,
|
||||
top = if (replyTitle != null) 4.dp else 6.dp,
|
||||
bottom = if (replyTitle != null) 4.dp else 6.dp
|
||||
)
|
||||
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
||||
@@ -211,17 +212,25 @@ fun MessageBubble(
|
||||
attachmentsContainerWidth = it.size.width
|
||||
}
|
||||
.clip(
|
||||
if (!shouldShowBubble) RoundedCornerShape(24.dp)
|
||||
else RoundedCornerShape(
|
||||
if (!shouldShowBubble) {
|
||||
RoundedCornerShape(
|
||||
topStart = if (replyTitle != null) 0.dp else 24.dp,
|
||||
topEnd = if (replyTitle != null) 0.dp else 24.dp,
|
||||
bottomEnd = 24.dp,
|
||||
bottomStart = 24.dp,
|
||||
)
|
||||
} else RoundedCornerShape(
|
||||
topStart = 0.dp,
|
||||
topEnd = 0.dp,
|
||||
bottomEnd = 24.dp,
|
||||
bottomStart = 24.dp,
|
||||
topStart = 0.dp,
|
||||
topEnd = 0.dp
|
||||
)
|
||||
)
|
||||
.background(attachmentBackgroundColor)
|
||||
) {
|
||||
Attachments(
|
||||
withText = text != null,
|
||||
withReply = replyTitle != null,
|
||||
modifier = Modifier,
|
||||
attachments = attachments,
|
||||
onClick = currentOnClick,
|
||||
@@ -261,6 +270,9 @@ private data class MessageBubbleColors(
|
||||
val container: Color,
|
||||
val content: Color,
|
||||
val replyContainer: Color,
|
||||
val replyInnerContainer: Color,
|
||||
val replyTitle: Color,
|
||||
val replyText: Color
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -268,31 +280,35 @@ private fun messageBubbleColors(isOut: Boolean): MessageBubbleColors {
|
||||
return if (isOut) {
|
||||
val containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
|
||||
val replyContainerColor = if (containerColor.isDark()) {
|
||||
containerColor.lighten(0.15f)
|
||||
} else {
|
||||
containerColor.darken(0.075f)
|
||||
}
|
||||
|
||||
MessageBubbleColors(
|
||||
container = containerColor,
|
||||
content = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
replyContainer = replyContainerColor
|
||||
replyContainer = containerColor,
|
||||
replyInnerContainer = MaterialTheme.colorScheme.background.copy(
|
||||
if (isSystemInDarkTheme()) 0.3f else 0.45f
|
||||
),
|
||||
replyTitle = MaterialTheme.colorScheme.primary,
|
||||
replyText = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
} else {
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
|
||||
MessageBubbleColors(
|
||||
container = MaterialTheme.colorScheme.surfaceContainer,
|
||||
container = containerColor,
|
||||
content = MaterialTheme.colorScheme.onSurface,
|
||||
replyContainer = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
replyContainer = containerColor,
|
||||
replyInnerContainer = MaterialTheme.colorScheme.surfaceColorAtElevation(20.dp),
|
||||
replyTitle = MaterialTheme.colorScheme.primary,
|
||||
replyText = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun Bubble() {
|
||||
AppTheme(
|
||||
useDarkTheme = true,
|
||||
useDarkTheme = isSystemInDarkTheme(),
|
||||
useDynamicColors = true
|
||||
) {
|
||||
Column {
|
||||
|
||||
+3
-1
@@ -188,7 +188,9 @@ fun MessageOptionsDialog(
|
||||
}
|
||||
|
||||
MessageOptionItem(
|
||||
title = viewCount?.let { "$it views" } ?: "...",
|
||||
title = viewCount?.let {
|
||||
if (it == 0) "No views" else "$it views"
|
||||
} ?: "...",
|
||||
iconResId = R.drawable.ic_visibility_round_24,
|
||||
tintColor = primaryColor,
|
||||
onClick = {}
|
||||
|
||||
+34
-18
@@ -26,9 +26,12 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -40,7 +43,6 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
@@ -193,11 +195,22 @@ fun MessagesList(
|
||||
}
|
||||
)
|
||||
|
||||
val offsetX = remember { Animatable(0f) }
|
||||
var animate by remember { mutableStateOf(false) }
|
||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||
var offsetDistinct by remember { mutableFloatStateOf(0f) }
|
||||
val offsetAnimatable = remember { Animatable(0f) }
|
||||
|
||||
val offsetDistinct by snapshotFlow { offsetX.value }
|
||||
.distinctUntilChanged()
|
||||
.collectAsStateWithLifecycle(offsetX)
|
||||
LaunchedEffect(offsetX) {
|
||||
if (!animate) {
|
||||
offsetAnimatable.snapTo(offsetX)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { offsetX.minus(5f).coerceIn(-100f, 0f) }
|
||||
.distinctUntilChanged()
|
||||
.collect { offsetDistinct = it }
|
||||
}
|
||||
|
||||
LaunchedEffect(offsetDistinct) {
|
||||
if (offsetDistinct == -100f && AppSettings.General.enableHaptic) {
|
||||
@@ -222,32 +235,35 @@ fun MessagesList(
|
||||
},
|
||||
onClick = { onMessageClicked(item.id) }
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
.pointerInput(item.cmId) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragCancel = {
|
||||
if (offsetX.value == -100f) {
|
||||
if (offsetX == -100f) {
|
||||
onRequestMessageReply(item.cmId)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
offsetX.animateTo(0f)
|
||||
animate = true
|
||||
offsetX = 0f
|
||||
offsetAnimatable.animateTo(0f)
|
||||
animate = false
|
||||
}
|
||||
},
|
||||
onDragEnd = {
|
||||
if (offsetX.value == -100f) {
|
||||
if (offsetX == -100f) {
|
||||
onRequestMessageReply(item.cmId)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
offsetX.animateTo(0f)
|
||||
animate = true
|
||||
offsetX = 0f
|
||||
offsetAnimatable.animateTo(0f)
|
||||
animate = false
|
||||
}
|
||||
},
|
||||
onHorizontalDrag = { _, dragAmount ->
|
||||
scope.launch {
|
||||
offsetX.snapTo(
|
||||
(offsetX.value + dragAmount).coerceIn(-100f, 0f)
|
||||
)
|
||||
}
|
||||
onHorizontalDrag = { change, dragAmount ->
|
||||
change.consume()
|
||||
offsetX = (offsetX + dragAmount).coerceIn(-100f, 0f)
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -278,7 +294,7 @@ fun MessagesList(
|
||||
onRequestScrollToCmId(item.replyCmId!!)
|
||||
}
|
||||
},
|
||||
offsetX = offsetX.value
|
||||
offsetX = offsetAnimatable.value
|
||||
)
|
||||
} else {
|
||||
IncomingMessageBubble(
|
||||
@@ -305,7 +321,7 @@ fun MessagesList(
|
||||
onRequestScrollToCmId(item.replyCmId!!)
|
||||
}
|
||||
},
|
||||
offsetX = offsetX.value
|
||||
offsetX = offsetAnimatable.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-13
@@ -16,7 +16,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -53,6 +52,8 @@ private val previewTypes = listOf(
|
||||
|
||||
@Composable
|
||||
fun Attachments(
|
||||
withText: Boolean,
|
||||
withReply: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
attachments: ImmutableList<out VkAttachment>,
|
||||
onClick: (VkAttachment) -> Unit = {},
|
||||
@@ -64,23 +65,20 @@ fun Attachments(
|
||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||
|
||||
Column(modifier = modifier) {
|
||||
val previewAttachments by remember(attachments) {
|
||||
derivedStateOf {
|
||||
attachments.values.filter { it.type in previewTypes }
|
||||
}
|
||||
val previewAttachments = remember(attachments) {
|
||||
attachments.values.filter { it.type in previewTypes }
|
||||
}
|
||||
|
||||
val nonPreviewAttachments by remember(attachments) {
|
||||
derivedStateOf {
|
||||
attachments.values.filterNot { it.type in previewTypes }
|
||||
.sortedBy { it.type.ordinal }
|
||||
}
|
||||
val nonPreviewAttachments = remember(attachments) {
|
||||
attachments.values.filterNot { it.type in previewTypes }.sortedBy { it.type.ordinal }
|
||||
}
|
||||
|
||||
if (previewAttachments.isNotEmpty()) {
|
||||
Previews(
|
||||
DynamicPreviewGrid(
|
||||
withText = withText,
|
||||
withReply = withReply,
|
||||
modifier = Modifier,
|
||||
photos = previewAttachments
|
||||
previews = previewAttachments
|
||||
.map(VkAttachment::asUiPhoto)
|
||||
.toImmutableList(),
|
||||
onClick = { index ->
|
||||
@@ -187,7 +185,8 @@ fun Attachments(
|
||||
.let(::downsampleWaveform)
|
||||
.let(::downsampleWaveform)
|
||||
.let { amplifyWaveform(it, audioMessage.waveform.max()) }
|
||||
.map(::WaveForm),
|
||||
.map(::WaveForm)
|
||||
.toImmutableList(),
|
||||
isPlaying = false,
|
||||
onPlayClick = {}
|
||||
)
|
||||
|
||||
+2
-1
@@ -26,11 +26,12 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.times
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.components.FastIconButton
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlin.collections.forEachIndexed
|
||||
|
||||
@Composable
|
||||
fun AudioMessage(
|
||||
waveform: List<WaveForm>,
|
||||
waveform: ImmutableList<WaveForm>,
|
||||
isPlaying: Boolean,
|
||||
onPlayClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
||||
+29
-25
@@ -1,6 +1,6 @@
|
||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -33,25 +33,12 @@ import dev.meloda.fast.ui.components.FastIconButton
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun Previews(
|
||||
modifier: Modifier = Modifier,
|
||||
photos: ImmutableList<UiPreview>,
|
||||
onClick: (index: Int) -> Unit = {},
|
||||
onLongClick: (index: Int) -> Unit = {}
|
||||
) {
|
||||
DynamicPreviewGrid(
|
||||
modifier = modifier,
|
||||
photos = photos,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@Composable
|
||||
fun DynamicPreviewGrid(
|
||||
photos: ImmutableList<UiPreview>,
|
||||
withText: Boolean,
|
||||
withReply: Boolean,
|
||||
previews: ImmutableList<UiPreview>,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: (index: Int) -> Unit = {},
|
||||
onLongClick: (index: Int) -> Unit = {}
|
||||
@@ -60,16 +47,27 @@ fun DynamicPreviewGrid(
|
||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||
|
||||
val spacing = 2.dp
|
||||
val shape = RoundedCornerShape(8.dp)
|
||||
val cornerRadius = 20.dp
|
||||
val insideRadius = 4.dp
|
||||
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val calculateShape by rememberUpdatedState { outer: Int, inner: Int, outLast: Int, inLast: Int ->
|
||||
RoundedCornerShape(
|
||||
topStart = if (!withText && !withReply && outer == 0 && inner == 0) cornerRadius else insideRadius,
|
||||
topEnd = if (!withText && !withReply && outer == 0 && inner == inLast) cornerRadius else insideRadius,
|
||||
bottomStart = if (outer == outLast && inner == 0) cornerRadius else insideRadius,
|
||||
bottomEnd = if (outer == outLast && inner == inLast) cornerRadius else insideRadius
|
||||
)
|
||||
}
|
||||
|
||||
BoxWithConstraints(modifier = modifier.padding(4.dp)) {
|
||||
val maxWidthPx = with(LocalDensity.current) { maxWidth.toPx() }
|
||||
val spacingPx = with(LocalDensity.current) { spacing.toPx() }
|
||||
|
||||
val rows = photos.chunked(3)
|
||||
val rows = previews.chunked(3)
|
||||
Log.d("ROWS", "DynamicPreviewGrid: ${rows.size}")
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(spacing)) {
|
||||
rows.forEachIndexed { index, row ->
|
||||
rows.forEachIndexed { outerIndex, row ->
|
||||
val aspectRatios = row.map { it.width.toFloat() / it.height }
|
||||
val totalAspect = aspectRatios.sum()
|
||||
|
||||
@@ -80,6 +78,8 @@ fun DynamicPreviewGrid(
|
||||
val height = photoWidthPx / aspectRatios[index]
|
||||
val heightDp = with(LocalDensity.current) { height.toDp() }
|
||||
|
||||
val shape = calculateShape(outerIndex, index, rows.lastIndex, row.lastIndex)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(heightDp)
|
||||
@@ -95,14 +95,14 @@ fun DynamicPreviewGrid(
|
||||
.height(heightDp)
|
||||
.clip(shape)
|
||||
.combinedClickable(
|
||||
onLongClick = { currentOnLongClick(index) },
|
||||
onClick = { currentOnClick(index) }
|
||||
onLongClick = { currentOnLongClick(outerIndex * 3 + index) },
|
||||
onClick = { currentOnClick(outerIndex * 3 + index) }
|
||||
)
|
||||
)
|
||||
|
||||
if (preview.isVideo) {
|
||||
FastIconButton(
|
||||
onClick = { currentOnClick(index) },
|
||||
onClick = { currentOnClick(outerIndex * 3 + index) },
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
@@ -146,6 +146,10 @@ fun PreviewDynamicPhotoGrid() {
|
||||
.padding(8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
DynamicPreviewGrid(photos = mockPhotos.take(10).toImmutableList())
|
||||
DynamicPreviewGrid(
|
||||
withText = false,
|
||||
withReply = false,
|
||||
previews = mockPhotos.take(10).toImmutableList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+50
-19
@@ -3,6 +3,7 @@ package dev.meloda.fast.messageshistory.presentation.attachments
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -23,12 +24,15 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.meloda.fast.domain.util.annotated
|
||||
import dev.meloda.fast.domain.util.orEmpty
|
||||
import dev.meloda.fast.ui.common.FastPreview
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun Reply(
|
||||
@@ -37,6 +41,8 @@ fun Reply(
|
||||
shape: Shape,
|
||||
backgroundColor: Color,
|
||||
innerBackgroundColor: Color,
|
||||
titleColor: Color,
|
||||
textColor: Color,
|
||||
title: String,
|
||||
summary: AnnotatedString?,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -47,7 +53,7 @@ fun Reply(
|
||||
color = backgroundColor,
|
||||
shape = shape
|
||||
)
|
||||
.height(40.dp)
|
||||
.height(48.dp)
|
||||
.padding(
|
||||
top = 4.dp,
|
||||
start = 4.dp,
|
||||
@@ -66,7 +72,7 @@ fun Reply(
|
||||
modifier = Modifier
|
||||
.width(3.dp)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colorScheme.onBackground)
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
@@ -77,17 +83,22 @@ fun Reply(
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 16.sp,
|
||||
color = titleColor
|
||||
)
|
||||
|
||||
AnimatedVisibility(summary != null) {
|
||||
Text(
|
||||
text = summary.orEmpty(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 20.sp,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -98,7 +109,9 @@ fun Reply(
|
||||
@Composable
|
||||
private fun ReplyBasePreview(
|
||||
backgroundColor: Color,
|
||||
innerBackgroundColor: Color
|
||||
innerBackgroundColor: Color,
|
||||
titleColor: Color,
|
||||
textColor: Color
|
||||
) {
|
||||
Reply(
|
||||
modifier = Modifier.width(120.dp),
|
||||
@@ -111,24 +124,42 @@ private fun ReplyBasePreview(
|
||||
summary = "2 photos".annotated(),
|
||||
backgroundColor = backgroundColor,
|
||||
innerBackgroundColor = innerBackgroundColor,
|
||||
bottomPadding = 0.dp
|
||||
titleColor = titleColor,
|
||||
textColor = textColor,
|
||||
bottomPadding = 0.dp,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun IncomingReplyPreview() {
|
||||
ReplyBasePreview(
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
innerBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(20.dp)
|
||||
)
|
||||
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||
ReplyBasePreview(
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
innerBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(20.dp),
|
||||
titleColor = MaterialTheme.colorScheme.primary,
|
||||
textColor = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun OutgoingReplyPreview() {
|
||||
ReplyBasePreview(
|
||||
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
innerBackgroundColor = MaterialTheme.colorScheme.inversePrimary
|
||||
)
|
||||
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||
val bg = MaterialTheme.colorScheme.primaryContainer
|
||||
val inner = MaterialTheme.colorScheme.background.copy(
|
||||
if (isSystemInDarkTheme()) 0.3f else 0.6f
|
||||
)
|
||||
val title = MaterialTheme.colorScheme.primary
|
||||
val text = MaterialTheme.colorScheme.onBackground
|
||||
|
||||
|
||||
ReplyBasePreview(
|
||||
backgroundColor = bg,
|
||||
innerBackgroundColor = inner,
|
||||
titleColor = title,
|
||||
textColor = text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user