feat: Display stickers in messages (#200)

This commit introduces the ability to display stickers within message bubbles.

Key changes:
- `Attachments.kt`: Added handling for `AttachmentType.STICKER`. If an attachment type is unsupported, a placeholder text is now displayed.
- `Sticker.kt`: New composable created to render `VkStickerDomain` using `AsyncImage`.
- `MessageBubble.kt`:
    - Adjusted background alpha for sticker messages to make the bubble transparent.
    - Minor refactoring of `minDateContainerWidth` and `dateContainerWidth` initialization.
- `VkStickerDomain.kt`: Added `getUrl()` function to construct sticker image URLs, with options for specifying width and background.
This commit is contained in:
2025-06-26 19:54:33 +03:00
committed by GitHub
parent 93d81f1e9e
commit 60a30b9422
4 changed files with 98 additions and 21 deletions
@@ -21,4 +21,12 @@ data class VkStickerDomain(
return null
}
fun getUrl(width: Int = 256, withBackground: Boolean = false): String? = when {
withBackground && backgroundImages != null -> {
backgroundImages.firstOrNull { it.width >= width }?.url
}
images != null -> images.firstOrNull { it.width >= width }?.url
else -> "https://vk.com/sticker/1-${id}-${width}b"
}
}
@@ -35,6 +35,7 @@ import dev.meloda.fast.messageshistory.model.SendingStatus
import dev.meloda.fast.messageshistory.presentation.attachments.Attachments
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.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.emptyImmutableList
@@ -81,22 +82,6 @@ fun MessageBubble(
MaterialTheme.colorScheme.onPrimaryContainer
}
val minDateContainerWidth by remember(isEdited, isOut, isPinned, isImportant) {
derivedStateOf {
val mainPart = if (isEdited) 50.dp else 30.dp
val readIndicatorPart = if (isOut) 14.dp else 0.dp
val pinnedIndicatorPart = if (isPinned) 14.dp else 0.dp
val importantIndicatorPart = if (isImportant) 14.dp else 0.dp
mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart
}
}
val dateContainerWidth by animateDpAsState(
targetValue = minDateContainerWidth,
label = "dateContainerWidth"
)
val shouldShowBubble by remember(text) {
derivedStateOf { text != null }
}
@@ -119,6 +104,22 @@ fun MessageBubble(
mutableIntStateOf(0)
}
val minDateContainerWidth by remember(isEdited, isOut, isPinned, isImportant) {
derivedStateOf {
val mainPart = if (isEdited) 50 else 30
val readIndicatorPart = if (isOut) 14 else 0
val pinnedIndicatorPart = if (isPinned) 14 else 0
val importantIndicatorPart = if (isImportant) 14 else 0
(mainPart + readIndicatorPart + pinnedIndicatorPart + importantIndicatorPart).dp
}
}
val dateContainerWidth by animateDpAsState(
targetValue = minDateContainerWidth,
label = "dateContainerWidth"
)
CompositionLocalProvider(LocalContentColor provides contentColor) {
Column(
modifier = modifier
@@ -217,7 +218,12 @@ fun MessageBubble(
topEnd = 0.dp
)
)
.background(backgroundColor)
.background(
backgroundColor.copy(
alpha = if (attachments.firstOrNull() is VkStickerDomain) 0f
else 1f
)
)
) {
Attachments(
modifier = Modifier,
@@ -1,20 +1,29 @@
package dev.meloda.fast.messageshistory.presentation.attachments
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import dev.meloda.fast.model.api.data.AttachmentType
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAudioDomain
import dev.meloda.fast.model.api.domain.VkFileDomain
import dev.meloda.fast.model.api.domain.VkLinkDomain
import dev.meloda.fast.model.api.domain.VkPhotoDomain
import dev.meloda.fast.model.api.domain.VkStickerDomain
import dev.meloda.fast.model.api.domain.VkVideoDomain
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
@@ -31,12 +40,12 @@ fun Attachments(
onClick: (VkAttachment) -> Unit = {},
onLongClick: (VkAttachment) -> Unit = {}
) {
if (attachments.isEmpty()) return
val currentOnClick by rememberUpdatedState(onClick)
val currentOnLongClick by rememberUpdatedState(onLongClick)
Column(modifier = modifier) {
if (attachments.isEmpty()) return
val previewAttachments by remember(attachments) {
derivedStateOf {
attachments.values.filter { it.type in previewTypes }
@@ -92,7 +101,25 @@ fun Attachments(
)
}
else -> Unit
AttachmentType.STICKER -> {
Sticker(
item = attachment as VkStickerDomain
)
}
else -> {
Text(
text = buildAnnotatedString {
append("Unsupported attachment: [${attachment.type}]")
addStyle(SpanStyle(fontWeight = FontWeight.Medium), 0, length)
addStyle(SpanStyle(fontStyle = FontStyle.Italic), 0, length)
addStyle(SpanStyle(textDecoration = TextDecoration.Underline), 0, length)
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
)
}
}
}
}
@@ -0,0 +1,36 @@
package dev.meloda.fast.messageshistory.presentation.attachments
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import dev.meloda.fast.model.api.domain.VkStickerDomain
@Composable
fun Sticker(
modifier: Modifier = Modifier,
item: VkStickerDomain
) {
Box(
modifier = modifier.size(192.dp),
contentAlignment = Alignment.Center
) {
AsyncImage(
model = item.getUrl(
width = 256,
withBackground = false
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(8.dp)
.fillMaxSize()
)
}
}