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 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.Attachments
import dev.meloda.fast.messageshistory.presentation.attachments.Reply import dev.meloda.fast.messageshistory.presentation.attachments.Reply
import dev.meloda.fast.model.api.domain.VkAttachment 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.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.emptyImmutableList import dev.meloda.fast.ui.util.emptyImmutableList
@@ -81,22 +82,6 @@ fun MessageBubble(
MaterialTheme.colorScheme.onPrimaryContainer 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) { val shouldShowBubble by remember(text) {
derivedStateOf { text != null } derivedStateOf { text != null }
} }
@@ -119,6 +104,22 @@ fun MessageBubble(
mutableIntStateOf(0) 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) { CompositionLocalProvider(LocalContentColor provides contentColor) {
Column( Column(
modifier = modifier modifier = modifier
@@ -217,7 +218,12 @@ fun MessageBubble(
topEnd = 0.dp topEnd = 0.dp
) )
) )
.background(backgroundColor) .background(
backgroundColor.copy(
alpha = if (attachments.firstOrNull() is VkStickerDomain) 0f
else 1f
)
)
) { ) {
Attachments( Attachments(
modifier = Modifier, modifier = Modifier,
@@ -1,20 +1,29 @@
package dev.meloda.fast.messageshistory.presentation.attachments package dev.meloda.fast.messageshistory.presentation.attachments
import androidx.compose.foundation.layout.Column 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.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier 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.data.AttachmentType
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAudioDomain import dev.meloda.fast.model.api.domain.VkAudioDomain
import dev.meloda.fast.model.api.domain.VkFileDomain import dev.meloda.fast.model.api.domain.VkFileDomain
import dev.meloda.fast.model.api.domain.VkLinkDomain import dev.meloda.fast.model.api.domain.VkLinkDomain
import dev.meloda.fast.model.api.domain.VkPhotoDomain 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.model.api.domain.VkVideoDomain
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
@@ -31,12 +40,12 @@ fun Attachments(
onClick: (VkAttachment) -> Unit = {}, onClick: (VkAttachment) -> Unit = {},
onLongClick: (VkAttachment) -> Unit = {} onLongClick: (VkAttachment) -> Unit = {}
) { ) {
if (attachments.isEmpty()) return
val currentOnClick by rememberUpdatedState(onClick) val currentOnClick by rememberUpdatedState(onClick)
val currentOnLongClick by rememberUpdatedState(onLongClick) val currentOnLongClick by rememberUpdatedState(onLongClick)
Column(modifier = modifier) { Column(modifier = modifier) {
if (attachments.isEmpty()) return
val previewAttachments by remember(attachments) { val previewAttachments by remember(attachments) {
derivedStateOf { derivedStateOf {
attachments.values.filter { it.type in previewTypes } 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()
)
}
}