mobile: send live message when there is any content (#1721)

* ios: send live message when there is any content

* android: improve live message logic

* fix, refactor

* prohibit live messages with quotes
This commit is contained in:
Evgeny Poberezkin
2023-01-11 12:01:02 +00:00
committed by GitHub
parent 9e3573fc76
commit a6d7604d21
8 changed files with 65 additions and 64 deletions

View File

@@ -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
)

View File

@@ -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

View File

@@ -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<Float, AnimationVector1D>,
alpha: Animatable<Float, AnimationVector1D>,
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)
)
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -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 {