feat(messages): Implement reply functionality
This commit introduces the ability to reply to messages. - **API & Data Layer:** - Replaced `replyTo` parameter with `forward` in `sendMessage` calls across the data, domain, and repository layers to support the new reply mechanism. - **ViewModel:** - Added logic to handle the reply state, including storing the replied message's ID (`replyToCmId`). - When a message is sent, it now correctly constructs a `forward` JSON object if it is a reply. - The UI state (`MessagesHistoryScreenState`) is updated to show and hide the reply preview. - Added a `onReplyCloseClicked` handler to cancel a reply. - The ViewModel interface was removed, and the implementation class `MessagesHistoryViewModelImpl` is used directly. - **UI (Compose):** - A new `ReplyContainer` is displayed above the message input bar when a reply is active. - The input bar's corner radius animates to integrate with the reply container. - Added a `FocusRequester` to automatically focus the input field when the reply action is selected. - Added spacing in the message list to prevent the reply preview from overlapping messages. - The message options dialog now passes the `messageId` and `cmId` when an option is picked.
This commit is contained in:
+66
@@ -0,0 +1,66 @@
|
||||
package dev.meloda.fast.messageshistory
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import dev.meloda.fast.messageshistory.model.MessageDialog
|
||||
import dev.meloda.fast.messageshistory.model.MessageNavigation
|
||||
import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState
|
||||
import dev.meloda.fast.messageshistory.model.UiItem
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface MessagesHistoryViewModel {
|
||||
|
||||
val screenState: StateFlow<MessagesHistoryScreenState>
|
||||
val navigation: StateFlow<MessageNavigation?>
|
||||
val messages: StateFlow<List<VkMessage>>
|
||||
val uiMessages: StateFlow<List<UiItem>>
|
||||
val dialog: StateFlow<MessageDialog?>
|
||||
val selectedMessages: StateFlow<List<VkMessage>>
|
||||
|
||||
val inputFieldFocusRequester: StateFlow<Boolean>
|
||||
|
||||
val isNeedToScrollToIndex: StateFlow<Int?>
|
||||
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val imagesToPreload: StateFlow<List<String>>
|
||||
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
fun onNavigationConsumed()
|
||||
|
||||
fun onTopBarClicked()
|
||||
|
||||
fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle)
|
||||
fun onDialogDismissed(dialog: MessageDialog)
|
||||
fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle)
|
||||
|
||||
fun onScrolledToIndex()
|
||||
|
||||
fun onCloseButtonClicked()
|
||||
fun onRefresh()
|
||||
fun onAttachmentButtonClicked()
|
||||
fun onMessageInputChanged(newText: TextFieldValue)
|
||||
fun onEmojiButtonLongClicked()
|
||||
fun onActionButtonClicked()
|
||||
|
||||
fun onPaginationConditionsMet()
|
||||
|
||||
fun onMessageClicked(messageId: Long)
|
||||
fun onMessageLongClicked(messageId: Long)
|
||||
|
||||
fun onPinnedMessageClicked(messageId: Long)
|
||||
fun onUnpinMessageClicked()
|
||||
|
||||
fun onDeleteSelectedMessagesClicked()
|
||||
|
||||
fun onBoldClicked()
|
||||
fun onItalicClicked()
|
||||
fun onUnderlineClicked()
|
||||
fun onLinkClicked()
|
||||
fun onRegularClicked()
|
||||
|
||||
fun onReplyCloseClicked()
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package dev.meloda.fast.messageshistory.presentation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
@Composable
|
||||
fun ReplyContainer(
|
||||
onCloseClicked: () -> Unit = {},
|
||||
title: String,
|
||||
text: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 48.dp)
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topStart = 24.dp,
|
||||
topEnd = 24.dp,
|
||||
bottomStart = 0.dp,
|
||||
bottomEnd = 0.dp
|
||||
)
|
||||
)
|
||||
.background(backgroundColor)
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.round_reply_24px),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
AnimatedVisibility(text != null) {
|
||||
Text(
|
||||
text = text.orEmpty(),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
RippledClickContainer(
|
||||
modifier = Modifier.size(36.dp),
|
||||
shape = CircleShape,
|
||||
onClick = onCloseClicked
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.round_close_24px),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RippledClickContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(4.dp),
|
||||
onClick: () -> Unit,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.clickable(
|
||||
interactionSource = null,
|
||||
indication = ripple(),
|
||||
onClick = onClick
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ReplyContainerPreview() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ReplyContainer(
|
||||
onCloseClicked = {},
|
||||
title = "В ответ Ишак",
|
||||
text = "Приветствую тебя, Ишак!",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user