From 0b45ddfc796655f1a22c8776fc1e2ee7196e92c4 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Fri, 25 Mar 2022 22:26:05 +0400 Subject: [PATCH] mobile: message update (restore #460) (#469) --- .../java/chat/simplex/app/model/ChatModel.kt | 86 +++++++++++-------- .../java/chat/simplex/app/model/SimpleXAPI.kt | 54 ++++++++++-- .../chat/simplex/app/views/TerminalView.kt | 8 +- .../chat/simplex/app/views/chat/ChatView.kt | 77 ++++++++++++----- .../simplex/app/views/chat/ComposeView.kt | 25 ++++-- .../{QuotedItemView.kt => ContextItemView.kt} | 51 +++++++---- .../simplex/app/views/chat/SendMsgView.kt | 38 +++++--- .../simplex/app/views/chat/item/CIMetaView.kt | 41 +++++++-- .../app/views/chat/item/ChatItemView.kt | 32 +++++-- .../app/views/chat/item/FramedItemView.kt | 35 +++++--- .../app/views/chat/item/TextItemView.kt | 6 +- apps/ios/Shared/Model/ChatModel.swift | 14 ++- apps/ios/Shared/Model/SimpleXAPI.swift | 44 +++++++++- .../Views/Chat/ChatItem/CIMetaView.swift | 13 ++- .../Views/Chat/ChatItem/FramedItemView.swift | 24 +++++- .../Views/Chat/ChatItem/MsgContentView.swift | 14 +-- apps/ios/Shared/Views/Chat/ChatView.swift | 52 ++++++++--- .../Chat/ComposeMessage/ComposeView.swift | 52 +++++++++-- .../Chat/ComposeMessage/ContextItemView.swift | 55 ++++++++++++ .../Chat/ComposeMessage/QuotedItemView.swift | 49 ----------- .../Chat/ComposeMessage/SendMessageView.swift | 38 ++++++-- apps/ios/Shared/Views/TerminalView.swift | 6 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 12 +-- src/Simplex/Chat/Store.hs | 4 +- 24 files changed, 599 insertions(+), 231 deletions(-) rename apps/android/app/src/main/java/chat/simplex/app/views/chat/{QuotedItemView.kt => ContextItemView.kt} (60%) create mode 100644 apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift delete mode 100644 apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift 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 2a67c09296..7bdf80eede 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 @@ -102,6 +102,37 @@ class ChatModel(val controller: ChatController) { } } + fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean { + // update previews + val i = getChatIndex(cInfo.id) + val chat: Chat + val res: Boolean + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.last() + if (pItem.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(cItem)) + } + res = false + } else { + addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem))) + res = true + } + // update current chat + if (chatId.value == cInfo.id) { + val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + chatItems[itemIndex] = cItem + return false + } else { + chatItems.add(cItem) + return true + } + } else { + return res + } + } + fun markChatItemsRead(cInfo: ChatInfo) { val chatIdx = getChatIndex(cInfo.id) // update current chat @@ -122,42 +153,13 @@ class ChatModel(val controller: ChatController) { } } -// -// func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { -// // update previews -// var res: Bool -// if let chat = getChat(cInfo.id) { -// if let pItem = chat.chatItems.last, pItem.id == cItem.id { -// chat.chatItems = [cItem] -// } -// res = false -// } else { -// addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) -// res = true -// } -// // update current chat -// if chatId == cInfo.id { -// if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) { -// withAnimation(.default) { -// self.chatItems[i] = cItem -// } -// return false -// } else { -// withAnimation { chatItems.append(cItem) } -// return true -// } -// } else { -// return res -// } -// } -// -// + // func popChat(_ id: String) { // if let i = getChatIndex(id) { // popChat_(i) // } // } -// + private fun popChat_(i: Int) { val chat = chats.removeAt(i) chats.add(index = 0, chat) @@ -494,11 +496,14 @@ data class ChatItem ( ts: Instant = Clock.System.now(), text: String = "hello\nthere", status: CIStatus = CIStatus.SndNew(), - quotedItem: CIQuote? = null + quotedItem: CIQuote? = null, + itemDeleted: Boolean = false, + itemEdited: Boolean = false, + editable: Boolean = true ) = ChatItem( chatDir = dir, - meta = CIMeta.getSample(id, ts, text, status), + meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable), content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), quotedItem = quotedItem ) @@ -536,18 +541,27 @@ data class CIMeta ( val itemTs: Instant, val itemText: String, val itemStatus: CIStatus, - val createdAt: Instant + val createdAt: Instant, + val itemDeleted: Boolean, + val itemEdited: Boolean, + val editable: Boolean ) { val timestampText: String get() = getTimestampText(itemTs) companion object { - fun getSample(id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta = + fun getSample( + id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), + itemDeleted: Boolean = false, itemEdited: Boolean = false, editable: Boolean = true + ): CIMeta = CIMeta( itemId = id, itemTs = ts, itemText = text, itemStatus = status, - createdAt = ts + createdAt = ts, + itemDeleted = itemDeleted, + itemEdited = itemEdited, + editable = editable ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index b8611633b1..c2ce6ad329 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -131,6 +131,20 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap return null } + suspend fun apiUpdateMessage(type: ChatType, id: Long, itemId: Long, mc: MsgContent): AChatItem? { + val r = sendCmd(CC.ApiUpdateMessage(type, id, itemId, mc)) + if (r is CR.ChatItemUpdated) return r.chatItem + Log.e(TAG, "apiUpdateMessage bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiDeleteMessage(type: ChatType, id: Long, itemId: Long, mode: MsgDeleteMode): AChatItem? { + val r = sendCmd(CC.ApiDeleteMessage(type, id, itemId, mode)) + if (r is CR.ChatItemDeleted) return r.chatItem + Log.e(TAG, "apiDeleteMessage bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun getUserSMPServers(): List? { val r = sendCmd(CC.GetUserSMPServers()) if (r is CR.UserSMPServers) return r.smpServers @@ -303,12 +317,23 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap ntfManager.notifyMessageReceived(cInfo, cItem) } } -// case let .chatItemUpdated(aChatItem): - // let cInfo = aChatItem.chatInfo - // let cItem = aChatItem.chatItem - // if chatModel.upsertChatItem(cInfo, cItem) { - // NtfManager.shared.notifyMessageReceived(cInfo, cItem) - // } + is CR.ChatItemStatusUpdated -> { + val cInfo = r.chatItem.chatInfo + val cItem = r.chatItem.chatItem + if (chatModel.upsertChatItem(cInfo, cItem)) { + ntfManager.notifyMessageReceived(cInfo, cItem) + } + } + is CR.ChatItemUpdated -> { + val cInfo = r.chatItem.chatInfo + val cItem = r.chatItem.chatItem + if (chatModel.upsertChatItem(cInfo, cItem)) { + ntfManager.notifyMessageReceived(cInfo, cItem) + } + } + is CR.ChatItemDeleted -> { + // TODO + } else -> Log.d(TAG , "unsupported event: ${r.responseType}") } @@ -336,6 +361,11 @@ open class ChatController(val ctrl: ChatCtrl, val ntfManager: NtfManager, val ap } } +enum class MsgDeleteMode(val mode: String) { + Broadcast("broadcast"), + Internal("internal"); +} + // ChatCommand sealed class CC { class Console(val cmd: String): CC() @@ -346,6 +376,8 @@ sealed class CC { class ApiGetChat(val type: ChatType, val id: Long): CC() class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC() class ApiSendMessageQuote(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC() + class ApiUpdateMessage(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC() + class ApiDeleteMessage(val type: ChatType, val id: Long, val itemId: Long, val mode: MsgDeleteMode): CC() class GetUserSMPServers(): CC() class SetUserSMPServers(val smpServers: List): CC() class AddContact: CC() @@ -368,6 +400,8 @@ sealed class CC { is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100" is ApiSendMessage -> "/_send ${chatRef(type, id)} ${mc.cmdString}" is ApiSendMessageQuote -> "/_send_quote ${chatRef(type, id)} $itemId ${mc.cmdString}" + is ApiUpdateMessage -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}" + is ApiDeleteMessage -> "/_delete item ${chatRef(type, id)} $itemId $mode" is GetUserSMPServers -> "/smp_servers" is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}" is AddContact -> "/connect" @@ -391,6 +425,8 @@ sealed class CC { is ApiGetChat -> "apiGetChat" is ApiSendMessage -> "apiSendMessage" is ApiSendMessageQuote -> "apiSendMessageQuote" + is ApiUpdateMessage -> "apiUpdateMessage" + is ApiDeleteMessage -> "apiDeleteMessage" is GetUserSMPServers -> "getUserSMPServers" is SetUserSMPServers -> "setUserSMPServers" is AddContact -> "addContact" @@ -474,7 +510,9 @@ sealed class CR { @Serializable @SerialName("groupEmpty") class GroupEmpty(val group: GroupInfo): CR() @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() @Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val chatItem: AChatItem): CR() @Serializable @SerialName("cmdOk") class CmdOk: CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val chatError: ChatError): CR() @Serializable @SerialName("chatError") class ChatRespError(val chatError: ChatError): CR() @@ -512,7 +550,9 @@ sealed class CR { is GroupEmpty -> "groupEmpty" is UserContactLinkSubscribed -> "userContactLinkSubscribed" is NewChatItem -> "newChatItem" + is ChatItemStatusUpdated -> "chatItemStatusUpdated" is ChatItemUpdated -> "chatItemUpdated" + is ChatItemDeleted -> "chatItemDeleted" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" @@ -551,7 +591,9 @@ sealed class CR { is GroupEmpty -> json.encodeToString(group) is UserContactLinkSubscribed -> noDetails() is NewChatItem -> json.encodeToString(chatItem) + is ChatItemStatusUpdated -> json.encodeToString(chatItem) is ChatItemUpdated -> json.encodeToString(chatItem) + is ChatItemDeleted -> json.encodeToString(chatItem) is CmdOk -> noDetails() is ChatCmdError -> chatError.string is ChatRespError -> chatError.string 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 5a9e3c0411..3ce907afc7 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 @@ -1,5 +1,6 @@ package chat.simplex.app.views +import android.annotation.SuppressLint import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.foundation.* @@ -8,8 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.* import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -40,11 +40,11 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) { } @Composable -fun TerminalLayout(terminalItems: List , close: () -> Unit, sendCommand: (String) -> Unit) { +fun TerminalLayout(terminalItems: List, close: () -> Unit, sendCommand: (String) -> Unit) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Scaffold( topBar = { CloseSheetBar(close) }, - bottomBar = { SendMsgView(sendCommand) }, + bottomBar = { SendMsgView(msg = remember { mutableStateOf("") }, sendCommand) }, modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> Surface( 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 b2ce920b74..a72ac162da 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 @@ -41,6 +41,8 @@ fun ChatView(chatModel: ChatModel) { chatModel.chatId.value = null } else { val quotedItem = remember { mutableStateOf(null) } + val editingItem = remember { mutableStateOf(null) } + var msg = remember { mutableStateOf("") } BackHandler { chatModel.chatId.value = null } // TODO a more advanced version would mark as read only if in view LaunchedEffect(chat.chatItems) { @@ -57,24 +59,37 @@ fun ChatView(chatModel: ChatModel) { } } } - ChatLayout(user, chat, chatModel.chatItems, quotedItem, + ChatLayout(user, chat, chatModel.chatItems, msg, quotedItem, editingItem, back = { chatModel.chatId.value = null }, info = { ModalManager.shared.showCustomModal { close -> ChatInfoView(chatModel, close) } }, sendMessage = { msg -> withApi { // show "in progress" val cInfo = chat.chatInfo - val newItem = chatModel.controller.apiSendMessage( - type = cInfo.chatType, - id = cInfo.apiId, - quotedItemId = quotedItem.value?.meta?.itemId, - mc = MsgContent.MCText(msg) - ) - quotedItem.value = null + val ei = editingItem.value + if (ei != null) { + val updatedItem = chatModel.controller.apiUpdateMessage( + type = cInfo.chatType, + id = cInfo.apiId, + itemId = ei.meta.itemId, + mc = MsgContent.MCText(msg) + ) + if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem) + } else { + val newItem = chatModel.controller.apiSendMessage( + type = cInfo.chatType, + id = cInfo.apiId, + quotedItemId = quotedItem.value?.meta?.itemId, + mc = MsgContent.MCText(msg) + ) + if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) + } // hide "in progress" - if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) + editingItem.value = null + quotedItem.value = null } - } + }, + resetMessage = { msg.value = "" } ) } } @@ -84,23 +99,27 @@ fun ChatLayout( user: User, chat: Chat, chatItems: List, + msg: MutableState, quotedItem: MutableState, + editingItem: MutableState, back: () -> Unit, info: () -> Unit, - sendMessage: (String) -> Unit + sendMessage: (String) -> Unit, + resetMessage: () -> Unit ) { Surface( Modifier .fillMaxWidth() - .background(MaterialTheme.colors.background)) { + .background(MaterialTheme.colors.background) + ) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Scaffold( topBar = { ChatInfoToolbar(chat, back, info) }, - bottomBar = { ComposeView(quotedItem, sendMessage) }, + bottomBar = { ComposeView(msg, quotedItem, editingItem, sendMessage, resetMessage) }, modifier = Modifier.navigationBarsWithImePadding() ) { contentPadding -> Box(Modifier.padding(contentPadding)) { - ChatItemsList(user, chatItems, quotedItem) + ChatItemsList(user, chatItems, msg, quotedItem, editingItem) } } } @@ -133,14 +152,19 @@ fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) { ) { val cInfo = chat.chatInfo ChatInfoImage(chat, size = 40.dp) - Column(Modifier.padding(start = 8.dp), + Column( + Modifier.padding(start = 8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text(cInfo.displayName, fontWeight = FontWeight.Bold, - maxLines = 1, overflow = TextOverflow.Ellipsis) + Text( + cInfo.displayName, fontWeight = FontWeight.Bold, + maxLines = 1, overflow = TextOverflow.Ellipsis + ) if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) { - Text(cInfo.fullName, - maxLines = 1, overflow = TextOverflow.Ellipsis) + Text( + cInfo.fullName, + maxLines = 1, overflow = TextOverflow.Ellipsis + ) } } } @@ -160,7 +184,13 @@ val CIListStateSaver = run { } @Composable -fun ChatItemsList(user: User, chatItems: List, quotedItem: MutableState) { +fun ChatItemsList( + user: User, + chatItems: List, + msg: MutableState, + quotedItem: MutableState, + editingItem: MutableState +) { val listState = rememberLazyListState() val keyboardState by getKeyboardState() val ciListState = rememberSaveable(stateSaver = CIListStateSaver) { @@ -171,7 +201,7 @@ fun ChatItemsList(user: User, chatItems: List, quotedItem: MutableStat val cxt = LocalContext.current LazyColumn(state = listState) { items(chatItems) { cItem -> - ChatItemView(user, cItem, quotedItem, cxt, uriHandler) + ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler) } val len = chatItems.count() if (len > 1 && (keyboardState != ciListState.value.keyboardState || !ciListState.value.scrolled || len != ciListState.value.itemCount)) { @@ -217,10 +247,13 @@ fun PreviewChatLayout() { chatStats = Chat.ChatStats() ), chatItems = chatItems, + msg = remember { mutableStateOf("") }, quotedItem = remember { mutableStateOf(null) }, + editingItem = remember { mutableStateOf(null) }, back = {}, info = {}, - sendMessage = {} + sendMessage = {}, + resetMessage = {} ) } } 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 ebea19ef94..790b34924d 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 @@ -1,14 +1,29 @@ package chat.simplex.app.views.chat import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.* import chat.simplex.app.model.ChatItem +// TODO ComposeState + @Composable -fun ComposeView(quotedItem: MutableState, sendMessage: (String) -> Unit) { +fun ComposeView( + msg: MutableState, + quotedItem: MutableState, + editingItem: MutableState, + sendMessage: (String) -> Unit, + resetMessage: () -> Unit +) { Column { - QuotedItemView(quotedItem) - SendMsgView(sendMessage) + when { + quotedItem.value != null -> { + ContextItemView(quotedItem) + } + editingItem.value != null -> { + ContextItemView(editingItem, editing = editingItem.value != null, resetMessage) + } + else -> {} + } + SendMsgView(msg, sendMessage, editing = editingItem.value != null) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/QuotedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt similarity index 60% rename from apps/android/app/src/main/java/chat/simplex/app/views/chat/QuotedItemView.kt rename to apps/android/app/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt index 5f8f1ac54f..3690e79883 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/QuotedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt @@ -19,27 +19,38 @@ import chat.simplex.app.views.chat.item.* import kotlinx.datetime.Clock @Composable -fun QuotedItemView(quotedItem: MutableState) { - val qi = quotedItem.value - if (qi != null) { - val sent = qi.chatDir.sent +fun ContextItemView( + contextItem: MutableState, + editing: Boolean = false, + resetMessage: () -> Unit = {} +) { + val cxtItem = contextItem.value + if (cxtItem != null) { + val sent = cxtItem.chatDir.sent Row( - Modifier.padding(top = 8.dp) + Modifier + .padding(top = 8.dp) .background(if (sent) SentColorLight else ReceivedColorLight), verticalAlignment = Alignment.CenterVertically ) { Box( - Modifier.padding(start = 16.dp) + Modifier + .padding(start = 16.dp) .padding(vertical = 12.dp) .fillMaxWidth() .weight(1F) ) { - QuoteText(qi) + ContextItemText(cxtItem) } - IconButton(onClick = { quotedItem.value = null }) { + IconButton(onClick = { + contextItem.value = null + if (editing) { + resetMessage() + } + }) { Icon( Icons.Outlined.Close, - "Remove quote", + contentDescription = "Cancel", tint = MaterialTheme.colors.primary, modifier = Modifier.padding(10.dp) ) @@ -49,14 +60,14 @@ fun QuotedItemView(quotedItem: MutableState) { } @Composable -private fun QuoteText(qi: ChatItem) { - val member = qi.memberDisplayName +private fun ContextItemText(cxtItem: ChatItem) { + val member = cxtItem.memberDisplayName if (member == null) { - Text(qi.content.text, maxLines = 3) + Text(cxtItem.content.text, maxLines = 3) } else { val annotatedText = buildAnnotatedString { withStyle(boldFont) { append(member) } - append(": ${qi.content.text}") + append(": ${cxtItem.content.text}") } Text(annotatedText, maxLines = 3) } @@ -64,13 +75,15 @@ private fun QuoteText(qi: ChatItem) { @Preview @Composable -fun PreviewTextItemViewEmoji() { +fun PreviewContextItemView() { SimpleXTheme { - QuotedItemView( - quotedItem = remember { - mutableStateOf(ChatItem.getSampleData( - 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" - )) + ContextItemView( + contextItem = remember { + mutableStateOf( + ChatItem.getSampleData( + 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ) + ) } ) } 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 90248d8818..89b5851f89 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 @@ -9,6 +9,7 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -24,15 +25,14 @@ import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.* @Composable -fun SendMsgView(sendMessage: (String) -> Unit) { - var msg by remember { mutableStateOf("") } +fun SendMsgView(msg: MutableState, sendMessage: (String) -> Unit, editing: Boolean = false) { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) var textStyle by remember { mutableStateOf(smallFont) } BasicTextField( - value = msg, + value = msg.value, onValueChange = { - msg = it - textStyle = if(isShortEmoji(it)) { + msg.value = it + textStyle = if (isShortEmoji(it)) { if (it.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont } else { smallFont @@ -64,9 +64,9 @@ fun SendMsgView(sendMessage: (String) -> Unit) { ) { innerTextField() } - val color = if (msg.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray + val color = if (msg.value.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray Icon( - Icons.Outlined.ArrowUpward, + if (editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward, "Send Message", tint = Color.White, modifier = Modifier @@ -75,9 +75,9 @@ fun SendMsgView(sendMessage: (String) -> Unit) { .clip(CircleShape) .background(color) .clickable { - if (msg.isNotEmpty()) { - sendMessage(msg) - msg = "" + if (msg.value.isNotEmpty()) { + sendMessage(msg.value) + msg.value = "" textStyle = smallFont } } @@ -98,7 +98,25 @@ fun SendMsgView(sendMessage: (String) -> Unit) { fun PreviewSendMsgView() { SimpleXTheme { SendMsgView( + msg = remember { mutableStateOf("") }, sendMessage = { msg -> println(msg) } ) } } + +@Preview(showBackground = true) +@Preview( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +) +@Composable +fun PreviewSendMsgViewEditing() { + SimpleXTheme { + SendMsgView( + msg = remember { mutableStateOf("") }, + sendMessage = { msg -> println(msg) }, + editing = true + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt index aafcb7d041..0517b0ab10 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -1,8 +1,15 @@ package chat.simplex.app.views.chat.item +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.model.CIDirection import chat.simplex.app.model.ChatItem @@ -11,11 +18,24 @@ import kotlinx.datetime.Clock @Composable fun CIMetaView(chatItem: ChatItem) { - Text( - chatItem.timestampText, - color = HighOrLowlight, - fontSize = 14.sp - ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (chatItem.meta.itemEdited) { + Icon( + Icons.Filled.Edit, + modifier = Modifier.height(12.dp), + contentDescription = "Edited", + tint = HighOrLowlight, + ) + } + Text( + chatItem.timestampText, + color = HighOrLowlight, + fontSize = 14.sp + ) + } } @Preview @@ -27,3 +47,14 @@ fun PreviewCIMetaView() { ) ) } + +@Preview +@Composable +fun PreviewCIMetaViewEdited() { + CIMetaView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", + itemEdited = true + ) + ) +} 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 acc249bdf7..c91f159c55 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -22,7 +23,15 @@ import chat.simplex.app.views.helpers.shareText import kotlinx.datetime.Clock @Composable -fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState, cxt: Context, uriHandler: UriHandler? = null) { +fun ChatItemView( + user: User, + cItem: ChatItem, + msg: MutableState, + quotedItem: MutableState, + editingItem: MutableState, + cxt: Context, + uriHandler: UriHandler? = null +) { val sent = cItem.chatDir.sent val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart var showMenu by remember { mutableStateOf(false) } @@ -44,6 +53,7 @@ fun ChatItemView(user: User, cItem: ChatItem, quotedItem: MutableState Unit) { DropdownMenuItem(onClick) { Row { - Text(text, modifier = Modifier - .fillMaxWidth() - .weight(1F)) + Text( + text, modifier = Modifier + .fillMaxWidth() + .weight(1F) + ) Icon(icon, text, tint = HighOrLowlight) } } @@ -81,7 +101,9 @@ fun PreviewChatItemView() { ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" ), + msg = remember { mutableStateOf("") }, quotedItem = remember { mutableStateOf(null) }, + editingItem = remember { mutableStateOf(null) }, cxt = LocalContext.current ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt index abdc15effc..d5b2d9c24b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.model.* @@ -48,7 +48,9 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { if (ci.formattedText == null && isShortEmoji(ci.content.text)) { Column( - Modifier.padding(bottom = 2.dp).fillMaxWidth(), + Modifier + .padding(bottom = 2.dp) + .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { EmojiText(ci.content.text) @@ -57,7 +59,7 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) { } else { MarkdownText( ci.content, ci.formattedText, ci.memberDisplayName, - metaText = ci.timestampText, uriHandler = uriHandler, senderBold = true + metaText = ci.timestampText, edited = ci.meta.itemEdited, uriHandler = uriHandler, senderBold = true ) } } @@ -69,14 +71,18 @@ fun FramedItemView(user: User, ci: ChatItem, uriHandler: UriHandler? = null) { } } +class EditedProvider: PreviewParameterProvider { + override val values = listOf(false, true).asSequence() +} + @Preview @Composable -fun PreviewTextItemViewSnd() { +fun PreviewTextItemViewSnd(@PreviewParameter(EditedProvider::class) edited: Boolean) { SimpleXTheme { FramedItemView( User.sampleData, ChatItem.getSampleData( - 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = edited ) ) } @@ -84,12 +90,12 @@ fun PreviewTextItemViewSnd() { @Preview @Composable -fun PreviewTextItemViewRcv() { +fun PreviewTextItemViewRcv(@PreviewParameter(EditedProvider::class) edited: Boolean) { SimpleXTheme { FramedItemView( User.sampleData, ChatItem.getSampleData( - 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" + 1, CIDirection.DirectRcv(), Clock.System.now(), "hello", itemEdited = edited ) ) } @@ -97,7 +103,7 @@ fun PreviewTextItemViewRcv() { @Preview @Composable -fun PreviewTextItemViewLong() { +fun PreviewTextItemViewLong(@PreviewParameter(EditedProvider::class) edited: Boolean) { SimpleXTheme { FramedItemView( User.sampleData, @@ -105,7 +111,8 @@ fun PreviewTextItemViewLong() { 1, CIDirection.DirectSnd(), Clock.System.now(), - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + itemEdited = edited ) ) } @@ -113,7 +120,7 @@ fun PreviewTextItemViewLong() { @Preview @Composable -fun PreviewTextItemViewQuote() { +fun PreviewTextItemViewQuote(@PreviewParameter(EditedProvider::class) edited: Boolean) { SimpleXTheme { FramedItemView( User.sampleData, @@ -122,7 +129,8 @@ fun PreviewTextItemViewQuote() { Clock.System.now(), "https://simplex.chat", CIStatus.SndSent(), - quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv()) + quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv()), + itemEdited = edited ) ) } @@ -130,7 +138,7 @@ fun PreviewTextItemViewQuote() { @Preview @Composable -fun PreviewTextItemViewEmoji() { +fun PreviewTextItemViewEmoji(@PreviewParameter(EditedProvider::class) edited: Boolean) { SimpleXTheme { FramedItemView( User.sampleData, @@ -139,7 +147,8 @@ fun PreviewTextItemViewEmoji() { Clock.System.now(), "👍", CIStatus.SndSent(), - quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv()) + quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv()), + itemEdited = edited ) ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index 6789833fe2..8d25881786 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -39,6 +39,7 @@ fun MarkdownText ( formattedText: List? = null, sender: String? = null, metaText: String? = null, + edited: Boolean = false, style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp), maxLines: Int = Int.MAX_VALUE, overflow: TextOverflow = TextOverflow.Clip, @@ -46,11 +47,12 @@ fun MarkdownText ( senderBold: Boolean = false, modifier: Modifier = Modifier ) { + val reserve = if (edited) " " else " " if (formattedText == null) { val annotatedText = buildAnnotatedString { appendSender(this, sender, senderBold) append(content.text) - if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") } + if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) } } Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow) } else { @@ -71,7 +73,7 @@ fun MarkdownText ( } } } - if (metaText != null) withStyle(reserveTimestampStyle) { append(" $metaText") } + if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) } } if (hasLinks && uriHandler != null) { ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 3836026c04..c0713686c3 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -564,10 +564,10 @@ struct ChatItem: Identifiable, Decodable { } } - static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil) -> ChatItem { + static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> ChatItem { ChatItem( chatDir: dir, - meta: CIMeta.getSample(id, ts, text, status), + meta: CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable), content: .sndMsgContent(msgContent: .text(text)), quotedItem: quotedItem ) @@ -598,16 +598,22 @@ struct CIMeta: Decodable { var itemText: String var itemStatus: CIStatus var createdAt: Date + var itemDeleted: Bool + var itemEdited: Bool + var editable: Bool var timestampText: Text { get { SimpleX.timestampText(itemTs) } } - static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew) -> CIMeta { + static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, _ itemDeleted: Bool = false, _ itemEdited: Bool = false, _ editable: Bool = true) -> CIMeta { CIMeta( itemId: id, itemTs: ts, itemText: text, itemStatus: status, - createdAt: ts + createdAt: ts, + itemDeleted: itemDeleted, + itemEdited: itemEdited, + editable: editable ) } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index da985998c6..0f0386bf18 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -15,6 +15,11 @@ private var chatController: chat_ctrl? private let jsonDecoder = getJSONDecoder() private let jsonEncoder = getJSONEncoder() +enum MsgDeleteMode: String { + case mdBroadcast = "broadcast" + case mdInternal = "internal" +} + enum ChatCommand { case showActiveUser case createActiveUser(profile: Profile) @@ -23,6 +28,8 @@ enum ChatCommand { case apiGetChat(type: ChatType, id: Int64) case apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) case apiSendMessageQuote(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) + case apiUpdateMessage(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) + case apiDeleteMessage(type: ChatType, id: Int64, itemId: Int64, mode: MsgDeleteMode) case getUserSMPServers case setUserSMPServers(smpServers: [String]) case addContact @@ -47,6 +54,8 @@ enum ChatCommand { case let .apiGetChat(type, id): return "/_get chat \(ref(type, id)) count=100" case let .apiSendMessage(type, id, mc): return "/_send \(ref(type, id)) \(mc.cmdString)" case let .apiSendMessageQuote(type, id, itemId, mc): return "/_send_quote \(ref(type, id)) \(itemId) \(mc.cmdString)" + case let .apiUpdateMessage(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)" + case let .apiDeleteMessage(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" case .getUserSMPServers: return "/smp_servers" case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))" case .addContact: return "/connect" @@ -74,6 +83,8 @@ enum ChatCommand { case .apiGetChat: return "apiGetChat" case .apiSendMessage: return "apiSendMessage" case .apiSendMessageQuote: return "apiSendMessageQuote" + case .apiUpdateMessage: return "apiUpdateMessage" + case .apiDeleteMessage: return "apiDeleteMessage" case .getUserSMPServers: return "getUserSMPServers" case .setUserSMPServers: return "setUserSMPServers" case .addContact: return "addContact" @@ -135,7 +146,9 @@ enum ChatResponse: Decodable, Error { case groupEmpty(groupInfo: GroupInfo) case userContactLinkSubscribed case newChatItem(chatItem: AChatItem) + case chatItemStatusUpdated(chatItem: AChatItem) case chatItemUpdated(chatItem: AChatItem) + case chatItemDeleted(chatItem: AChatItem) case cmdOk case chatCmdError(chatError: ChatError) case chatError(chatError: ChatError) @@ -173,7 +186,9 @@ enum ChatResponse: Decodable, Error { case .groupEmpty: return "groupEmpty" case .userContactLinkSubscribed: return "userContactLinkSubscribed" case .newChatItem: return "newChatItem" + case .chatItemStatusUpdated: return "chatItemStatusUpdated" case .chatItemUpdated: return "chatItemUpdated" + case .chatItemDeleted: return "chatItemDeleted" case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" @@ -214,7 +229,9 @@ enum ChatResponse: Decodable, Error { case let .groupEmpty(groupInfo): return String(describing: groupInfo) case .userContactLinkSubscribed: return noDetails case let .newChatItem(chatItem): return String(describing: chatItem) + case let .chatItemStatusUpdated(chatItem): return String(describing: chatItem) case let .chatItemUpdated(chatItem): return String(describing: chatItem) + case let .chatItemDeleted(chatItem): return String(describing: chatItem) case .cmdOk: return noDetails case let .chatCmdError(chatError): return String(describing: chatError) case let .chatError(chatError): return String(describing: chatError) @@ -393,6 +410,18 @@ func apiSendMessage(type: ChatType, id: Int64, quotedItemId: Int64?, msg: MsgCon throw r } +func apiUpdateMessage(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) async throws -> ChatItem { + let r = await chatSendCmd(.apiUpdateMessage(type: type, id: id, itemId: itemId, msg: msg), bgDelay: msgDelay) + if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem } + throw r +} + +func apiDeleteMessage(type: ChatType, id: Int64, itemId: Int64, mode: MsgDeleteMode) async throws -> ChatItem { + let r = await chatSendCmd(.apiDeleteMessage(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay) + if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem } + throw r +} + func getUserSMPServers() throws -> [String] { let r = chatSendCmdSync(.getUserSMPServers) if case let .userSMPServers(smpServers) = r { return smpServers } @@ -601,7 +630,7 @@ func processReceivedMsg(_ res: ChatResponse) { let cItem = aChatItem.chatItem chatModel.addChatItem(cInfo, cItem) NtfManager.shared.notifyMessageReceived(cInfo, cItem) - case let .chatItemUpdated(aChatItem): + case let .chatItemStatusUpdated(aChatItem): let cInfo = aChatItem.chatInfo let cItem = aChatItem.chatItem if chatModel.upsertChatItem(cInfo, cItem) { @@ -614,6 +643,15 @@ func processReceivedMsg(_ res: ChatResponse) { default: break } } + case let .chatItemUpdated(aChatItem): + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + if chatModel.upsertChatItem(cInfo, cItem) { + NtfManager.shared.notifyMessageReceived(cInfo, cItem) + } + case .chatItemDeleted(_): + // TODO let .chatItemDeleted(aChatItem) + return default: logger.debug("unsupported event: \(res.responseType)") } @@ -745,6 +783,8 @@ enum ChatErrorType: Decodable { case fileSend(fileId: Int64, agentError: String) case fileRcvChunk(message: String) case fileInternal(message: String) + case invalidQuote + case invalidMessageUpdate case agentVersion case commandError(message: String) } @@ -776,6 +816,8 @@ enum StoreError: Decodable { case noMsgDelivery(connId: Int64, agentMsgId: String) case badChatItem(itemId: Int64) case chatItemNotFound(itemId: Int64) + case quotedChatItemNotFound + case chatItemSharedMsgIdNotFound(sharedMsgId: String) } enum AgentErrorType: Decodable { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 08112976fc..80ed91e072 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -13,6 +13,10 @@ struct CIMetaView: View { var body: some View { HStack(alignment: .center, spacing: 4) { + if chatItem.meta.itemEdited { + statusImage("pencil", .secondary, 9) + } + switch chatItem.meta.itemStatus { case .sndSent: statusImage("checkmark", .secondary) @@ -31,17 +35,20 @@ struct CIMetaView: View { } } - private func statusImage(_ systemName: String, _ color: Color) -> some View { + private func statusImage(_ systemName: String, _ color: Color, _ maxHeight: CGFloat = 8) -> some View { Image(systemName: systemName) .resizable() .aspectRatio(contentMode: .fit) .foregroundColor(color) - .frame(maxHeight: 8) + .frame(maxHeight: maxHeight) } } struct CIMetaView_Previews: PreviewProvider { static var previews: some View { - CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent)) + return Group { + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent)) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, false, true)) + } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 90e5d4e2a6..b50abd392c 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -54,7 +54,8 @@ struct FramedItemView: View { content: chatItem.content, formattedText: chatItem.formattedText, sender: chatItem.memberDisplayName, - metaText: chatItem.timestampText + metaText: chatItem.timestampText, + edited: chatItem.meta.itemEdited ) .padding(.vertical, 6) .padding(.horizontal, 12) @@ -63,14 +64,15 @@ struct FramedItemView: View { .textSelection(.enabled) } } - .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } CIMetaView(chatItem: chatItem) - .padding(.trailing, 12) + .padding(.horizontal, 12) .padding(.bottom, 6) + .overlay(DetermineWidth()) } .background(chatItemFrameColor(chatItem, colorScheme)) .cornerRadius(18) + .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } switch chatItem.meta.itemStatus { case .sndErrorAuth: @@ -110,3 +112,19 @@ struct FramedItemView_Previews: PreviewProvider { .previewLayout(.fixed(width: 360, height: 200)) } } + +struct FramedItemViewEdited_Previews: PreviewProvider { + static var previews: some View { + Group{ + FramedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, false, true)) + FramedItemView(chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), false, true)) + FramedItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), false, true)) + FramedItemView(chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), false, true)) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, false, true)) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, false, true)) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, false, true)) + FramedItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, false, true)) + } + .previewLayout(.fixed(width: 360, height: 200)) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 4620fb34c8..9ffea020a3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -16,20 +16,22 @@ struct MsgContentView: View { var formattedText: [FormattedText]? = nil var sender: String? = nil var metaText: Text? = nil + var edited: Bool = false - var body: some View { + var body: some View { let v = messageText(content, formattedText, sender) if let mt = metaText { - return v + reserveSpaceForMeta(mt) + return v + reserveSpaceForMeta(mt, edited) } else { return v } } - private func reserveSpaceForMeta(_ meta: Text) -> Text { - (Text(" ") + meta) - .font(.caption) - .foregroundColor(.clear) + private func reserveSpaceForMeta(_ meta: Text, _ edited: Bool) -> Text { + let reserve = edited ? " " : " " + return (Text(reserve) + meta) + .font(.caption) + .foregroundColor(.clear) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index bc67bb498e..cac7716dfc 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -12,7 +12,9 @@ struct ChatView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.colorScheme) var colorScheme @ObservedObject var chat: Chat + @State var message: String = "" @State var quotedItem: ChatItem? = nil + @State var editingItem: ChatItem? = nil @State private var inProgress: Bool = false @FocusState private var keyboardVisible: Bool @State private var showChatInfo = false @@ -31,7 +33,10 @@ struct ChatView: View { ChatItemView(chatItem: ci) .contextMenu { Button { - withAnimation { quotedItem = ci } + withAnimation { + editingItem = nil + quotedItem = ci + } } label: { Label("Reply", systemImage: "arrowshape.turn.up.left") } Button { showShareSheet(items: [ci.content.text]) @@ -39,6 +44,15 @@ struct ChatView: View { Button { UIPasteboard.general.string = ci.content.text } label: { Label("Copy", systemImage: "doc.on.doc") } + if (ci.chatDir.sent && ci.meta.editable) { + Button { + withAnimation { + quotedItem = nil + editingItem = ci + message = ci.content.text + } + } label: { Label("Edit", systemImage: "square.and.pencil") } + } } .padding(.horizontal) .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) @@ -71,8 +85,11 @@ struct ChatView: View { Spacer(minLength: 0) ComposeView( + message: $message, quotedItem: $quotedItem, + editingItem: $editingItem, sendMessage: sendMessage, + resetMessage: { message = "" }, inProgress: inProgress, keyboardVisible: $keyboardVisible ) @@ -134,18 +151,31 @@ struct ChatView: View { Task { logger.debug("ChatView sendMessage: in Task") do { - let chatItem = try await apiSendMessage( - type: chat.chatInfo.chatType, - id: chat.chatInfo.apiId, - quotedItemId: quotedItem?.meta.itemId, - msg: .text(msg) - ) - DispatchQueue.main.async { - quotedItem = nil - chatModel.addChatItem(chat.chatInfo, chatItem) + if let ei = editingItem { + let chatItem = try await apiUpdateMessage( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + itemId: ei.id, + msg: .text(msg) + ) + DispatchQueue.main.async { + editingItem = nil + let _ = chatModel.upsertChatItem(chat.chatInfo, chatItem) + } + } else { + let chatItem = try await apiSendMessage( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + quotedItemId: quotedItem?.meta.itemId, + msg: .text(msg) + ) + DispatchQueue.main.async { + quotedItem = nil + chatModel.addChatItem(chat.chatInfo, chatItem) + } } } catch { - logger.error("ChatView.sendMessage apiSendMessage error: \(error.localizedDescription)") + logger.error("ChatView.sendMessage error: \(error.localizedDescription)") } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 1e02ee3eeb..3dc3e1b71a 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -8,35 +8,69 @@ import SwiftUI +// TODO +//enum ComposeState { +// case plain +// case quoted(quotedItem: ChatItem) +// case editing(editingItem: ChatItem) +//} + struct ComposeView: View { + @Binding var message: String @Binding var quotedItem: ChatItem? + @Binding var editingItem: ChatItem? var sendMessage: (String) -> Void + var resetMessage: () -> Void var inProgress: Bool = false @FocusState.Binding var keyboardVisible: Bool + @State var editing: Bool = false var body: some View { VStack(spacing: 0) { - QuotedItemView(quotedItem: $quotedItem) - .transition(.move(edge: .bottom)) + if (quotedItem != nil) { + ContextItemView(contextItem: $quotedItem, editing: $editing) + } else if (editingItem != nil) { + ContextItemView(contextItem: $editingItem, editing: $editing, resetMessage: resetMessage) + } SendMessageView( sendMessage: sendMessage, inProgress: inProgress, - keyboardVisible: $keyboardVisible + message: $message, + keyboardVisible: $keyboardVisible, + editing: $editing ) .background(.background) } + .onChange(of: editingItem == nil) { _ in + editing = (editingItem != nil) + } } } struct ComposeView_Previews: PreviewProvider { static var previews: some View { + @State var message: String = "" @FocusState var keyboardVisible: Bool - @State var quotedItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") + @State var item: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") + @State var nilItem: ChatItem? = nil - return ComposeView( - quotedItem: $quotedItem, - sendMessage: { print ($0) }, - keyboardVisible: $keyboardVisible - ) + return Group { + ComposeView( + message: $message, + quotedItem: $item, + editingItem: $nilItem, + sendMessage: { print ($0) }, + resetMessage: {}, + keyboardVisible: $keyboardVisible + ) + ComposeView( + message: $message, + quotedItem: $nilItem, + editingItem: $item, + sendMessage: { print ($0) }, + resetMessage: {}, + keyboardVisible: $keyboardVisible + ) + } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift new file mode 100644 index 0000000000..d853e17026 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -0,0 +1,55 @@ +// +// ContextItemView.swift +// SimpleX +// +// Created by JRoberts on 13/03/2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ContextItemView: View { + @Environment(\.colorScheme) var colorScheme + @Binding var contextItem: ChatItem? + @Binding var editing: Bool + var resetMessage: () -> Void = {} + + var body: some View { + if let cxtItem = contextItem { + HStack { + contextText(cxtItem).lineLimit(3) + Spacer() + Button { + withAnimation { + contextItem = nil + if editing { resetMessage() } + } + } label: { + Image(systemName: "multiply") + } + } + .padding(12) + .frame(maxWidth: .infinity) + .background(chatItemFrameColor(cxtItem, colorScheme)) + .padding(.top, 8) + } else { + EmptyView() + } + } + + func contextText(_ cxtItem: ChatItem) -> some View { + if let s = cxtItem.memberDisplayName { + return (Text(s).fontWeight(.medium) + Text(": \(cxtItem.content.text)")) + } else { + return Text(cxtItem.content.text) + } + } +} + +struct ContextItemView_Previews: PreviewProvider { + static var previews: some View { + @State var contextItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") + @State var editing: Bool = false + return ContextItemView(contextItem: $contextItem, editing: $editing) + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift deleted file mode 100644 index 36a49637ac..0000000000 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/QuotedItemView.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// QuotedItemView.swift -// SimpleX -// -// Created by Evgeny on 13/03/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -struct QuotedItemView: View { - @Environment(\.colorScheme) var colorScheme - @Binding var quotedItem: ChatItem? - - var body: some View { - if let qi = quotedItem { - HStack { - quoteText(qi).lineLimit(3) - Spacer() - Button { - withAnimation { quotedItem = nil } - } label: { - Image(systemName: "multiply") - } - } - .padding(12) - .frame(maxWidth: .infinity) - .background(chatItemFrameColor(qi, colorScheme)) - .padding(.top, 8) - } else { - EmptyView() - } - } - - func quoteText(_ qi: ChatItem) -> some View { - if let s = qi.memberDisplayName { - return (Text(s).fontWeight(.medium) + Text(": \(qi.content.text)")) - } else { - return Text(qi.content.text) - } - } -} - -struct QuotedItemView_Previews: PreviewProvider { - static var previews: some View { - @State var quotedItem: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") - return QuotedItemView(quotedItem: $quotedItem) - } -} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index af639999fc..a760195fa9 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -11,9 +11,10 @@ import SwiftUI struct SendMessageView: View { var sendMessage: (String) -> Void var inProgress: Bool = false - @State private var message: String = "" //Lorem ipsum dolor sit amet, consectetur" // adipiscing elit, sed do eiusmod tempor incididunt ut labor7 et dolore magna aliqua. Ut enim ad minim veniam, quis"// nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."// Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + @Binding var message: String //Lorem ipsum dolor sit amet, consectetur" // adipiscing elit, sed do eiusmod tempor incididunt ut labor7 et dolore magna aliqua. Ut enim ad minim veniam, quis"// nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."// Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." @Namespace var namespace @FocusState.Binding var keyboardVisible: Bool + @Binding var editing: Bool @State private var teHeight: CGFloat = 42 @State private var teFont: Font = .body var maxHeight: CGFloat = 360 @@ -47,7 +48,7 @@ struct SendMessageView: View { .padding([.bottom, .trailing], 3) } else { Button(action: submit) { - Image(systemName: "arrow.up.circle.fill") + Image(systemName: editing ? "checkmark.circle.fill" : "arrow.up.circle.fill") .resizable() .foregroundColor(.accentColor) } @@ -85,15 +86,34 @@ struct SendMessageView: View { struct SendMessageView_Previews: PreviewProvider { static var previews: some View { + @State var message: String = "" @FocusState var keyboardVisible: Bool + @State var editingOff: Bool = false + @State var editingOn: Bool = true + @State var item: ChatItem? = ChatItem.getSample(1, .directSnd, .now, "hello") + @State var nilItem: ChatItem? = nil - return VStack { - Text("") - Spacer(minLength: 0) - SendMessageView( - sendMessage: { print ($0) }, - keyboardVisible: $keyboardVisible - ) + return Group { + VStack { + Text("") + Spacer(minLength: 0) + SendMessageView( + sendMessage: { print ($0) }, + message: $message, + keyboardVisible: $keyboardVisible, + editing: $editingOff + ) + } + VStack { + Text("") + Spacer(minLength: 0) + SendMessageView( + sendMessage: { print ($0) }, + message: $message, + keyboardVisible: $keyboardVisible, + editing: $editingOn + ) + } } } } diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 60e54807a9..ae9321fd43 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -13,7 +13,9 @@ private let terminalFont = Font.custom("Menlo", size: 16) struct TerminalView: View { @EnvironmentObject var chatModel: ChatModel @State var inProgress: Bool = false + @State var message: String = "" @FocusState private var keyboardVisible: Bool + @State var editing: Bool = false var body: some View { VStack { @@ -54,7 +56,9 @@ struct TerminalView: View { SendMessageView( sendMessage: sendMessage, inProgress: inProgress, - keyboardVisible: $keyboardVisible + message: $message, + keyboardVisible: $keyboardVisible, + editing: $editing ) } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index e4598f1f1d..f56df3377e 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -110,12 +110,12 @@ 5CE4407A27ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 5CEACCE427DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; - 5CEACCE727DE97B6000BD591 /* QuotedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */; }; - 5CEACCE827DE97B6000BD591 /* QuotedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 5CEACCEE27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; 640F50E427CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; + 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; + 64AA1C6A27EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -196,9 +196,9 @@ 5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; - 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotedItemView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; }; + 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -440,7 +440,7 @@ children = ( 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */, 5CEACCE227DE9246000BD591 /* ComposeView.swift */, - 5CEACCE627DE97B6000BD591 /* QuotedItemView.swift */, + 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */, ); path = ComposeMessage; sourceTree = ""; @@ -623,7 +623,6 @@ 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */, 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */, - 5CEACCE727DE97B6000BD591 /* QuotedItemView.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 5C2E261227A30FEA00F70299 /* TerminalView.swift in Sources */, @@ -656,6 +655,7 @@ 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, 5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */, 5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */, + 64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -674,7 +674,6 @@ 5C3A88D227DF57800060F1C2 /* FramedItemView.swift in Sources */, 5CB924E527A8683A00ACCCDD /* UserAddress.swift in Sources */, 640F50E427CF991C001E05C2 /* SMPServers.swift in Sources */, - 5CEACCE827DE97B6000BD591 /* QuotedItemView.swift in Sources */, 5C063D2827A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 5C2E261327A30FEA00F70299 /* TerminalView.swift in Sources */, @@ -707,6 +706,7 @@ 5C5F2B7127EBC704006A9D5F /* ProfileImage.swift in Sources */, 5CE4407327ADB1D0007B033A /* Emoji.swift in Sources */, 5C1A4C1F27A715B700EAD5AD /* ChatItemView.swift in Sources */, + 64AA1C6A27EE10C800AC7277 /* ContextItemView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 33045999d0..1ccc0c9ef6 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -2854,7 +2854,7 @@ updateDirectChatItem_ db userId contactId itemId newContent msgId = runExceptT $ |] (newContent, newText, currentTs, userId, contactId, itemId) liftIO $ DB.execute db "INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?)" (itemId, msgId, currentTs, currentTs) - pure ci {content = newContent, meta = (meta ci) {itemText = newText}, formattedText = parseMaybeMarkdownList newText} + pure ci {content = newContent, meta = (meta ci) {itemText = newText, itemEdited = True}, formattedText = parseMaybeMarkdownList newText} where correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci @@ -2938,7 +2938,7 @@ updateGroupChatItem_ db user@User {userId} groupId itemId newContent msgId = run |] (newContent, newText, currentTs, userId, groupId, itemId) liftIO $ DB.execute db "INSERT INTO chat_item_messages (chat_item_id, message_id, created_at, updated_at) VALUES (?,?,?,?)" (itemId, msgId, currentTs, currentTs) - pure ci {content = newContent, meta = (meta ci) {itemText = newText}, formattedText = parseMaybeMarkdownList newText} + pure ci {content = newContent, meta = (meta ci) {itemText = newText, itemEdited = True}, formattedText = parseMaybeMarkdownList newText} where correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci