From 5a7d61c96423d23154034ccee6176f3211b18cf7 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Mon, 9 Jan 2023 18:30:26 +0000 Subject: [PATCH] android: Live messages without sending an empty text (#1709) * android: Live messages without sending an empty text * Better quoted messages handling * Do not add item into preview * Change * Changes --- .../java/chat/simplex/app/model/ChatModel.kt | 44 ++++++++++++++++++- .../chat/simplex/app/views/TerminalView.kt | 4 +- .../chat/simplex/app/views/chat/ChatView.kt | 2 +- .../simplex/app/views/chat/ComposeView.kt | 40 +++++++++++++---- .../simplex/app/views/chat/SendMsgView.kt | 27 ++++++++++-- .../app/src/main/res/values/strings.xml | 1 + 6 files changed, 102 insertions(+), 16 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index e3dcc4911b..8d3f901e78 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -180,7 +180,11 @@ class ChatModel(val controller: ChatController) { } // add to current chat if (chatId.value == cInfo.id) { - chatItems.add(cItem) + if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem) + } else { + chatItems.add(cItem) + } } } @@ -255,6 +259,21 @@ class ChatModel(val controller: ChatController) { } } + fun addLiveChatItemDummy(quotedCItem: ChatItem?, chatInfo: ChatInfo): ChatItem { + val quoted = if (quotedCItem?.content?.msgContent != null) { + CIQuote(chatDir = quotedCItem.chatDir, itemId = quotedCItem.id, sentAt = quotedCItem.meta.createdAt, content = quotedCItem.content.msgContent!!) + } else null + val cItem = ChatItem.liveChatItemDummy(chatInfo is ChatInfo.Direct, quoted) + chatItems.add(cItem) + return cItem + } + + fun removeLiveChatItemDummy() { + if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + chatItems.removeLast() + } + } + fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { val markedRead = markItemsReadInCurrentChat(cInfo, range) // update preview @@ -1278,7 +1297,8 @@ data class ChatItem ( } private const val TEMP_DELETED_CHAT_ITEM_ID = -1L - + const val TEMP_LIVE_CHAT_ITEM_ID = -2L + val deletedItemDummy: ChatItem get() = ChatItem( chatDir = CIDirection.DirectRcv(), @@ -1300,6 +1320,26 @@ data class ChatItem ( file = null ) + fun liveChatItemDummy(direct: Boolean, quoted: CIQuote?): ChatItem = ChatItem( + chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(), + meta = CIMeta( + itemId = TEMP_LIVE_CHAT_ITEM_ID, + itemTs = Clock.System.now(), + itemText = "", + itemStatus = CIStatus.RcvRead(), + createdAt = Clock.System.now(), + updatedAt = Clock.System.now(), + itemDeleted = false, + itemEdited = false, + itemTimed = null, + itemLive = true, + editable = false + ), + content = CIContent.SndMsgContent(MsgContent.MCText("")), + quotedItem = quoted, + file = null + ) + fun invalidJSON(json: String): ChatItem = ChatItem( chatDir = CIDirection.DirectSnd(), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index acfb205d38..feed70ce75 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -147,8 +147,8 @@ fun TerminalLayout( sendMessage = sendCommand, sendLiveMessage = null, updateLiveMessage = null, - ::onMessageChange, - textStyle + onMessageChange = ::onMessageChange, + textStyle = textStyle ) } }, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index e774bb9420..7d15b5ff67 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -568,7 +568,7 @@ fun BoxWithConstraintsScope.ChatItemsList( scope.launch { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) - } else { + } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem)) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt index e4581931f7..0409046066 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ComposeView.kt @@ -41,6 +41,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.views.chat.item.* import chat.simplex.app.views.helpers.* import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable @@ -67,7 +68,8 @@ sealed class ComposeContextItem { data class LiveMessage( val chatItem: ChatItem, val typedMsg: String, - val sentMsg: String + val sentMsg: String, + val sent: Boolean ) @Serializable @@ -352,6 +354,7 @@ fun ComposeView( chosenContent.value = emptyList() chosenAudio.value = null chosenFile.value = null + chatModel.removeLiveChatItemDummy() } suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? { @@ -430,7 +433,7 @@ fun ComposeView( if (cs.contextItem is ComposeContextItem.EditingItem) { val ei = cs.contextItem.chatItem sent = updateMessage(ei, cInfo, live) - } else if (liveMessage != null) { + } else if (liveMessage != null && liveMessage.sent) { sent = updateMessage(liveMessage.chatItem, cInfo, live) } else { val msgs: ArrayList = ArrayList() @@ -571,11 +574,14 @@ fun ComposeView( suspend fun sendLiveMessage() { val typedMsg = composeState.value.message val sentMsg = truncateToWords(typedMsg) - if (composeState.value.liveMessage == null) { + if (sentMsg.isNotEmpty() && (composeState.value.liveMessage == null || composeState.value.liveMessage?.sent == false)) { val ci = sendMessageAsync(sentMsg, live = true) if (ci != null) { - composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg)) + composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true)) } + } else if (composeState.value.liveMessage == null) { + val cItem = chatModel.addLiveChatItemDummy((composeState.value.contextItem as? ComposeContextItem.QuotedItem)?.chatItem, chat.chatInfo) + composeState.value = composeState.value.copy(liveMessage = LiveMessage(cItem, typedMsg = typedMsg, sentMsg = sentMsg, sent = false)) } } @@ -592,7 +598,7 @@ fun ComposeView( if (sentMsg != null) { val ci = sendMessageAsync(sentMsg, live = true) if (ci != null) { - composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg)) + composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true)) } } else if (liveMessage.typedMsg != typedMsg) { composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg)) @@ -696,14 +702,28 @@ fun ComposeView( } } } + LaunchedEffect(Unit) { + snapshotFlow { composeState.value.contextItem } + .distinctUntilChanged() + .collect { + if (composeState.value.liveMessage?.sent == false) { + chatModel.removeLiveChatItemDummy() + chatModel.addLiveChatItemDummy((it as? ComposeContextItem.QuotedItem)?.chatItem, chat.chatInfo) + } + } + } val activity = LocalContext.current as Activity DisposableEffect(Unit) { val orientation = activity.resources.configuration.orientation onDispose { - if (orientation == activity.resources.configuration.orientation && composeState.value.liveMessage != null) { - sendMessage() - resetLinkPreview() + if (orientation == activity.resources.configuration.orientation) { + val cs = composeState.value + if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) { + sendMessage() + resetLinkPreview() + } + chatModel.removeLiveChatItemDummy() } } } @@ -723,6 +743,10 @@ fun ComposeView( }, sendLiveMessage = ::sendLiveMessage, updateLiveMessage = ::updateLiveMessage, + cancelLiveMessage = { + composeState.value = composeState.value.copy(liveMessage = null) + chatModel.removeLiveChatItemDummy() + }, onMessageChange = ::onMessageChange, textStyle = textStyle ) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index 1bf9b70383..379cc36b44 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.* import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat.getSystemService import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat @@ -65,6 +64,7 @@ fun SendMsgView( sendMessage: () -> Unit, sendLiveMessage: ( suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, + cancelLiveMessage: (() -> Unit)? = null, onMessageChange: (String) -> Unit, textStyle: MutableState ) { @@ -119,9 +119,14 @@ fun SendMsgView( } } } + cs.liveMessage?.sent == false && cs.message.isEmpty() -> { + CancelLiveMessageButton { + cancelLiveMessage?.invoke() + } + } else -> { val icon = if (cs.editing || cs.liveMessage != null) Icons.Filled.Check else Icons.Outlined.ArrowUpward - val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight + val color = if (cs.sendEnabled() && cs.message.isNotEmpty()) MaterialTheme.colors.primary else HighOrLowlight if (composeState.value.liveMessage == null && cs.preview !is ComposePreview.VoicePreview && !cs.editing && sendLiveMessage != null && updateLiveMessage != null @@ -144,7 +149,7 @@ fun SendMsgView( ) } } else { - SendTextButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled(), sendMessage) + SendTextButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled() && cs.message.isNotEmpty(), sendMessage) } } } @@ -373,6 +378,22 @@ private fun ProgressIndicator() { CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp) } +@Composable +private fun CancelLiveMessageButton( + onClick: () -> Unit +) { + IconButton(onClick, Modifier.size(36.dp)) { + Icon( + Icons.Filled.Close, + stringResource(R.string.icon_descr_cancel_live_message), + tint = MaterialTheme.colors.primary, + modifier = Modifier + .size(36.dp) + .padding(4.dp) + ) + } +} + @Composable private fun SendTextButton( icon: ImageVector, diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 7fd343c223..e40198c80a 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -271,6 +271,7 @@ Live message! Send a live message - it will update for the recipient(s) as you type it Send + Cancel live message Back