Reply attachment (#195)
This commit is contained in:
@@ -24,6 +24,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@@ -88,7 +89,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
requestNotificationPermissions()
|
requestNotificationPermissions()
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val context = LocalContext.current
|
val resources = LocalResources.current
|
||||||
|
|
||||||
val userSettings: UserSettings = koinInject()
|
val userSettings: UserSettings = koinInject()
|
||||||
val longPollController: LongPollController = koinInject()
|
val longPollController: LongPollController = koinInject()
|
||||||
@@ -164,10 +165,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val deviceWidthDp = remember(true) {
|
val deviceWidthDp = remember(true) {
|
||||||
context.resources.displayMetrics.widthPixels.pxToDp()
|
resources.displayMetrics.widthPixels.pxToDp()
|
||||||
}
|
}
|
||||||
val deviceHeightDp = remember(true) {
|
val deviceHeightDp = remember(true) {
|
||||||
context.resources.displayMetrics.heightPixels.pxToDp()
|
resources.displayMetrics.heightPixels.pxToDp()
|
||||||
}
|
}
|
||||||
|
|
||||||
val deviceWidthSize by remember(deviceWidthDp) {
|
val deviceWidthSize by remember(deviceWidthDp) {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class VkUsersMap(
|
|||||||
if (message.fromId > 0) map[message.fromId]
|
if (message.fromId > 0) map[message.fromId]
|
||||||
else null
|
else null
|
||||||
|
|
||||||
fun user(userid: Long): VkUser? = map[userId]
|
fun user(userId: Long): VkUser? = map[userId]
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -75,7 +75,13 @@ class ConversationsRepositoryImpl(
|
|||||||
user = usersMap.messageUser(message),
|
user = usersMap.messageUser(message),
|
||||||
group = groupsMap.messageGroup(message),
|
group = groupsMap.messageGroup(message),
|
||||||
actionUser = usersMap.messageActionUser(message),
|
actionUser = usersMap.messageActionUser(message),
|
||||||
actionGroup = groupsMap.messageActionGroup(message)
|
actionGroup = groupsMap.messageActionGroup(message),
|
||||||
|
replyMessage = message.replyMessage?.copy(
|
||||||
|
user = usersMap.messageUser(message),
|
||||||
|
group = groupsMap.messageGroup(message),
|
||||||
|
actionUser = usersMap.messageActionUser(message),
|
||||||
|
actionGroup = groupsMap.messageActionGroup(message),
|
||||||
|
)
|
||||||
).also { VkMemoryCache[message.id] = it }
|
).also { VkMemoryCache[message.id] = it }
|
||||||
}
|
}
|
||||||
item.conversation.asDomain(lastMessage).let { conversation ->
|
item.conversation.asDomain(lastMessage).let { conversation ->
|
||||||
|
|||||||
+14
-2
@@ -90,7 +90,13 @@ class MessagesRepositoryImpl(
|
|||||||
user = usersMap.messageUser(message),
|
user = usersMap.messageUser(message),
|
||||||
group = groupsMap.messageGroup(message),
|
group = groupsMap.messageGroup(message),
|
||||||
actionUser = usersMap.messageActionUser(message),
|
actionUser = usersMap.messageActionUser(message),
|
||||||
actionGroup = groupsMap.messageActionGroup(message)
|
actionGroup = groupsMap.messageActionGroup(message),
|
||||||
|
replyMessage = message.replyMessage?.copy(
|
||||||
|
user = usersMap.messageUser(message),
|
||||||
|
group = groupsMap.messageGroup(message),
|
||||||
|
actionUser = usersMap.messageActionUser(message),
|
||||||
|
actionGroup = groupsMap.messageActionGroup(message),
|
||||||
|
)
|
||||||
).also { VkMemoryCache[message.id] = it }
|
).also { VkMemoryCache[message.id] = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,7 +165,13 @@ class MessagesRepositoryImpl(
|
|||||||
user = usersMap.messageUser(message),
|
user = usersMap.messageUser(message),
|
||||||
group = groupsMap.messageGroup(message),
|
group = groupsMap.messageGroup(message),
|
||||||
actionUser = usersMap.messageActionUser(message),
|
actionUser = usersMap.messageActionUser(message),
|
||||||
actionGroup = groupsMap.messageActionGroup(message)
|
actionGroup = groupsMap.messageActionGroup(message),
|
||||||
|
replyMessage = message.replyMessage?.asDomain()?.copy(
|
||||||
|
user = usersMap.messageUser(message),
|
||||||
|
group = groupsMap.messageGroup(message),
|
||||||
|
actionUser = usersMap.messageActionUser(message),
|
||||||
|
actionGroup = groupsMap.messageActionGroup(message),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
|
|||||||
actionConversationMessageId = action?.conversationMessageId,
|
actionConversationMessageId = action?.conversationMessageId,
|
||||||
actionMessage = action?.message,
|
actionMessage = action?.message,
|
||||||
geoType = geo?.type,
|
geoType = geo?.type,
|
||||||
isImportant = important ?: false,
|
isImportant = important == true,
|
||||||
updateTime = updateTime,
|
updateTime = updateTime,
|
||||||
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
|
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
|
||||||
attachments = attachments.map(VkAttachmentItemData::toDomain),
|
attachments = attachments.map(VkAttachmentItemData::toDomain),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ data class VkMessage(
|
|||||||
val user: VkUser?,
|
val user: VkUser?,
|
||||||
val group: VkGroupDomain?,
|
val group: VkGroupDomain?,
|
||||||
val actionUser: VkUser?,
|
val actionUser: VkUser?,
|
||||||
val actionGroup: VkGroupDomain?
|
val actionGroup: VkGroupDomain?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun isPeerChat() = peerId > 2_000_000_000
|
fun isPeerChat() = peerId > 2_000_000_000
|
||||||
|
|||||||
+3
-3
@@ -923,7 +923,9 @@ class MessagesHistoryViewModelImpl(
|
|||||||
isImportant = false,
|
isImportant = false,
|
||||||
forwards = null,
|
forwards = null,
|
||||||
attachments = null,
|
attachments = null,
|
||||||
replyMessage = null,
|
replyMessage = when {
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
geoType = null,
|
geoType = null,
|
||||||
user = VkMemoryCache.getUser(UserConfig.userId),
|
user = VkMemoryCache.getUser(UserConfig.userId),
|
||||||
group = null,
|
group = null,
|
||||||
@@ -932,8 +934,6 @@ class MessagesHistoryViewModelImpl(
|
|||||||
isPinned = false,
|
isPinned = false,
|
||||||
isSpam = false,
|
isSpam = false,
|
||||||
pinnedAt = null,
|
pinnedAt = null,
|
||||||
|
|
||||||
// TODO: 04-Apr-25, Danil Nikolaev: implement
|
|
||||||
formatData = formatData,
|
formatData = formatData,
|
||||||
)
|
)
|
||||||
formatData = formatData.copy(items = emptyList())
|
formatData = formatData.copy(items = emptyList())
|
||||||
|
|||||||
+4
-1
@@ -29,7 +29,10 @@ sealed class UiItem(
|
|||||||
val isSelected: Boolean,
|
val isSelected: Boolean,
|
||||||
val isPinned: Boolean,
|
val isPinned: Boolean,
|
||||||
val isImportant: Boolean,
|
val isImportant: Boolean,
|
||||||
val attachments: List<VkAttachment>?
|
val attachments: List<VkAttachment>?,
|
||||||
|
val replyCmId: Long?,
|
||||||
|
val replyTitle: String?,
|
||||||
|
val replySummary: String?
|
||||||
) : UiItem(id, cmId)
|
) : UiItem(id, cmId)
|
||||||
|
|
||||||
data class ActionMessage(
|
data class ActionMessage(
|
||||||
|
|||||||
+9
-4
@@ -30,24 +30,26 @@ import coil.compose.rememberAsyncImagePainter
|
|||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun IncomingMessageBubble(
|
fun IncomingMessageBubble(
|
||||||
|
enableAnimations: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
onClick: (VkAttachment) -> Unit = {},
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
onLongClick: (VkAttachment) -> Unit = {},
|
||||||
|
onReplyClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val currentOnClick by rememberUpdatedState(onClick)
|
val currentOnClick by rememberUpdatedState(onClick)
|
||||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||||
|
val currentOnReplyClick by rememberUpdatedState(onReplyClick)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(
|
.then(
|
||||||
if (LocalThemeConfig.current.enableAnimations) Modifier.animateContentSize()
|
if (enableAnimations) Modifier.animateContentSize()
|
||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
@@ -103,8 +105,11 @@ fun IncomingMessageBubble(
|
|||||||
isImportant = message.isImportant,
|
isImportant = message.isImportant,
|
||||||
isSelected = message.isSelected,
|
isSelected = message.isSelected,
|
||||||
attachments = message.attachments?.toImmutableList(),
|
attachments = message.attachments?.toImmutableList(),
|
||||||
|
replyTitle = message.replyTitle,
|
||||||
|
replySummary = message.replySummary,
|
||||||
onClick = currentOnClick,
|
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.Column
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -25,11 +27,13 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.messageshistory.model.SendingStatus
|
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.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
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
|
||||||
@@ -48,9 +52,14 @@ fun MessageBubble(
|
|||||||
isImportant: Boolean,
|
isImportant: Boolean,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
attachments: ImmutableList<VkAttachment>?,
|
attachments: ImmutableList<VkAttachment>?,
|
||||||
|
replyTitle: String?,
|
||||||
|
replySummary: String? = null,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
onClick: (VkAttachment) -> Unit = {},
|
||||||
onLongClick: (VkAttachment) -> Unit = {}
|
onLongClick: (VkAttachment) -> Unit = {},
|
||||||
|
onReplyClick: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val currentOnClick by rememberUpdatedState(onClick)
|
val currentOnClick by rememberUpdatedState(onClick)
|
||||||
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
val currentOnLongClick by rememberUpdatedState(onLongClick)
|
||||||
|
|
||||||
@@ -60,6 +69,11 @@ fun MessageBubble(
|
|||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
}
|
}
|
||||||
|
val replyBackgroundColor = if (!isOut) {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.inversePrimary
|
||||||
|
}
|
||||||
|
|
||||||
val contentColor = if (!isOut) {
|
val contentColor = if (!isOut) {
|
||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onSurface
|
||||||
@@ -101,8 +115,38 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var containerWidth by remember {
|
||||||
|
mutableIntStateOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
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) {
|
if (shouldShowBubble) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -111,18 +155,19 @@ fun MessageBubble(
|
|||||||
}
|
}
|
||||||
.widthIn(min = if (shouldFill) attachmentsContainerWidth.dp else 56.dp)
|
.widthIn(min = if (shouldFill) attachmentsContainerWidth.dp else 56.dp)
|
||||||
.clip(
|
.clip(
|
||||||
if (attachments == null) RoundedCornerShape(24.dp)
|
RoundedCornerShape(
|
||||||
else RoundedCornerShape(
|
topStart = if (replyTitle == null) 24.dp else 0.dp,
|
||||||
topStart = 24.dp,
|
topEnd = if (replyTitle == null) 24.dp else 0.dp,
|
||||||
topEnd = 24.dp,
|
bottomStart = if (attachments != null) 0.dp else 24.dp,
|
||||||
bottomStart = 0.dp,
|
bottomEnd = if (attachments != null) 0.dp else 24.dp
|
||||||
bottomEnd = 0.dp
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = 8.dp,
|
start = 8.dp,
|
||||||
vertical = 6.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),
|
.then(if (theme.enableAnimations) Modifier.animateContentSize() else Modifier),
|
||||||
) {
|
) {
|
||||||
@@ -223,6 +268,10 @@ private fun Bubble() {
|
|||||||
isPinned = true,
|
isPinned = true,
|
||||||
isImportant = true,
|
isImportant = true,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
attachments = emptyImmutableList()
|
attachments = emptyImmutableList(),
|
||||||
|
replyTitle = "Danil Nikolaev",
|
||||||
|
replySummary = "2 photos",
|
||||||
|
onClick = {},
|
||||||
|
onLongClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -17,22 +17,22 @@ fun MessageTextContainer(
|
|||||||
isOut: Boolean,
|
isOut: Boolean,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
) {
|
) {
|
||||||
if (text != null) {
|
if (text == null) return
|
||||||
if (isSelected) {
|
|
||||||
SelectionContainer {
|
if (isSelected) {
|
||||||
MessageText(
|
SelectionContainer {
|
||||||
modifier = modifier,
|
|
||||||
text = text,
|
|
||||||
isOut = isOut,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
MessageText(
|
MessageText(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = text,
|
text = text,
|
||||||
isOut = isOut,
|
isOut = isOut,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
MessageText(
|
||||||
|
modifier = modifier,
|
||||||
|
text = text,
|
||||||
|
isOut = isOut,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -209,13 +209,18 @@ fun MessagesList(
|
|||||||
)
|
)
|
||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
|
enableAnimations = theme.enableAnimations,
|
||||||
message = item,
|
message = item,
|
||||||
onClick = { attachment ->
|
onClick = { attachment ->
|
||||||
|
|
||||||
onAttachmentClick(item, attachment)
|
onAttachmentClick(item, attachment)
|
||||||
},
|
},
|
||||||
onLongClick = { attachment ->
|
onLongClick = { attachment ->
|
||||||
onAttachmentLongClick(item, attachment)
|
onAttachmentLongClick(item, attachment)
|
||||||
|
},
|
||||||
|
onReplyClick = {
|
||||||
|
if (item.replyCmId != null) {
|
||||||
|
onRequestScrollToCmId(item.replyCmId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -230,12 +235,18 @@ fun MessagesList(
|
|||||||
)
|
)
|
||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
|
enableAnimations = theme.enableAnimations,
|
||||||
message = item,
|
message = item,
|
||||||
onClick = { attachment ->
|
onClick = { attachment ->
|
||||||
onAttachmentClick(item, attachment)
|
onAttachmentClick(item, attachment)
|
||||||
},
|
},
|
||||||
onLongClick = { attachment ->
|
onLongClick = { attachment ->
|
||||||
onAttachmentLongClick(item, 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.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.messageshistory.model.UiItem
|
import dev.meloda.fast.messageshistory.model.UiItem
|
||||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
|
||||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OutgoingMessageBubble(
|
fun OutgoingMessageBubble(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
enableAnimations: Boolean,
|
||||||
message: UiItem.Message,
|
message: UiItem.Message,
|
||||||
onClick: (VkAttachment) -> Unit = {},
|
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(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(
|
.then(
|
||||||
if (LocalThemeConfig.current.enableAnimations) Modifier.animateContentSize()
|
if (enableAnimations) Modifier.animateContentSize()
|
||||||
else Modifier
|
else Modifier
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.End
|
horizontalArrangement = Arrangement.End
|
||||||
) {
|
) {
|
||||||
Column(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(end = 16.dp)
|
.padding(end = 16.dp)
|
||||||
.fillMaxWidth(0.85f),
|
.fillMaxWidth(0.85f),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalAlignment = Alignment.End,
|
horizontalArrangement = Arrangement.End
|
||||||
) {
|
) {
|
||||||
MessageBubble(
|
MessageBubble(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
@@ -51,8 +57,11 @@ fun OutgoingMessageBubble(
|
|||||||
isImportant = message.isImportant,
|
isImportant = message.isImportant,
|
||||||
isSelected = message.isSelected,
|
isSelected = message.isSelected,
|
||||||
attachments = message.attachments?.toImmutableList(),
|
attachments = message.attachments?.toImmutableList(),
|
||||||
onClick = onClick,
|
replyTitle = message.replyTitle,
|
||||||
onLongClick = onLongClick
|
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")
|
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) {
|
fun VkConversation.extractAvatar(): UiImage = when (peerType) {
|
||||||
PeerType.USER -> {
|
PeerType.USER -> {
|
||||||
if (isAccount(id)) null
|
if (isAccount(id)) null
|
||||||
@@ -144,7 +157,10 @@ fun VkMessage.asPresentation(
|
|||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
isPinned = isPinned,
|
isPinned = isPinned,
|
||||||
isImportant = isImportant,
|
isImportant = isImportant,
|
||||||
attachments = attachments?.ifEmpty { null }
|
attachments = attachments?.ifEmpty { null },
|
||||||
|
replyCmId = replyMessage?.cmId,
|
||||||
|
replyTitle = extractReplyTitle(),
|
||||||
|
replySummary = extractReplySummary()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user