From 60a30b9422ec1df5b9722e2dbb4aa85d504dbf9f Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Thu, 26 Jun 2025 19:54:33 +0300 Subject: [PATCH] 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. --- .../fast/model/api/domain/VkStickerDomain.kt | 8 ++++ .../presentation/MessageBubble.kt | 40 +++++++++++-------- .../presentation/attachments/Attachments.kt | 35 ++++++++++++++-- .../presentation/attachments/Sticker.kt | 36 +++++++++++++++++ 4 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Sticker.kt diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt index afccb0ee..51878afa 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/domain/VkStickerDomain.kt @@ -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" + } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt index de9b3562..2fb3970d 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessageBubble.kt @@ -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, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Attachments.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Attachments.kt index 69517eb1..8aab16c2 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Attachments.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Attachments.kt @@ -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) + ) + } } } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Sticker.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Sticker.kt new file mode 100644 index 00000000..ed091d52 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/attachments/Sticker.kt @@ -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() + ) + } +}