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:
committed by
GitHub
parent
9e3573fc76
commit
a6d7604d21
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user