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:
@@ -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>
|
||||||
|
|||||||
+2
-2
@@ -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() }
|
||||||
|
|||||||
+66
-56
@@ -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 ->
|
||||||
+6
-2
@@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-7
@@ -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
-2
@@ -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(
|
||||||
|
|||||||
+10
-3
@@ -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 {
|
||||||
|
|||||||
+5
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user