mobile: message delete (#480)

* mobile: message delete

* ios

* android api

* meta

* android

* new ios libs

* bug fixes

* adjust alert

* fix deleted item upsert

* change border color for ios

* format

* android - red button

* ios: deleted item design

* android: deleted item design

* android alert msg

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
JRoberts
2022-03-30 20:37:47 +04:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent 8574674c2d
commit f388512592
17 changed files with 578 additions and 163 deletions
@@ -134,6 +134,26 @@ class ChatModel(val controller: ChatController) {
}
}
fun removeChatItem(cInfo: ChatInfo, cItem: ChatItem) {
// update previews
val i = getChatIndex(cInfo.id)
val chat: Chat
if (i >= 0) {
chat = chats[i]
val pItem = chat.chatItems.last()
if (pItem.id == cItem.id) {
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
}
}
// remove from current chat
if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems.removeAt(itemIndex)
}
}
}
fun markChatItemsRead(cInfo: ChatInfo) {
val chatIdx = getChatIndex(cInfo.id)
// update current chat
@@ -490,6 +510,20 @@ data class ChatItem (
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.memberProfile.displayName
else null
val isMsgContent: Boolean get() =
when (content) {
is CIContent.SndMsgContent -> true
is CIContent.RcvMsgContent -> true
else -> false
}
val isDeletedContent: Boolean get() =
when (content) {
is CIContent.SndDeleted -> true
is CIContent.RcvDeleted -> true
else -> false
}
companion object {
fun getSampleData(
id: Long = 1,
@@ -508,6 +542,20 @@ data class ChatItem (
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem
)
fun getDeletedContentSampleData(
id: Long = 1,
dir: CIDirection = CIDirection.DirectRcv(),
ts: Instant = Clock.System.now(),
text: String = "this item is deleted",
status: CIStatus = CIStatus.RcvRead()
) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, false, false, false),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null
)
}
}
@@ -598,6 +646,12 @@ sealed class CIStatus {
class RcvRead: CIStatus()
}
@Serializable
enum class CIDeleteMode(val deleteMode: String) {
@SerialName("internal") cidmInternal("internal"),
@SerialName("broadcast") cidmBroadcast("broadcast");
}
interface ItemContent {
val text: String
}
@@ -616,6 +670,16 @@ sealed class CIContent: ItemContent {
override val text get() = msgContent.text
}
@Serializable @SerialName("sndDeleted")
class SndDeleted(val deleteMode: CIDeleteMode): CIContent() {
override val text get() = "deleted"
}
@Serializable @SerialName("rcvDeleted")
class RcvDeleted(val deleteMode: CIDeleteMode): CIContent() {
override val text get() = "deleted"
}
@Serializable @SerialName("sndFileInvitation")
class SndFileInvitation(val fileId: Long, val filePath: String): CIContent() {
override val text get() = "sending files is not supported yet"
@@ -147,17 +147,17 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
return null
}
suspend fun apiUpdateMessage(type: ChatType, id: Long, itemId: Long, mc: MsgContent): AChatItem? {
val r = sendCmd(CC.ApiUpdateMessage(type, id, itemId, mc))
suspend fun apiUpdateChatItem(type: ChatType, id: Long, itemId: Long, mc: MsgContent): AChatItem? {
val r = sendCmd(CC.ApiUpdateChatItem(type, id, itemId, mc))
if (r is CR.ChatItemUpdated) return r.chatItem
Log.e(TAG, "apiUpdateMessage bad response: ${r.responseType} ${r.details}")
Log.e(TAG, "apiUpdateChatItem 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}")
suspend fun apiDeleteChatItem(type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): AChatItem? {
val r = sendCmd(CC.ApiDeleteChatItem(type, id, itemId, mode))
if (r is CR.ChatItemDeleted) return r.toChatItem
Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}")
return null
}
@@ -176,7 +176,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
Log.e(TAG, "setUserSMPServers bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
"Error saving SMP servers",
"Make sure SMP server addresses are in correct format, line separated and are not duplicated"
"Make sure SMP server addresses are in correct format, line separated and are not duplicated."
)
false
}
@@ -196,7 +196,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
r is CR.SentConfirmation || r is CR.SentInvitation -> return true
r is CR.ContactAlreadyExists -> {
AlertManager.shared.showAlertMsg("Contact already exists",
"You are already connected to ${r.contact.displayName} via this link"
"You are already connected to ${r.contact.displayName} via this link."
)
return false
}
@@ -223,7 +223,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) {
AlertManager.shared.showAlertMsg(
"Can't delete contact!",
"Contact ${e.errorType.contact.displayName} cannot be deleted, it is a member of the group(s) ${e.errorType.groupNames}"
"Contact ${e.errorType.contact.displayName} cannot be deleted, it is a member of the group(s) ${e.errorType.groupNames}."
)
}
}
@@ -336,7 +336,11 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
is CR.ChatItemStatusUpdated -> {
val cInfo = r.chatItem.chatInfo
val cItem = r.chatItem.chatItem
if (chatModel.upsertChatItem(cInfo, cItem)) {
var res = false
if (!cItem.isDeletedContent) {
res = chatModel.upsertChatItem(cInfo, cItem)
}
if (res) {
ntfManager.notifyMessageReceived(cInfo, cItem)
}
}
@@ -348,7 +352,14 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
}
}
is CR.ChatItemDeleted -> {
// TODO
val cInfo = r.toChatItem.chatInfo
val cItem = r.toChatItem.chatItem
if (cItem.meta.itemDeleted) {
chatModel.removeChatItem(cInfo, cItem)
} else {
// currently only broadcast deletion of rcv message can be received, and only this case should happen
chatModel.upsertChatItem(cInfo, cItem)
}
}
else ->
Log.d(TAG , "unsupported event: ${r.responseType}")
@@ -450,11 +461,6 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
}
}
enum class MsgDeleteMode(val mode: String) {
Broadcast("broadcast"),
Internal("internal");
}
// ChatCommand
sealed class CC {
class Console(val cmd: String): CC()
@@ -465,8 +471,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 ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class GetUserSMPServers(): CC()
class SetUserSMPServers(val smpServers: List<String>): CC()
class AddContact: CC()
@@ -489,8 +495,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 ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId ${mc.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
is GetUserSMPServers -> "/smp_servers"
is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}"
is AddContact -> "/connect"
@@ -514,8 +520,8 @@ sealed class CC {
is ApiGetChat -> "apiGetChat"
is ApiSendMessage -> "apiSendMessage"
is ApiSendMessageQuote -> "apiSendMessageQuote"
is ApiUpdateMessage -> "apiUpdateMessage"
is ApiDeleteMessage -> "apiDeleteMessage"
is ApiUpdateChatItem -> "apiUpdateChatItem"
is ApiDeleteChatItem -> "apiDeleteChatItem"
is GetUserSMPServers -> "getUserSMPServers"
is SetUserSMPServers -> "setUserSMPServers"
is AddContact -> "addContact"
@@ -601,7 +607,7 @@ sealed class 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("chatItemDeleted") class ChatItemDeleted(val deletedChatItem: AChatItem, val toChatItem: 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()
@@ -682,7 +688,7 @@ sealed class CR {
is NewChatItem -> json.encodeToString(chatItem)
is ChatItemStatusUpdated -> json.encodeToString(chatItem)
is ChatItemUpdated -> json.encodeToString(chatItem)
is ChatItemDeleted -> json.encodeToString(chatItem)
is ChatItemDeleted -> "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}"
is CmdOk -> noDetails()
is ChatCmdError -> chatError.string
is ChatRespError -> chatError.string
@@ -77,7 +77,7 @@ fun ChatView(chatModel: ChatModel) {
val cInfo = chat.chatInfo
val ei = editingItem.value
if (ei != null) {
val updatedItem = chatModel.controller.apiUpdateMessage(
val updatedItem = chatModel.controller.apiUpdateChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = ei.meta.itemId,
@@ -98,7 +98,19 @@ fun ChatView(chatModel: ChatModel) {
quotedItem.value = null
}
},
resetMessage = { msg.value = "" }
resetMessage = { msg.value = "" },
deleteMessage = { itemId, mode ->
withApi {
val cInfo = chat.chatInfo
val toItem = chatModel.controller.apiDeleteChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = itemId,
mode = mode
)
if (toItem != null) chatModel.removeChatItem(cInfo, toItem.chatItem)
}
}
)
}
}
@@ -115,7 +127,8 @@ fun ChatLayout(
info: () -> Unit,
openDirectChat: (Long) -> Unit,
sendMessage: (String) -> Unit,
resetMessage: () -> Unit
resetMessage: () -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
Surface(
Modifier
@@ -129,7 +142,7 @@ fun ChatLayout(
modifier = Modifier.navigationBarsWithImePadding()
) { contentPadding ->
Box(Modifier.padding(contentPadding)) {
ChatItemsList(user, chat, chatItems, msg, quotedItem, editingItem, openDirectChat)
ChatItemsList(user, chat, chatItems, msg, quotedItem, editingItem, openDirectChat, deleteMessage)
}
}
}
@@ -201,7 +214,8 @@ fun ChatItemsList(
msg: MutableState<String>,
quotedItem: MutableState<ChatItem?>,
editingItem: MutableState<ChatItem?>,
openDirectChat: (Long) -> Unit
openDirectChat: (Long) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
val listState = rememberLazyListState()
val keyboardState by getKeyboardState()
@@ -224,7 +238,11 @@ fun ChatItemsList(
if (contactId == null) {
MemberImage(member)
} else {
Box(Modifier.clip(CircleShape).clickable { openDirectChat(contactId) }) {
Box(
Modifier
.clip(CircleShape)
.clickable { openDirectChat(contactId) }
) {
MemberImage(member)
}
}
@@ -232,20 +250,22 @@ fun ChatItemsList(
} else {
Spacer(Modifier.size(42.dp))
}
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler, showMember = showMember)
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler, showMember = showMember, deleteMessage = deleteMessage)
}
} else {
Box(Modifier.padding(start = 86.dp, end = 12.dp)) {
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler)
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler, deleteMessage = deleteMessage)
}
}
} else { // direct message
val sent = cItem.chatDir.sent
Box(Modifier.padding(
start = if (sent) 76.dp else 12.dp,
end = if (sent) 12.dp else 76.dp,
)) {
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler)
Box(
Modifier.padding(
start = if (sent) 76.dp else 12.dp,
end = if (sent) 12.dp else 76.dp,
)
) {
ChatItemView(user, cItem, msg, quotedItem, editingItem, cxt, uriHandler, deleteMessage = deleteMessage)
}
}
}
@@ -261,9 +281,7 @@ fun ChatItemsList(
fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean {
return prevItem == null || prevItem.chatDir is CIDirection.GroupSnd ||
( prevItem.chatDir is CIDirection.GroupRcv &&
prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId
)
(prevItem.chatDir is CIDirection.GroupRcv && prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId)
}
@Composable
@@ -287,14 +305,15 @@ fun PreviewChatLayout() {
ChatItem.getSampleData(
2, CIDirection.DirectRcv(), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
3, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
ChatItem.getDeletedContentSampleData(3),
ChatItem.getSampleData(
4, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
5, CIDirection.DirectRcv(), Clock.System.now(), "hello"
5, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
6, CIDirection.DirectRcv(), Clock.System.now(), "hello"
)
)
ChatLayout(
@@ -312,7 +331,8 @@ fun PreviewChatLayout() {
info = {},
openDirectChat = {},
sendMessage = {},
resetMessage = {}
resetMessage = {},
deleteMessage = { _, _ -> }
)
}
}
@@ -328,14 +348,15 @@ fun PreviewGroupChatLayout() {
ChatItem.getSampleData(
2, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
),
ChatItem.getDeletedContentSampleData(3),
ChatItem.getSampleData(
3, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
4, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
4, CIDirection.GroupSnd(), Clock.System.now(), "hello"
5, CIDirection.GroupSnd(), Clock.System.now(), "hello"
),
ChatItem.getSampleData(
5, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
6, CIDirection.GroupRcv(GroupMember.sampleData), Clock.System.now(), "hello"
)
)
ChatLayout(
@@ -353,7 +374,8 @@ fun PreviewGroupChatLayout() {
info = {},
openDirectChat = {},
sendMessage = {},
resetMessage = {}
resetMessage = {},
deleteMessage = { _, _ -> }
)
}
}
@@ -22,13 +22,16 @@ fun CIMetaView(chatItem: ChatItem) {
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,
)
if (!chatItem.isDeletedContent) {
if (chatItem.meta.itemEdited) {
Icon(
Icons.Filled.Edit,
modifier = Modifier.height(12.dp),
contentDescription = "Edited",
tint = HighOrLowlight,
)
}
// TODO status
}
Text(
chatItem.timestampText,
@@ -58,3 +61,11 @@ fun PreviewCIMetaViewEdited() {
)
)
}
@Preview
@Composable
fun PreviewCIMetaViewDeletedContent() {
CIMetaView(
chatItem = ChatItem.getDeletedContentSampleData()
)
}
@@ -9,6 +9,7 @@ import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.UriHandler
@@ -17,8 +18,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.copyText
import chat.simplex.app.views.helpers.shareText
import chat.simplex.app.views.helpers.*
import kotlinx.datetime.Clock
@Composable
@@ -31,6 +31,7 @@ fun ChatItemView(
cxt: Context,
uriHandler: UriHandler? = null,
showMember: Boolean = false,
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
val sent = cItem.chatDir.sent
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
@@ -42,52 +43,96 @@ fun ChatItemView(
contentAlignment = alignment,
) {
Column(Modifier.combinedClickable(onLongClick = { showMenu = true }, onClick = {})) {
if (cItem.quotedItem == null && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem)
} else {
FramedItemView(user, cItem, uriHandler, showMember = showMember)
if (cItem.isMsgContent) {
if (cItem.quotedItem == null && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem)
} else {
FramedItemView(user, cItem, uriHandler, showMember = showMember)
}
} else if (cItem.isDeletedContent) {
DeletedItemView(cItem, showMember = showMember)
}
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
ItemAction("Reply", Icons.Outlined.Reply, onClick = {
editingItem.value = null
quotedItem.value = cItem
showMenu = false
})
ItemAction("Share", Icons.Outlined.Share, onClick = {
shareText(cxt, cItem.content.text)
showMenu = false
})
ItemAction("Copy", Icons.Outlined.ContentCopy, onClick = {
copyText(cxt, cItem.content.text)
showMenu = false
})
// if (cItem.chatDir.sent && cItem.meta.editable) {
// ItemAction("Edit", Icons.Filled.Edit, onClick = {
// quotedItem.value = null
// editingItem.value = cItem
// msg.value = cItem.content.text
// showMenu = false
// })
// }
if (cItem.isMsgContent) {
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
ItemAction("Reply", Icons.Outlined.Reply, onClick = {
editingItem.value = null
quotedItem.value = cItem
showMenu = false
})
ItemAction("Share", Icons.Outlined.Share, onClick = {
shareText(cxt, cItem.content.text)
showMenu = false
})
ItemAction("Copy", Icons.Outlined.ContentCopy, onClick = {
copyText(cxt, cItem.content.text)
showMenu = false
})
if (cItem.chatDir.sent && cItem.meta.editable) {
ItemAction("Edit", Icons.Filled.Edit, onClick = {
quotedItem.value = null
editingItem.value = cItem
msg.value = cItem.content.text
showMenu = false
})
}
ItemAction(
"Delete",
Icons.Outlined.Delete,
onClick = {
showMenu = false
deleteMessageAlertDialog(cItem, deleteMessage = deleteMessage)
},
color = Color.Red
)
}
}
}
}
}
@Composable
private fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit) {
private fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = HighOrLowlight) {
DropdownMenuItem(onClick) {
Row {
Text(
text, modifier = Modifier
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.weight(1F),
color = color
)
Icon(icon, text, tint = HighOrLowlight)
Icon(icon, text, tint = color)
}
}
}
fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialogButtons(
title = "Delete message?",
text = "Message will be deleted - this cannot be undone!",
buttons = {
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.End,
) {
Button(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
AlertManager.shared.hideAlert()
}) { Text("For me only") }
if (chatItem.meta.editable) {
Spacer(Modifier.padding(horizontal = 4.dp))
Button(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
AlertManager.shared.hideAlert()
}) { Text("For everyone") }
}
}
}
)
}
@Preview
@Composable
fun PreviewChatItemView() {
@@ -100,7 +145,24 @@ fun PreviewChatItemView() {
msg = remember { mutableStateOf("") },
quotedItem = remember { mutableStateOf(null) },
editingItem = remember { mutableStateOf(null) },
cxt = LocalContext.current
cxt = LocalContext.current,
deleteMessage = { _, _ -> }
)
}
}
@Preview
@Composable
fun PreviewChatItemViewDeletedContent() {
SimpleXTheme {
ChatItemView(
User.sampleData,
ChatItem.getDeletedContentSampleData(),
msg = remember { mutableStateOf("") },
quotedItem = remember { mutableStateOf(null) },
editingItem = remember { mutableStateOf(null) },
cxt = LocalContext.current,
deleteMessage = { _, _ -> }
)
}
}
@@ -0,0 +1,56 @@
package chat.simplex.app.views.chat.item
import android.content.res.Configuration
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
Surface(
shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight,
) {
Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.Bottom
) {
Text(
buildAnnotatedString {
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(ci.content.text) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci)
}
}
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode"
)
@Composable
fun PreviewDeletedItemView() {
SimpleXTheme {
DeletedItemView(
ChatItem.getDeletedContentSampleData()
)
}
}
@@ -43,7 +43,7 @@ suspend fun openChat(chatModel: ChatModel, cInfo: ChatInfo) {
fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
AlertManager.shared.showAlertDialog(
title = "Accept connection request?",
text = "If you choose to reject sender will NOT be notified",
text = "If you choose to reject sender will NOT be notified.",
confirmText = "Accept",
onConfirm = {
withApi {
@@ -21,6 +21,22 @@ class AlertManager {
alertView.value = null
}
fun showAlertDialogButtons(
title: String,
text: String? = null,
buttons: @Composable () -> Unit,
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = this::hideAlert,
title = { Text(title) },
text = alertText,
buttons = buttons
)
}
}
fun showAlertDialog(
title: String,
text: String? = null,
@@ -61,7 +61,7 @@ fun SMPServersView(chatModel: ChatModel) {
if (userSMPServers.isNotEmpty()) {
AlertManager.shared.showAlertMsg(
title = "Use SimpleX Chat servers?",
text = "Saved SMP servers will be removed",
text = "Saved SMP servers will be removed.",
confirmText = "Confirm",
onConfirm = {
saveSMPServers(listOf())
@@ -33,7 +33,7 @@ fun UserAddressView(chatModel: ChatModel) {
deleteAddress = {
AlertManager.shared.showAlertMsg(
title = "Delete address?",
text = "All your contacts will remain connected",
text = "All your contacts will remain connected.",
confirmText = "Delete",
onConfirm = {
withApi {
+55 -4
View File
@@ -135,6 +135,23 @@ final class ChatModel: ObservableObject {
return res
}
}
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
// update previews
if let chat = getChat(cInfo.id) {
if let pItem = chat.chatItems.last, pItem.id == cItem.id {
chat.chatItems = [cItem]
}
}
// remove from current chat
if chatId == cInfo.id {
if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) {
_ = withAnimation {
self.chatItems.remove(at: i)
}
}
}
}
func markChatItemsRead(_ cInfo: ChatInfo) {
// update preview
@@ -571,6 +588,22 @@ struct ChatItem: Identifiable, Decodable {
if case .rcvNew = meta.itemStatus { return true }
return false
}
func isMsgContent() -> Bool {
switch content {
case .sndMsgContent: return true
case .rcvMsgContent: return true
default: return false
}
}
func isDeletedContent() -> Bool {
switch content {
case .sndDeleted: return true
case .rcvDeleted: return true
default: return false
}
}
var memberDisplayName: String? {
get {
@@ -584,10 +617,19 @@ struct ChatItem: Identifiable, Decodable {
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, itemDeleted, itemEdited, editable),
content: .sndMsgContent(msgContent: .text(text)),
quotedItem: quotedItem
chatDir: dir,
meta: CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
content: .sndMsgContent(msgContent: .text(text)),
quotedItem: quotedItem
)
}
static func getDeletedContentSample (_ id: Int64 = 1, dir: CIDirection = .directRcv, _ ts: Date = .now, _ text: String = "this item is deleted", _ status: CIStatus = .rcvRead) -> ChatItem {
ChatItem(
chatDir: dir,
meta: CIMeta.getSample(id, ts, text, status, false, false, false),
content: .rcvDeleted(deleteMode: .cidmBroadcast),
quotedItem: nil
)
}
}
@@ -655,6 +697,11 @@ enum CIStatus: Decodable {
case rcvRead
}
enum CIDeleteMode: String, Decodable {
case cidmBroadcast = "broadcast"
case cidmInternal = "internal"
}
protocol ItemContent {
var text: String { get }
}
@@ -662,6 +709,8 @@ protocol ItemContent {
enum CIContent: Decodable, ItemContent {
case sndMsgContent(msgContent: MsgContent)
case rcvMsgContent(msgContent: MsgContent)
case sndDeleted(deleteMode: CIDeleteMode)
case rcvDeleted(deleteMode: CIDeleteMode)
case sndFileInvitation(fileId: Int64, filePath: String)
case rcvFileInvitation(rcvFileTransfer: RcvFileTransfer)
@@ -670,6 +719,8 @@ enum CIContent: Decodable, ItemContent {
switch self {
case let .sndMsgContent(mc): return mc.text
case let .rcvMsgContent(mc): return mc.text
case .sndDeleted: return "deleted"
case .rcvDeleted: return "deleted"
case .sndFileInvitation: return "sending files is not supported yet"
case .rcvFileInvitation: return "receiving files is not supported yet"
}
+29 -23
View File
@@ -15,11 +15,6 @@ 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)
@@ -28,8 +23,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 apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent)
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
case getUserSMPServers
case setUserSMPServers(smpServers: [String])
case addContact
@@ -54,8 +49,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 let .apiUpdateChatItem(type, id, itemId, mc): return "/_update item \(ref(type, id)) \(itemId) \(mc.cmdString)"
case let .apiDeleteChatItem(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"
@@ -83,8 +78,8 @@ enum ChatCommand {
case .apiGetChat: return "apiGetChat"
case .apiSendMessage: return "apiSendMessage"
case .apiSendMessageQuote: return "apiSendMessageQuote"
case .apiUpdateMessage: return "apiUpdateMessage"
case .apiDeleteMessage: return "apiDeleteMessage"
case .apiUpdateChatItem: return "apiUpdateChatItem"
case .apiDeleteChatItem: return "apiDeleteChatItem"
case .getUserSMPServers: return "getUserSMPServers"
case .setUserSMPServers: return "setUserSMPServers"
case .addContact: return "addContact"
@@ -148,7 +143,7 @@ enum ChatResponse: Decodable, Error {
case newChatItem(chatItem: AChatItem)
case chatItemStatusUpdated(chatItem: AChatItem)
case chatItemUpdated(chatItem: AChatItem)
case chatItemDeleted(chatItem: AChatItem)
case chatItemDeleted(deletedChatItem: AChatItem, toChatItem: AChatItem)
case cmdOk
case chatCmdError(chatError: ChatError)
case chatError(chatError: ChatError)
@@ -231,7 +226,7 @@ enum ChatResponse: Decodable, Error {
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 let .chatItemDeleted(deletedChatItem, toChatItem): return "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))"
case .cmdOk: return noDetails
case let .chatCmdError(chatError): return String(describing: chatError)
case let .chatError(chatError): return String(describing: chatError)
@@ -410,15 +405,15 @@ 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)
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) async throws -> ChatItem {
let r = await chatSendCmd(.apiUpdateChatItem(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 }
func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> ChatItem {
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
if case let .chatItemDeleted(_, toChatItem) = r { return toChatItem.chatItem }
throw r
}
@@ -633,7 +628,11 @@ func processReceivedMsg(_ res: ChatResponse) {
case let .chatItemStatusUpdated(aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if chatModel.upsertChatItem(cInfo, cItem) {
var res = false
if !cItem.isDeletedContent() {
res = chatModel.upsertChatItem(cInfo, cItem)
}
if res {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
} else if let endTask = chatModel.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
@@ -649,9 +648,15 @@ func processReceivedMsg(_ res: ChatResponse) {
if chatModel.upsertChatItem(cInfo, cItem) {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
}
case .chatItemDeleted(_):
// TODO let .chatItemDeleted(aChatItem)
return
case let .chatItemDeleted(_, toChatItem):
let cInfo = toChatItem.chatInfo
let cItem = toChatItem.chatItem
if cItem.meta.itemDeleted {
chatModel.removeChatItem(cInfo, cItem)
} else {
// currently only broadcast deletion of rcv message can be received, and only this case should happen
_ = chatModel.upsertChatItem(cInfo, cItem)
}
default:
logger.debug("unsupported event: \(res.responseType)")
}
@@ -784,7 +789,8 @@ enum ChatErrorType: Decodable {
case fileRcvChunk(message: String)
case fileInternal(message: String)
case invalidQuote
case invalidMessageUpdate
case invalidChatItemUpdate
case invalidChatItemDelete
case agentVersion
case commandError(message: String)
}
@@ -13,20 +13,22 @@ struct CIMetaView: View {
var body: some View {
HStack(alignment: .center, spacing: 4) {
if chatItem.meta.itemEdited {
statusImage("pencil", .secondary, 9)
}
if !chatItem.isDeletedContent() {
if chatItem.meta.itemEdited {
statusImage("pencil", .secondary, 9)
}
switch chatItem.meta.itemStatus {
case .sndSent:
statusImage("checkmark", .secondary)
case .sndErrorAuth:
statusImage("multiply", .red)
case .sndError:
statusImage("exclamationmark.triangle.fill", .yellow)
case .rcvNew:
statusImage("circlebadge.fill", Color.accentColor)
default: EmptyView()
switch chatItem.meta.itemStatus {
case .sndSent:
statusImage("checkmark", .secondary)
case .sndErrorAuth:
statusImage("multiply", .red)
case .sndError:
statusImage("exclamationmark.triangle.fill", .yellow)
case .rcvNew:
statusImage("circlebadge.fill", Color.accentColor)
default: EmptyView()
}
}
chatItem.timestampText
@@ -49,6 +51,8 @@ struct CIMetaView_Previews: PreviewProvider {
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))
CIMetaView(chatItem: ChatItem.getDeletedContentSample())
}
.previewLayout(.fixed(width: 360, height: 100))
}
}
@@ -0,0 +1,51 @@
//
// FramedItemView.swift
// SimpleX
//
// Created by JRoberts on 04/02/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct DeletedItemView: View {
@Environment(\.colorScheme) var colorScheme
var chatItem: ChatItem
var showMember = false
var body: some View {
HStack(alignment: .bottom, spacing: 0) {
if showMember, let member = chatItem.memberDisplayName {
Text(member).fontWeight(.medium) + Text(": ")
}
Text(chatItem.content.text)
.foregroundColor(.secondary)
.italic()
CIMetaView(chatItem: chatItem)
.padding(.horizontal, 12)
}
.padding(.leading, 12)
.padding(.vertical, 6)
.background(Color(uiColor: .tertiarySystemGroupedBackground))
.cornerRadius(18)
.textSelection(.disabled)
// .background(Color(uiColor: .systemBackground))
// .overlay(
// RoundedRectangle(cornerRadius: 18)
// .stroke(.quaternary, lineWidth: 1)
// )
}
}
struct DeletedItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
DeletedItemView(chatItem: ChatItem.getDeletedContentSample())
DeletedItemView(
chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)),
showMember: true
)
}
.previewLayout(.fixed(width: 360, height: 200))
}
}
@@ -13,10 +13,14 @@ struct ChatItemView: View {
var showMember = false
var body: some View {
if (chatItem.quotedItem == nil && isShortEmoji(chatItem.content.text)) {
EmojiItemView(chatItem: chatItem)
} else {
FramedItemView(chatItem: chatItem, showMember: showMember)
if chatItem.isMsgContent() {
if (chatItem.quotedItem == nil && isShortEmoji(chatItem.content.text)) {
EmojiItemView(chatItem: chatItem)
} else {
FramedItemView(chatItem: chatItem, showMember: showMember)
}
} else if chatItem.isDeletedContent() {
DeletedItemView(chatItem: chatItem, showMember: showMember)
}
}
}
@@ -29,6 +33,7 @@ struct ChatItemView_Previews: PreviewProvider {
ChatItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂"))
ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"))
ChatItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"))
ChatItemView(chatItem: ChatItem.getDeletedContentSample())
}
.previewLayout(.fixed(width: 360, height: 70))
}
+71 -26
View File
@@ -17,9 +17,11 @@ struct ChatView: View {
@State var message: String = ""
@State var quotedItem: ChatItem? = nil
@State var editingItem: ChatItem? = nil
@State var deletingItem: ChatItem? = nil
@State private var inProgress: Bool = false
@FocusState private var keyboardVisible: Bool
@State private var showChatInfo = false
@State private var showDeleteMessage = false
var body: some View {
let cInfo = chat.chatInfo
@@ -118,27 +120,46 @@ struct ChatView: View {
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
return ChatItemView(chatItem: ci, showMember: showMember)
.contextMenu {
Button {
withAnimation {
editingItem = nil
quotedItem = ci
if ci.isMsgContent() {
Button {
withAnimation {
editingItem = nil
quotedItem = ci
}
} label: { Label("Reply", systemImage: "arrowshape.turn.up.left") }
Button {
showShareSheet(items: [ci.content.text])
} label: { Label("Share", systemImage: "square.and.arrow.up") }
Button {
UIPasteboard.general.string = ci.content.text
} label: { Label("Copy", systemImage: "doc.on.doc") }
if ci.meta.editable {
Button {
withAnimation {
quotedItem = nil
editingItem = ci
message = ci.content.text
}
} label: { Label("Edit", systemImage: "square.and.pencil") }
}
} label: { Label("Reply", systemImage: "arrowshape.turn.up.left") }
Button {
showShareSheet(items: [ci.content.text])
} label: { Label("Share", systemImage: "square.and.arrow.up") }
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") }
// }
Button(role: .destructive) {
showDeleteMessage = true
deletingItem = ci
} label: {
Label("Delete", systemImage: "trash")
}
}
}
.confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) {
Button("Delete for me", role: .destructive) {
deleteMessage(.cidmInternal)
}
if let di = deletingItem {
if di.meta.editable {
Button("Delete for everyone",role: .destructive) { deleteMessage(.cidmBroadcast)
}
}
}
}
.frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment)
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
@@ -185,7 +206,7 @@ struct ChatView: View {
logger.debug("ChatView sendMessage: in Task")
do {
if let ei = editingItem {
let chatItem = try await apiUpdateMessage(
let chatItem = try await apiUpdateChatItem(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemId: ei.id,
@@ -212,6 +233,29 @@ struct ChatView: View {
}
}
}
func deleteMessage(_ mode: CIDeleteMode) {
logger.debug("ChatView deleteMessage")
Task {
logger.debug("ChatView deleteMessage: in Task")
do {
if let di = deletingItem {
let toItem = try await apiDeleteChatItem(
type: chat.chatInfo.chatType,
id: chat.chatInfo.apiId,
itemId: di.id,
mode: mode
)
DispatchQueue.main.async {
deletingItem = nil
let _ = chatModel.removeChatItem(chat.chatInfo, toItem)
}
}
} catch {
logger.error("ChatView.deleteMessage error: \(error.localizedDescription)")
}
}
}
}
struct ChatView_Previews: PreviewProvider {
@@ -222,11 +266,12 @@ struct ChatView_Previews: PreviewProvider {
ChatItem.getSample(1, .directSnd, .now, "hello"),
ChatItem.getSample(2, .directRcv, .now, "hi"),
ChatItem.getSample(3, .directRcv, .now, "hi there"),
ChatItem.getSample(4, .directRcv, .now, "hello again"),
ChatItem.getSample(5, .directSnd, .now, "hi there!!!"),
ChatItem.getSample(6, .directSnd, .now, "how are you?"),
ChatItem.getSample(7, .directSnd, .now, "👍👍👍👍"),
ChatItem.getSample(8, .directSnd, .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.")
ChatItem.getDeletedContentSample(4),
ChatItem.getSample(5, .directRcv, .now, "hello again"),
ChatItem.getSample(6, .directSnd, .now, "hi there!!!"),
ChatItem.getSample(7, .directSnd, .now, "how are you?"),
ChatItem.getSample(8, .directSnd, .now, "👍👍👍👍"),
ChatItem.getSample(9, .directSnd, .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.")
]
return ChatView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []))
.environmentObject(chatModel)
@@ -116,6 +116,13 @@
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 */; };
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64AA1C6D27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
64C2576927F45A42007F8541 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C2576427F45A42007F8541 /* libgmpxx.a */; };
64C2576A27F45A42007F8541 /* libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C2576527F45A42007F8541 /* libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou-ghc8.10.7.a */; };
64C2576B27F45A42007F8541 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C2576627F45A42007F8541 /* libgmp.a */; };
64C2576C27F45A42007F8541 /* libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C2576727F45A42007F8541 /* libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou.a */; };
64C2576D27F45A42007F8541 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C2576827F45A42007F8541 /* libffi.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -199,6 +206,12 @@
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; };
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
64C2576427F45A42007F8541 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64C2576527F45A42007F8541 /* libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou-ghc8.10.7.a"; sourceTree = "<group>"; };
64C2576627F45A42007F8541 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C2576727F45A42007F8541 /* libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.4.0-35IBkEJuAyg38MasSQs4Ou.a"; sourceTree = "<group>"; };
64C2576827F45A42007F8541 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -431,6 +444,7 @@
5CE4407827ADB701007B033A /* EmojiItemView.swift */,
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */,
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */,
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */,
);
path = ChatItem;
sourceTree = "<group>";
@@ -653,6 +667,7 @@
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5CE4407227ADB1D0007B033A /* Emoji.swift in Sources */,
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
@@ -704,6 +719,7 @@
5C2E260827A2941F00F70299 /* SimpleXAPI.swift in Sources */,
5CB924D527A853F100ACCCDD /* SettingsButton.swift in Sources */,
5C5F2B7127EBC704006A9D5F /* ProfileImage.swift in Sources */,
64AA1C6D27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5CE4407327ADB1D0007B033A /* Emoji.swift in Sources */,
5C1A4C1F27A715B700EAD5AD /* ChatItemView.swift in Sources */,
64AA1C6A27EE10C800AC7277 /* ContextItemView.swift in Sources */,