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 8d3f901e78..2e689f24cd 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 @@ -259,16 +259,13 @@ 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) + fun addLiveDummy(chatInfo: ChatInfo): ChatItem { + val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) chatItems.add(cItem) return cItem } - fun removeLiveChatItemDummy() { + fun removeLiveDummy() { if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { chatItems.removeLast() } @@ -1320,7 +1317,7 @@ data class ChatItem ( file = null ) - fun liveChatItemDummy(direct: Boolean, quoted: CIQuote?): ChatItem = ChatItem( + fun liveDummy(direct: Boolean): ChatItem = ChatItem( chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(), meta = CIMeta( itemId = TEMP_LIVE_CHAT_ITEM_ID, @@ -1336,7 +1333,7 @@ data class ChatItem ( editable = false ), content = CIContent.SndMsgContent(MsgContent.MCText("")), - quotedItem = quoted, + quotedItem = null, file = null ) 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 0409046066..7d712a8a7b 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,7 +41,6 @@ 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 @@ -105,6 +104,9 @@ data class ComposeState( } hasContent && !inProgress } + val endLiveDisabled: Boolean + get() = liveMessage != null && message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem + val linkPreviewAllowed: Boolean get() = when (preview) { @@ -354,7 +356,7 @@ fun ComposeView( chosenContent.value = emptyList() chosenAudio.value = null chosenFile.value = null - chatModel.removeLiveChatItemDummy() + chatModel.removeLiveDummy() } suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? { @@ -572,16 +574,16 @@ fun ComposeView( } suspend fun sendLiveMessage() { - val typedMsg = composeState.value.message - val sentMsg = truncateToWords(typedMsg) - if (sentMsg.isNotEmpty() && (composeState.value.liveMessage == null || composeState.value.liveMessage?.sent == false)) { - val ci = sendMessageAsync(sentMsg, live = true) + val cs = composeState.value + val typedMsg = cs.message + if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) { + val ci = sendMessageAsync(typedMsg, live = true) if (ci != null) { - composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true)) + composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, 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)) + } else if (cs.liveMessage == null) { + val cItem = chatModel.addLiveDummy(chat.chatInfo) + composeState.value = composeState.value.copy(liveMessage = LiveMessage(cItem, typedMsg = typedMsg, sentMsg = typedMsg, sent = false)) } } @@ -702,16 +704,6 @@ 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) { @@ -723,7 +715,7 @@ fun ComposeView( sendMessage() resetLinkPreview() } - chatModel.removeLiveChatItemDummy() + chatModel.removeLiveDummy() } } } @@ -745,7 +737,7 @@ fun ComposeView( updateLiveMessage = ::updateLiveMessage, cancelLiveMessage = { composeState.value = composeState.value.copy(liveMessage = null) - chatModel.removeLiveChatItemDummy() + chatModel.removeLiveDummy() }, 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 5063233342..e84af43f0b 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 @@ -109,7 +109,10 @@ fun SendMsgView( else -> RecordVoiceView(recState, stopRecOnNextClick) } - if (sendLiveMessage != null && updateLiveMessage != null && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)) { + if (sendLiveMessage != null + && updateLiveMessage != null + && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value) + && cs.contextItem is ComposeContextItem.NoContextItem) { Spacer(Modifier.width(10.dp)) StartLiveMessageButton { if (composeState.value.preview is ComposePreview.NoPreview) { @@ -125,14 +128,18 @@ fun SendMsgView( } } else -> { + val cs = composeState.value 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 - if (composeState.value.liveMessage == null && + val disabled = !cs.sendEnabled() || + (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) || + cs.endLiveDisabled + if (cs.liveMessage == null && cs.preview !is ComposePreview.VoicePreview && !cs.editing && + cs.contextItem is ComposeContextItem.NoContextItem && sendLiveMessage != null && updateLiveMessage != null ) { var showDropdown by rememberSaveable { mutableStateOf(false) } - SendMsgButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled(), sendMessage) { showDropdown = true } + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true } DropdownMenu( expanded = showDropdown, @@ -149,7 +156,7 @@ fun SendMsgView( ) } } else { - SendMsgButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled(), sendMessage) + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) } } } @@ -396,7 +403,6 @@ private fun CancelLiveMessageButton( @Composable private fun SendMsgButton( icon: ImageVector, - backgroundColor: Color, sizeDp: Animatable, alpha: Animatable, enabled: Boolean, @@ -425,7 +431,7 @@ private fun SendMsgButton( .padding(4.dp) .alpha(alpha.value) .clip(CircleShape) - .background(backgroundColor) + .background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight) .padding(3.dp) ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 1eb4f21cee..4749390646 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -54,6 +54,7 @@ fun ChatItemView( val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) } val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file) val onLinkLongClick = { _: String -> showMenu.value = true } + val live = composeState.value.liveMessage != null Box( modifier = Modifier @@ -97,7 +98,7 @@ fun ChatItemView( onDismissRequest = { showMenu.value = false }, Modifier.width(220.dp) ) { - if (!cItem.meta.itemDeleted) { + if (!cItem.meta.itemDeleted && !live) { ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) @@ -133,7 +134,7 @@ fun ChatItemView( }) } } - if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice) { + if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) { ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = { composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews) showMenu.value = false @@ -149,7 +150,9 @@ fun ChatItemView( } ) } - DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) + if (!(live && cItem.meta.isLive)) { + DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) + } } } diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index ef3a6f8f32..5a15c4946a 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -283,12 +283,8 @@ final class ChatModel: ObservableObject { return nil } - func addLiveDummy(_ quotedCItem: ChatItem?, _ chatInfo: ChatInfo) -> ChatItem { - var quoted: CIQuote? = nil - if let quotedItem = quotedCItem, let msgContent = quotedItem.content.msgContent { - quoted = CIQuote.getSampleWithMsgContent(itemId: quotedItem.id, sentAt: quotedItem.meta.updatedAt, msgContent: msgContent, chatDir: quotedItem.chatDir) - } - let cItem = ChatItem.liveChatItemDummy(chatInfo.chatType, quoted) + func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem { + let cItem = ChatItem.liveDummy(chatInfo.chatType) withAnimation { reversedChatItems.insert(cItem, at: 0) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index ab8297cfc8..5ca4627917 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -96,6 +96,13 @@ struct ComposeState { } } + var quoting: Bool { + switch contextItem { + case .quotedItem: return true + default: return false + } + } + var sendEnabled: Bool { switch preview { case .imagePreviews: return true @@ -105,6 +112,10 @@ struct ComposeState { } } + var endLiveDisabled: Bool { + liveMessage != nil && message.isEmpty && noPreview && !quoting + } + var linkPreviewAllowed: Bool { switch preview { case .imagePreviews: return false @@ -400,18 +411,15 @@ struct ComposeView: View { private func sendLiveMessage() async { let typedMsg = composeState.message - let sentMsg = truncateToWords(typedMsg) - if !sentMsg.isEmpty && (composeState.liveMessage == nil || composeState.liveMessage?.sentMsg == nil), - let ci = await sendMessageAsync(sentMsg, live: true) { + let lm = composeState.liveMessage + if (composeState.sendEnabled || composeState.quoting) + && (lm == nil || lm?.sentMsg == nil), + let ci = await sendMessageAsync(typedMsg, live: true) { await MainActor.run { - composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: sentMsg)) + composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: typedMsg)) } - } else if composeState.liveMessage == nil { - var quoted: ChatItem? = nil - if case let .quotedItem(item) = composeState.contextItem { - quoted = item - } - let cItem = chatModel.addLiveDummy(quoted, chat.chatInfo) + } else if lm == nil { + let cItem = chatModel.addLiveDummy(chat.chatInfo) await MainActor.run { composeState = composeState.copy(liveMessage: LiveMessage(chatItem: cItem, typedMsg: typedMsg, sentMsg: nil)) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 5e85140a05..b5f8e9b5fd 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -100,7 +100,9 @@ struct SendMessageView: View { } else { voiceMessageNotAllowedButton() } - if let send = sendLiveMessage, let update = updateLiveMessage { + if let send = sendLiveMessage, + let update = updateLiveMessage, + case .noContextItem = composeState.contextItem { startLiveMessageButton(send: send, update: update) } } @@ -137,11 +139,12 @@ struct SendMessageView: View { !composeState.sendEnabled || composeState.disabled || (!voiceMessageAllowed && composeState.voicePreview) || - (composeState.liveMessage != nil && composeState.message.isEmpty) + composeState.endLiveDisabled ) .frame(width: 29, height: 29) if composeState.liveMessage == nil, + case .noContextItem = composeState.contextItem, !composeState.voicePreview && !composeState.editing, let send = sendLiveMessage, let update = updateLiveMessage { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 9f46ba3912..dae0bbe08d 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1863,7 +1863,7 @@ public struct ChatItem: Identifiable, Decodable { ) } - public static func liveChatItemDummy(_ chatType: ChatType, _ quoted: CIQuote?) -> ChatItem { + public static func liveDummy(_ chatType: ChatType) -> ChatItem { var item = ChatItem( chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd, meta: CIMeta( @@ -1879,7 +1879,7 @@ public struct ChatItem: Identifiable, Decodable { editable: false ), content: .sndMsgContent(msgContent: .text("")), - quotedItem: quoted, + quotedItem: nil, file: nil ) item.isLiveDummy = true @@ -2135,10 +2135,6 @@ public struct CIQuote: Decodable, ItemContent { } return CIQuote(chatDir: chatDir, itemId: itemId, sentAt: sentAt, content: mc) } - - public static func getSampleWithMsgContent(itemId: Int64?, sentAt: Date, msgContent: MsgContent, chatDir: CIDirection) -> CIQuote { - return CIQuote(chatDir: chatDir, itemId: itemId, sentAt: sentAt, content: msgContent) - } } public struct CIFile: Decodable {