forked from melod1n/fast-messenger
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:
@@ -0,0 +1,7 @@
|
|||||||
|
package dev.meloda.fast.common.util
|
||||||
|
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
fun String.urlEncode(encoding: String = "utf-8"): String {
|
||||||
|
return URLEncoder.encode(this, encoding)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:autoMirrored="true"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M760,760L760,600Q760,550 725,515Q690,480 640,480L273,480L417,624L360,680L120,440L360,200L417,256L273,400L640,400Q723,400 781.5,458.5Q840,517 840,600L840,760L760,760Z" />
|
||||||
|
</vector>
|
||||||
+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