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:
2025-12-03 06:07:03 +03:00
parent dcddddea9b
commit 821ee46cef
12 changed files with 150 additions and 79 deletions
@@ -32,7 +32,7 @@ interface MessagesRepository {
peerId: Long, peerId: Long,
randomId: Long, randomId: Long,
message: String?, message: String?,
replyTo: Long?, forward: String?,
attachments: List<VkAttachment>?, attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData? formatData: VkMessage.FormatData?
): ApiResult<MessagesSendResponse, RestApiErrorDomain> ): ApiResult<MessagesSendResponse, RestApiErrorDomain>
@@ -195,7 +195,7 @@ class MessagesRepositoryImpl(
peerId: Long, peerId: Long,
randomId: Long, randomId: Long,
message: String?, message: String?,
replyTo: Long?, forward: String?,
attachments: List<VkAttachment>?, attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData? formatData: VkMessage.FormatData?
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -203,7 +203,7 @@ class MessagesRepositoryImpl(
peerId = peerId, peerId = peerId,
randomId = randomId, randomId = randomId,
message = message, message = message,
replyTo = replyTo, forward = forward,
attachments = attachments, attachments = attachments,
formatData = formatData formatData = formatData
) )
@@ -32,7 +32,7 @@ interface MessagesUseCase : BaseUseCase {
peerId: Long, peerId: Long,
randomId: Long, randomId: Long,
message: String?, message: String?,
replyTo: Long?, forward: String?,
attachments: List<VkAttachment>?, attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData? formatData: VkMessage.FormatData?
): Flow<State<MessagesSendResponse>> ): Flow<State<MessagesSendResponse>>
@@ -56,7 +56,7 @@ class MessagesUseCaseImpl(
peerId: Long, peerId: Long,
randomId: Long, randomId: Long,
message: String?, message: String?,
replyTo: Long?, forward: String?,
attachments: List<VkAttachment>?, attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData? formatData: VkMessage.FormatData?
): Flow<State<MessagesSendResponse>> = flowNewState { ): Flow<State<MessagesSendResponse>> = flowNewState {
@@ -64,7 +64,7 @@ class MessagesUseCaseImpl(
peerId = peerId, peerId = peerId,
randomId = randomId, randomId = randomId,
message = message, message = message,
replyTo = replyTo, forward = forward,
attachments = attachments, attachments = attachments,
formatData = formatData formatData = formatData
).mapToState() ).mapToState()
@@ -34,7 +34,7 @@ data class MessagesSendRequest(
val message: String?, val message: String?,
val lat: Int? = null, val lat: Int? = null,
val lon: Int? = null, val lon: Int? = null,
val replyTo: Long? = null, val forward: String? = null,
val stickerId: Long? = null, val stickerId: Long? = null,
val disableMentions: Boolean? = null, val disableMentions: Boolean? = null,
val doNotParseLinks: Boolean? = null, val doNotParseLinks: Boolean? = null,
@@ -51,7 +51,7 @@ data class MessagesSendRequest(
message?.let { this["message"] = it } message?.let { this["message"] = it }
lat?.let { this["lat"] = it.toString() } lat?.let { this["lat"] = it.toString() }
lon?.let { this["lon"] = it.toString() } lon?.let { this["lon"] = it.toString() }
replyTo?.let { this["reply_to"] = it.toString() } forward?.let { this["forward"] = it }
stickerId?.let { this["sticker_id"] = it.toString() } stickerId?.let { this["sticker_id"] = it.toString() }
disableMentions?.let { this["disable_mentions"] = it.asInt().toString() } disableMentions?.let { this["disable_mentions"] = it.asInt().toString() }
doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() } doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() }
@@ -65,62 +65,15 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import kotlin.math.abs import kotlin.math.abs
import kotlin.random.Random import kotlin.random.Random
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 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()
}
class MessagesHistoryViewModelImpl( class MessagesHistoryViewModelImpl(
private val applicationContext: Context, private val applicationContext: Context,
private val messagesUseCase: MessagesUseCase, private val messagesUseCase: MessagesUseCase,
@@ -137,6 +90,8 @@ class MessagesHistoryViewModelImpl(
override val dialog = MutableStateFlow<MessageDialog?>(null) override val dialog = MutableStateFlow<MessageDialog?>(null)
override val selectedMessages = MutableStateFlow<List<VkMessage>>(emptyList()) override val selectedMessages = MutableStateFlow<List<VkMessage>>(emptyList())
override val inputFieldFocusRequester = MutableStateFlow(false)
override val isNeedToScrollToIndex = MutableStateFlow<Int?>(null) override val isNeedToScrollToIndex = MutableStateFlow<Int?>(null)
override val baseError = MutableStateFlow<BaseError?>(null) override val baseError = MutableStateFlow<BaseError?>(null)
@@ -154,6 +109,8 @@ class MessagesHistoryViewModelImpl(
private val sendingMessages: MutableList<VkMessage> = mutableListOf() private val sendingMessages: MutableList<VkMessage> = mutableListOf()
private val failedMessages: MutableList<VkMessage> = mutableListOf() private val failedMessages: MutableList<VkMessage> = mutableListOf()
private var replyToCmId: Long? = null
init { init {
val arguments = MessagesHistory.from(savedStateHandle).arguments val arguments = MessagesHistory.from(savedStateHandle).arguments
@@ -268,6 +225,9 @@ class MessagesHistoryViewModelImpl(
override fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) { override fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle) {
when (dialog) { when (dialog) {
is MessageDialog.MessageOptions -> { is MessageDialog.MessageOptions -> {
val messageId = bundle.getLong("messageId")
val cmId = bundle.getLong("cmId")
when (val option = bundle.getParcelableCompat("option", MessageOption::class)) { when (val option = bundle.getParcelableCompat("option", MessageOption::class)) {
null -> Unit null -> Unit
@@ -275,9 +235,30 @@ class MessagesHistoryViewModelImpl(
// TODO: 28-Mar-25, Danil Nikolaev: retry sending // TODO: 28-Mar-25, Danil Nikolaev: retry sending
} }
MessageOption.Reply -> {} MessageOption.Reply -> {
MessageOption.ForwardHere -> {} inputFieldFocusRequester.setValue { true }
MessageOption.Forward -> {} replyToCmId = cmId
screenState.setValue { old ->
val msg = messages.value.find { it.id == messageId }
if (msg == null) {
old
} else {
old.copy(
replyTitle = msg.extractTitle(),
replyText = msg.text
)
}
}
}
MessageOption.ForwardHere -> {
}
MessageOption.Forward -> {
}
MessageOption.Pin -> { MessageOption.Pin -> {
this.dialog.setValue { this.dialog.setValue {
@@ -584,6 +565,17 @@ class MessagesHistoryViewModelImpl(
updateStyles() updateStyles()
} }
override fun onReplyCloseClicked() {
replyToCmId = null
screenState.setValue { old ->
old.copy(
replyTitle = null,
replyText = null
)
}
}
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
val message = event.message val message = event.message
@@ -927,6 +919,7 @@ class MessagesHistoryViewModelImpl(
forwards = null, forwards = null,
attachments = null, attachments = null,
replyMessage = when { replyMessage = when {
replyToCmId != null -> messages.value.find { it.cmId == replyToCmId }
else -> null else -> null
}, },
geoType = null, geoType = null,
@@ -947,15 +940,32 @@ class MessagesHistoryViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
message = TextFieldValue(), message = TextFieldValue(),
actionMode = ActionMode.RECORD_AUDIO actionMode = ActionMode.RECORD_AUDIO,
replyTitle = null,
replyText = null
) )
} }
val replyCmId = replyToCmId
replyToCmId = null
val forward = when {
replyCmId != null -> {
buildJsonObject {
put("peer_id", screenState.value.conversationId)
put("conversation_message_ids", buildJsonArray { add(replyCmId) })
put("is_reply", true)
}.toString()
}
else -> null
}
messagesUseCase.sendMessage( messagesUseCase.sendMessage(
peerId = screenState.value.conversationId, peerId = screenState.value.conversationId,
randomId = newMessage.randomId, randomId = newMessage.randomId,
message = newMessage.text, message = newMessage.text,
replyTo = null, forward = forward,
attachments = null, attachments = null,
formatData = newMessage.formatData formatData = newMessage.formatData
).listenValue(viewModelScope) { state -> ).listenValue(viewModelScope) { state ->
@@ -24,7 +24,9 @@ data class MessagesHistoryScreenState(
val conversation: VkConversation, val conversation: VkConversation,
val pinnedMessage: VkMessage?, val pinnedMessage: VkMessage?,
val pinnedTitle: String?, val pinnedTitle: String?,
val pinnedSummary: AnnotatedString? val pinnedSummary: AnnotatedString?,
val replyTitle: String?,
val replyText: String?
) { ) {
companion object { companion object {
@@ -43,7 +45,9 @@ data class MessagesHistoryScreenState(
conversation = VkConversation.EMPTY, conversation = VkConversation.EMPTY,
pinnedMessage = null, pinnedMessage = null,
pinnedTitle = null, pinnedTitle = null,
pinnedSummary = null pinnedSummary = null,
replyTitle = null,
replyText = null,
) )
} }
} }
@@ -180,7 +180,13 @@ fun MessageOptionsDialog(
onClick = { onClick = {
onDismissed() onDismissed()
val pickedOption = options[index] val pickedOption = options[index]
onItemPicked(bundleOf("option" to pickedOption)) onItemPicked(
bundleOf(
"option" to pickedOption,
"messageId" to message.id,
"cmId" to message.cmId
)
)
} }
) )
} }
@@ -1,7 +1,9 @@
package dev.meloda.fast.messageshistory.presentation package dev.meloda.fast.messageshistory.presentation
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
@@ -32,10 +34,14 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
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.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -54,9 +60,9 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.messageshistory.model.ActionMode import dev.meloda.fast.messageshistory.model.ActionMode
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.IconButton import dev.meloda.fast.ui.components.IconButton
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.R
@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class) @OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class)
@Composable @Composable
@@ -68,6 +74,9 @@ fun MessagesHistoryInputBar(
showEmojiButton: Boolean, showEmojiButton: Boolean,
showAttachmentButton: Boolean, showAttachmentButton: Boolean,
actionMode: ActionMode, actionMode: ActionMode,
replyTitle: String?,
replyText: String?,
inputFieldFocusRequester: Boolean,
onMessageInputChanged: (TextFieldValue) -> Unit = {}, onMessageInputChanged: (TextFieldValue) -> Unit = {},
onBoldRequested: () -> Unit = {}, onBoldRequested: () -> Unit = {},
onItalicRequested: () -> Unit = {}, onItalicRequested: () -> Unit = {},
@@ -77,16 +86,28 @@ fun MessagesHistoryInputBar(
onSetMessageBarHeight: (Dp) -> Unit = {}, onSetMessageBarHeight: (Dp) -> Unit = {},
onEmojiButtonLongClicked: () -> Unit = {}, onEmojiButtonLongClicked: () -> Unit = {},
onAttachmentButtonClicked: () -> Unit = {}, onAttachmentButtonClicked: () -> Unit = {},
onActionButtonClicked: () -> Unit = {} onActionButtonClicked: () -> Unit = {},
onReplyCloseClicked: () -> Unit = {}
) { ) {
val view = LocalView.current val view = LocalView.current
val context = LocalContext.current val context = LocalContext.current
val density = LocalDensity.current val density = LocalDensity.current
val scope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() }
LaunchedEffect(inputFieldFocusRequester) {
if (inputFieldFocusRequester) {
focusRequester.requestFocus()
}
}
val theme = LocalThemeConfig.current val theme = LocalThemeConfig.current
val inputBarTopCornerRadius by animateDpAsState(
targetValue = if (replyTitle == null) 24.dp else 0.dp,
label = "inputBarTopCornerRadius"
)
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -95,6 +116,14 @@ fun MessagesHistoryInputBar(
.navigationBarsPadding() .navigationBarsPadding()
.imePadding() .imePadding()
) { ) {
AnimatedVisibility(replyTitle != null) {
ReplyContainer(
title = replyTitle.orEmpty(),
text = replyText.orEmpty(),
onCloseClicked = onReplyCloseClicked,
)
}
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -102,10 +131,17 @@ fun MessagesHistoryInputBar(
.imeNestedScroll(), .imeNestedScroll(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(8.dp))
Row( Row(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(36.dp)) .clip(
RoundedCornerShape(
topStart = inputBarTopCornerRadius,
topEnd = inputBarTopCornerRadius,
bottomStart = 24.dp,
bottomEnd = 24.dp
)
)
.then( .then(
if (theme.enableBlur) { if (theme.enableBlur) {
Modifier Modifier
@@ -166,6 +202,7 @@ fun MessagesHistoryInputBar(
TextField( TextField(
modifier = Modifier modifier = Modifier
.focusRequester(focusRequester)
.weight(1f) .weight(1f)
.appendTextContextMenuComponents { .appendTextContextMenuComponents {
separator() separator()
@@ -287,7 +324,7 @@ fun MessagesHistoryInputBar(
Spacer(modifier = Modifier.width(6.dp)) Spacer(modifier = Modifier.width(6.dp))
} }
Spacer(modifier = Modifier.width(10.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
} }
} }
@@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.messageshistory.MessagesHistoryViewModel import dev.meloda.fast.messageshistory.MessagesHistoryViewModel
import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl
@@ -30,6 +29,7 @@ fun MessagesHistoryRoute(
val baseError by viewModel.baseError.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle() val scrollIndex by viewModel.isNeedToScrollToIndex.collectAsStateWithLifecycle()
val inputFieldFocusRequester by viewModel.inputFieldFocusRequester.collectAsStateWithLifecycle()
LaunchedEffect(navigationEvent) { LaunchedEffect(navigationEvent) {
val needToConsume = when (val navigation = navigationEvent) { val needToConsume = when (val navigation = navigationEvent) {
@@ -56,6 +56,7 @@ fun MessagesHistoryRoute(
showEmojiButton = AppSettings.General.showEmojiButton, showEmojiButton = AppSettings.General.showEmojiButton,
showAttachmentButton = AppSettings.General.showAttachmentButton, showAttachmentButton = AppSettings.General.showAttachmentButton,
enableHaptic = AppSettings.General.enableHaptic, enableHaptic = AppSettings.General.enableHaptic,
inputFieldFocusRequester = inputFieldFocusRequester,
onBack = onBack, onBack = onBack,
onClose = viewModel::onCloseButtonClicked, onClose = viewModel::onCloseButtonClicked,
onScrolledToIndex = viewModel::onScrolledToIndex, onScrolledToIndex = viewModel::onScrolledToIndex,
@@ -77,7 +78,8 @@ fun MessagesHistoryRoute(
onItalicRequested = viewModel::onItalicClicked, onItalicRequested = viewModel::onItalicClicked,
onUnderlineRequested = viewModel::onUnderlineClicked, onUnderlineRequested = viewModel::onUnderlineClicked,
onLinkRequested = viewModel::onLinkClicked, onLinkRequested = viewModel::onLinkClicked,
onRegularRequested = viewModel::onRegularClicked onRegularRequested = viewModel::onRegularClicked,
onReplyCloseClicked = viewModel::onReplyCloseClicked
) )
HandleDialogs( HandleDialogs(
@@ -44,13 +44,13 @@ import dev.meloda.fast.messageshistory.model.UiItem
import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.Loader import dev.meloda.fast.ui.components.Loader
import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.components.VkErrorView
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
import dev.meloda.fast.ui.util.emptyImmutableList import dev.meloda.fast.ui.util.emptyImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import dev.meloda.fast.ui.R
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
@@ -70,6 +70,7 @@ fun MessagesHistoryScreen(
showEmojiButton: Boolean = false, showEmojiButton: Boolean = false,
showAttachmentButton: Boolean = false, showAttachmentButton: Boolean = false,
enableHaptic: Boolean = false, enableHaptic: Boolean = false,
inputFieldFocusRequester: Boolean,
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onClose: () -> Unit = {}, onClose: () -> Unit = {},
onScrolledToIndex: () -> Unit = {}, onScrolledToIndex: () -> Unit = {},
@@ -91,7 +92,8 @@ fun MessagesHistoryScreen(
onItalicRequested: () -> Unit = {}, onItalicRequested: () -> Unit = {},
onLinkRequested: () -> Unit = {}, onLinkRequested: () -> Unit = {},
onUnderlineRequested: () -> Unit = {}, onUnderlineRequested: () -> Unit = {},
onRegularRequested: () -> Unit = {} onRegularRequested: () -> Unit = {},
onReplyCloseClicked: () -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val view = LocalView.current val view = LocalView.current
@@ -215,6 +217,7 @@ fun MessagesHistoryScreen(
uiMessages = uiMessages, uiMessages = uiMessages,
isSelectedAtLeastOne = isSelectedAtLeastOne, isSelectedAtLeastOne = isSelectedAtLeastOne,
isPaginating = screenState.isPaginating, isPaginating = screenState.isPaginating,
isReplying = screenState.replyTitle != null,
messageBarHeight = messageBarHeight, messageBarHeight = messageBarHeight,
onRequestScrollToCmId = { cmId -> onRequestScrollToCmId = { cmId ->
val index = uiMessages.values.indexOfMessageByCmId(cmId) val index = uiMessages.values.indexOfMessageByCmId(cmId)
@@ -252,10 +255,14 @@ fun MessagesHistoryScreen(
showEmojiButton = showEmojiButton, showEmojiButton = showEmojiButton,
showAttachmentButton = showAttachmentButton, showAttachmentButton = showAttachmentButton,
actionMode = screenState.actionMode, actionMode = screenState.actionMode,
replyTitle = screenState.replyTitle,
replyText = screenState.replyText,
inputFieldFocusRequester = inputFieldFocusRequester,
onSetMessageBarHeight = { messageBarHeight = it }, onSetMessageBarHeight = { messageBarHeight = it },
onEmojiButtonLongClicked = onEmojiButtonLongClicked, onEmojiButtonLongClicked = onEmojiButtonLongClicked,
onAttachmentButtonClicked = onAttachmentButtonClicked, onAttachmentButtonClicked = onAttachmentButtonClicked,
onActionButtonClicked = onActionButtonClicked onActionButtonClicked = onActionButtonClicked,
onReplyCloseClicked = onReplyCloseClicked
) )
when { when {
@@ -53,6 +53,7 @@ fun MessagesList(
uiMessages: ImmutableList<UiItem>, uiMessages: ImmutableList<UiItem>,
isSelectedAtLeastOne: Boolean, isSelectedAtLeastOne: Boolean,
isPaginating: Boolean, isPaginating: Boolean,
isReplying: Boolean,
messageBarHeight: Dp, messageBarHeight: Dp,
onRequestScrollToCmId: (cmId: Long) -> Unit = {}, onRequestScrollToCmId: (cmId: Long) -> Unit = {},
onMessageClicked: (Long) -> Unit = {}, onMessageClicked: (Long) -> Unit = {},
@@ -132,6 +133,10 @@ fun MessagesList(
reverseLayout = true reverseLayout = true
) { ) {
item { item {
AnimatedVisibility(isReplying) {
Spacer(modifier = Modifier.height(48.dp))
}
Spacer(modifier = Modifier.height(messageBarHeight.plus(18.dp))) Spacer(modifier = Modifier.height(messageBarHeight.plus(18.dp)))
Spacer( Spacer(
modifier = Modifier modifier = Modifier