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