Reply attachment (#195)
This commit is contained in:
+3
-3
@@ -923,7 +923,9 @@ class MessagesHistoryViewModelImpl(
|
||||
isImportant = false,
|
||||
forwards = null,
|
||||
attachments = null,
|
||||
replyMessage = null,
|
||||
replyMessage = when {
|
||||
else -> null
|
||||
},
|
||||
geoType = null,
|
||||
user = VkMemoryCache.getUser(UserConfig.userId),
|
||||
group = null,
|
||||
@@ -932,8 +934,6 @@ class MessagesHistoryViewModelImpl(
|
||||
isPinned = false,
|
||||
isSpam = false,
|
||||
pinnedAt = null,
|
||||
|
||||
// TODO: 04-Apr-25, Danil Nikolaev: implement
|
||||
formatData = formatData,
|
||||
)
|
||||
formatData = formatData.copy(items = emptyList())
|
||||
|
||||
+4
-1
@@ -29,7 +29,10 @@ sealed class UiItem(
|
||||
val isSelected: Boolean,
|
||||
val isPinned: Boolean,
|
||||
val isImportant: Boolean,
|
||||
val attachments: List<VkAttachment>?
|
||||
val attachments: List<VkAttachment>?,
|
||||
val replyCmId: Long?,
|
||||
val replyTitle: String?,
|
||||
val replySummary: String?
|
||||
) : UiItem(id, cmId)
|
||||
|
||||
data class ActionMessage(
|
||||
|
||||
+9
-4
@@ -30,24 +30,26 @@ import coil.compose.rememberAsyncImagePainter
|
||||
import coil.imageLoader
|
||||
import dev.meloda.fast.messageshistory.model.UiItem
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun IncomingMessageBubble(
|
||||
enableAnimations: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
message: UiItem.Message,
|
||||
onClick: (VkAttachment) -> Unit = {},
|
||||
onLongClick: (VkAttachment) -> Unit = {}
|
||||
onLongClick: (VkAttachment) -> Unit = {},
|
||||
onReplyClick: () -> Unit = {}
|
||||
) {
|
||||
val currentOnClick by rememberUpdatedState(onClick)
|
||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||
val currentOnReplyClick by rememberUpdatedState(onReplyClick)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (LocalThemeConfig.current.enableAnimations) Modifier.animateContentSize()
|
||||
if (enableAnimations) Modifier.animateContentSize()
|
||||
else Modifier
|
||||
),
|
||||
) {
|
||||
@@ -103,8 +105,11 @@ fun IncomingMessageBubble(
|
||||
isImportant = message.isImportant,
|
||||
isSelected = message.isSelected,
|
||||
attachments = message.attachments?.toImmutableList(),
|
||||
replyTitle = message.replyTitle,
|
||||
replySummary = message.replySummary,
|
||||
onClick = currentOnClick,
|
||||
onLongClick = currentOnLongClick
|
||||
onLongClick = currentOnLongClick,
|
||||
onReplyClick = currentOnReplyClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+60
-11
@@ -7,7 +7,9 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -25,11 +27,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
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.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.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
@@ -48,9 +52,14 @@ fun MessageBubble(
|
||||
isImportant: Boolean,
|
||||
isSelected: Boolean,
|
||||
attachments: ImmutableList<VkAttachment>?,
|
||||
replyTitle: String?,
|
||||
replySummary: String? = null,
|
||||
onClick: (VkAttachment) -> Unit = {},
|
||||
onLongClick: (VkAttachment) -> Unit = {}
|
||||
onLongClick: (VkAttachment) -> Unit = {},
|
||||
onReplyClick: () -> Unit = {}
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
val currentOnClick by rememberUpdatedState(onClick)
|
||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||
|
||||
@@ -60,6 +69,11 @@ fun MessageBubble(
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
}
|
||||
val replyBackgroundColor = if (!isOut) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.inversePrimary
|
||||
}
|
||||
|
||||
val contentColor = if (!isOut) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
@@ -101,8 +115,38 @@ fun MessageBubble(
|
||||
}
|
||||
}
|
||||
|
||||
var containerWidth by remember {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.wrapContentWidth()
|
||||
.onGloballyPositioned {
|
||||
containerWidth = it.size.width
|
||||
}
|
||||
) {
|
||||
if (replyTitle != null) {
|
||||
Reply(
|
||||
modifier = Modifier
|
||||
.padding(if (attachments == null || text != null) 0.dp else 4.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
|
||||
),
|
||||
onClick = onReplyClick,
|
||||
title = replyTitle,
|
||||
summary = replySummary,
|
||||
backgroundColor = backgroundColor,
|
||||
innerBackgroundColor = replyBackgroundColor
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldShowBubble) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
@@ -111,18 +155,19 @@ fun MessageBubble(
|
||||
}
|
||||
.widthIn(min = if (shouldFill) attachmentsContainerWidth.dp else 56.dp)
|
||||
.clip(
|
||||
if (attachments == null) RoundedCornerShape(24.dp)
|
||||
else RoundedCornerShape(
|
||||
topStart = 24.dp,
|
||||
topEnd = 24.dp,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp
|
||||
RoundedCornerShape(
|
||||
topStart = if (replyTitle == null) 24.dp else 0.dp,
|
||||
topEnd = if (replyTitle == null) 24.dp else 0.dp,
|
||||
bottomStart = if (attachments != null) 0.dp else 24.dp,
|
||||
bottomEnd = if (attachments != null) 0.dp else 24.dp
|
||||
)
|
||||
)
|
||||
.background(backgroundColor)
|
||||
.padding(
|
||||
horizontal = 8.dp,
|
||||
vertical = 6.dp
|
||||
start = 8.dp,
|
||||
end = 8.dp,
|
||||
top = if (replyTitle != null) 0.dp else 6.dp,
|
||||
bottom = if (replyTitle != null) 4.dp else 6.dp
|
||||
)
|
||||
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
||||
) {
|
||||
@@ -223,6 +268,10 @@ private fun Bubble() {
|
||||
isPinned = true,
|
||||
isImportant = true,
|
||||
isSelected = false,
|
||||
attachments = emptyImmutableList()
|
||||
attachments = emptyImmutableList(),
|
||||
replyTitle = "Danil Nikolaev",
|
||||
replySummary = "2 photos",
|
||||
onClick = {},
|
||||
onLongClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
+10
-10
@@ -17,22 +17,22 @@ fun MessageTextContainer(
|
||||
isOut: Boolean,
|
||||
isSelected: Boolean,
|
||||
) {
|
||||
if (text != null) {
|
||||
if (isSelected) {
|
||||
SelectionContainer {
|
||||
MessageText(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
isOut = isOut,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (text == null) return
|
||||
|
||||
if (isSelected) {
|
||||
SelectionContainer {
|
||||
MessageText(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
isOut = isOut,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
MessageText(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
isOut = isOut,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -209,13 +209,18 @@ fun MessagesList(
|
||||
)
|
||||
else Modifier
|
||||
),
|
||||
enableAnimations = theme.enableAnimations,
|
||||
message = item,
|
||||
onClick = { attachment ->
|
||||
|
||||
onAttachmentClick(item, attachment)
|
||||
},
|
||||
onLongClick = { attachment ->
|
||||
onAttachmentLongClick(item, attachment)
|
||||
},
|
||||
onReplyClick = {
|
||||
if (item.replyCmId != null) {
|
||||
onRequestScrollToCmId(item.replyCmId)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
@@ -230,12 +235,18 @@ fun MessagesList(
|
||||
)
|
||||
else Modifier
|
||||
),
|
||||
enableAnimations = theme.enableAnimations,
|
||||
message = item,
|
||||
onClick = { attachment ->
|
||||
onAttachmentClick(item, attachment)
|
||||
},
|
||||
onLongClick = { attachment ->
|
||||
onAttachmentLongClick(item, attachment)
|
||||
},
|
||||
onReplyClick = {
|
||||
if (item.replyCmId != null) {
|
||||
onRequestScrollToCmId(item.replyCmId)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+18
-9
@@ -2,42 +2,48 @@ package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.messageshistory.model.UiItem
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
|
||||
@Composable
|
||||
fun OutgoingMessageBubble(
|
||||
modifier: Modifier = Modifier,
|
||||
enableAnimations: Boolean,
|
||||
message: UiItem.Message,
|
||||
onClick: (VkAttachment) -> Unit = {},
|
||||
onLongClick: (VkAttachment) -> Unit = {}
|
||||
onLongClick: (VkAttachment) -> Unit = {},
|
||||
onReplyClick: () -> Unit = {}
|
||||
) {
|
||||
val currentOnClick by rememberUpdatedState(onClick)
|
||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||
val currentOnReplyClick by rememberUpdatedState(onReplyClick)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (LocalThemeConfig.current.enableAnimations) Modifier.animateContentSize()
|
||||
if (enableAnimations) Modifier.animateContentSize()
|
||||
else Modifier
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
Column(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.fillMaxWidth(0.85f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
MessageBubble(
|
||||
modifier = Modifier,
|
||||
@@ -51,8 +57,11 @@ fun OutgoingMessageBubble(
|
||||
isImportant = message.isImportant,
|
||||
isSelected = message.isSelected,
|
||||
attachments = message.attachments?.toImmutableList(),
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
replyTitle = message.replyTitle,
|
||||
replySummary = message.replySummary,
|
||||
onClick = currentOnClick,
|
||||
onLongClick = currentOnLongClick,
|
||||
onReplyClick = currentOnReplyClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package dev.meloda.fast.messageshistory.presentation.attachments
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun Reply(
|
||||
modifier: Modifier = Modifier,
|
||||
bottomPadding: Dp,
|
||||
shape: Shape,
|
||||
onClick: () -> Unit,
|
||||
backgroundColor: Color,
|
||||
innerBackgroundColor: Color,
|
||||
title: String,
|
||||
summary: String?
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = shape
|
||||
)
|
||||
.height(40.dp)
|
||||
.padding(
|
||||
top = 4.dp,
|
||||
start = 4.dp,
|
||||
end = 4.dp,
|
||||
bottom = bottomPadding
|
||||
)
|
||||
) {
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.fillMaxSize()
|
||||
.background(innerBackgroundColor)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(3.dp)
|
||||
.fillMaxHeight()
|
||||
.background(MaterialTheme.colorScheme.onBackground)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
summary?.let {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReplyBasePreview(
|
||||
backgroundColor: Color,
|
||||
innerBackgroundColor: Color
|
||||
) {
|
||||
Reply(
|
||||
modifier = Modifier.width(120.dp),
|
||||
shape = RoundedCornerShape(
|
||||
topStart = 12.dp,
|
||||
topEnd = 12.dp
|
||||
),
|
||||
onClick = {},
|
||||
title = "Danil Nikolaev",
|
||||
summary = "2 photos",
|
||||
backgroundColor = backgroundColor,
|
||||
innerBackgroundColor = innerBackgroundColor,
|
||||
bottomPadding = 0.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun IncomingReplyPreview() {
|
||||
ReplyBasePreview(
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
|
||||
innerBackgroundColor = MaterialTheme.colorScheme.surfaceColorAtElevation(20.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun OutgoingReplyPreview() {
|
||||
ReplyBasePreview(
|
||||
backgroundColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
innerBackgroundColor = MaterialTheme.colorScheme.inversePrimary
|
||||
)
|
||||
}
|
||||
+17
-1
@@ -57,6 +57,19 @@ fun VkMessage.extractTitle(): String = when {
|
||||
else -> throw IllegalStateException("Message is not from user nor group. fromId: $fromId")
|
||||
}
|
||||
|
||||
fun VkMessage.extractReplyTitle(): String? = replyMessage?.extractTitle()
|
||||
|
||||
// TODO: 24-Jun-25, Danil Nikolaev: improve
|
||||
fun VkMessage.extractReplySummary(): String? = when (val message = replyMessage) {
|
||||
null -> null
|
||||
else -> {
|
||||
when {
|
||||
message.text != null -> message.text
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun VkConversation.extractAvatar(): UiImage = when (peerType) {
|
||||
PeerType.USER -> {
|
||||
if (isAccount(id)) null
|
||||
@@ -144,7 +157,10 @@ fun VkMessage.asPresentation(
|
||||
isSelected = isSelected,
|
||||
isPinned = isPinned,
|
||||
isImportant = isImportant,
|
||||
attachments = attachments?.ifEmpty { null }
|
||||
attachments = attachments?.ifEmpty { null },
|
||||
replyCmId = replyMessage?.cmId,
|
||||
replyTitle = extractReplyTitle(),
|
||||
replySummary = extractReplySummary()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user