From 682dfe503cdcaea6cb16d6f9eabe6fdcdb6afd3d Mon Sep 17 00:00:00 2001
From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
Date: Thu, 28 Sep 2023 13:52:43 +0400
Subject: [PATCH] android, desktop: notify contact about contact deletion
(#3139)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
---
.../chat/simplex/common/model/ChatModel.kt | 23 ++++++-
.../chat/simplex/common/model/SimpleXAPI.kt | 10 +++
.../simplex/common/views/chat/ChatInfoView.kt | 5 +-
.../simplex/common/views/chat/ChatView.kt | 15 ++--
.../common/views/chat/item/ChatItemView.kt | 1 +
.../common/views/chatlist/ChatPreviewView.kt | 68 +++++++++++--------
.../commonMain/resources/MR/base/strings.xml | 3 +
7 files changed, 87 insertions(+), 38 deletions(-)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
index 887abe756..0eb02515b 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt
@@ -797,6 +797,7 @@ data class Contact(
val activeConn: Connection,
val viaGroup: Long? = null,
val contactUsed: Boolean,
+ val contactStatus: ContactStatus,
val chatSettings: ChatSettings,
val userPreferences: ChatPreferences,
val mergedPreferences: ContactUserPreferences,
@@ -809,8 +810,9 @@ data class Contact(
override val id get() = "@$contactId"
override val apiId get() = contactId
override val ready get() = activeConn.connStatus == ConnStatus.Ready
+ val active get() = contactStatus == ContactStatus.Active
override val sendMsgEnabled get() =
- (ready && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
+ (ready && active && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
|| nextSendGrpInv
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
override val ntfsEnabled get() = chatSettings.enableNtfs
@@ -859,6 +861,7 @@ data class Contact(
profile = LocalProfile.sampleData,
activeConn = Connection.sampleData,
contactUsed = true,
+ contactStatus = ContactStatus.Active,
chatSettings = ChatSettings(enableNtfs = true, sendRcpts = null, favorite = false),
userPreferences = ChatPreferences.sampleData,
mergedPreferences = ContactUserPreferences.sampleData,
@@ -869,6 +872,12 @@ data class Contact(
}
}
+@Serializable
+enum class ContactStatus {
+ @SerialName("active") Active,
+ @SerialName("deleted") Deleted;
+}
+
@Serializable
class ContactRef(
val contactId: Long,
@@ -1471,6 +1480,7 @@ data class ChatItem (
is CIContent.RcvDecryptionError -> showNtfDir
is CIContent.RcvGroupInvitation -> showNtfDir
is CIContent.SndGroupInvitation -> showNtfDir
+ is CIContent.RcvDirectEventContent -> false
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
is RcvGroupEvent.MemberAdded -> false
is RcvGroupEvent.MemberConnected -> false
@@ -1854,6 +1864,7 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
+ @Serializable @SerialName("rcvDirectEvent") class RcvDirectEventContent(val rcvDirectEvent: RcvDirectEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@@ -1881,6 +1892,7 @@ sealed class CIContent: ItemContent {
is RcvDecryptionError -> msgDecryptError.text
is RcvGroupInvitation -> groupInvitation.text
is SndGroupInvitation -> groupInvitation.text
+ is RcvDirectEventContent -> rcvDirectEvent.text
is RcvGroupEventContent -> rcvGroupEvent.text
is SndGroupEventContent -> sndGroupEvent.text
is RcvConnEventContent -> rcvConnEvent.text
@@ -2487,6 +2499,15 @@ sealed class MsgErrorType() {
}
}
+@Serializable
+sealed class RcvDirectEvent() {
+ @Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent()
+
+ val text: String get() = when (this) {
+ is ContactDeleted -> generalGetString(MR.strings.rcv_direct_event_contact_deleted)
+ }
+}
+
@Serializable
sealed class RcvGroupEvent() {
@Serializable @SerialName("memberAdded") class MemberAdded(val groupMemberId: Long, val profile: Profile): RcvGroupEvent()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
index 87cac179f..619788f6e 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt
@@ -1366,6 +1366,11 @@ object ChatController {
chatModel.removeChat(r.connection.id)
}
}
+ is CR.ContactDeletedByContact -> {
+ if (active(r.user) && r.contact.directOrUsed) {
+ chatModel.updateContact(r.contact)
+ }
+ }
is CR.ContactConnected -> {
if (active(r.user) && r.contact.directOrUsed) {
chatModel.updateContact(r.contact)
@@ -3304,6 +3309,7 @@ sealed class CR {
@Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR()
+ @Serializable @SerialName("contactDeletedByContact") class ContactDeletedByContact(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR()
@Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR()
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR()
@@ -3435,6 +3441,7 @@ sealed class CR {
is ContactAlreadyExists -> "contactAlreadyExists"
is ContactRequestAlreadyAccepted -> "contactRequestAlreadyAccepted"
is ContactDeleted -> "contactDeleted"
+ is ContactDeletedByContact -> "contactDeletedByContact"
is ChatCleared -> "chatCleared"
is UserProfileNoChange -> "userProfileNoChange"
is UserProfileUpdated -> "userProfileUpdated"
@@ -3563,6 +3570,7 @@ sealed class CR {
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
is ContactDeleted -> withUser(user, json.encodeToString(contact))
+ is ContactDeletedByContact -> withUser(user, json.encodeToString(contact))
is ChatCleared -> withUser(user, json.encodeToString(chatInfo))
is UserProfileNoChange -> withUser(user, noDetails())
is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile))
@@ -3831,6 +3839,7 @@ sealed class ChatErrorType {
is InvalidConnReq -> "invalidConnReq"
is InvalidChatMessage -> "invalidChatMessage"
is ContactNotReady -> "contactNotReady"
+ is ContactNotActive -> "contactNotActive"
is ContactDisabled -> "contactDisabled"
is ConnectionDisabled -> "connectionDisabled"
is GroupUserRole -> "groupUserRole"
@@ -3906,6 +3915,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType()
+ @Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType()
@Serializable @SerialName("contactDisabled") class ContactDisabled(val contact: Contact): ChatErrorType()
@Serializable @SerialName("connectionDisabled") class ConnectionDisabled(val connection: Connection): ChatErrorType()
@Serializable @SerialName("groupUserRole") class GroupUserRole(val groupInfo: GroupInfo, val requiredRole: GroupMemberRole): ChatErrorType()
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
index 5fcb90c1c..1ba742b03 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.*
import androidx.compose.material.*
import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -291,7 +290,7 @@ fun ChatInfoLayout(
SectionDividerSpaced()
}
- if (contact.ready) {
+ if (contact.ready && contact.active) {
SectionView {
if (connectionCode != null) {
VerifyCodeButton(contact.verified, verifyClicked)
@@ -318,7 +317,7 @@ fun ChatInfoLayout(
SectionDividerSpaced()
}
- if (contact.ready) {
+ if (contact.ready && contact.active) {
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SectionItemView({
AlertManager.shared.showAlertMsg(
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
index 596a7426a..b22b2f91c 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt
@@ -118,7 +118,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
- if (chat.chatInfo is ChatInfo.Direct && !chat.chatInfo.contact.ready && !chat.chatInfo.contact.nextSendGrpInv) {
+ if (
+ chat.chatInfo is ChatInfo.Direct
+ && !chat.chatInfo.contact.ready
+ && chat.chatInfo.contact.active
+ && !chat.chatInfo.contact.nextSendGrpInv
+ ) {
Text(
generalGetString(MR.strings.contact_connection_pending),
Modifier.padding(top = 4.dp),
@@ -550,15 +555,15 @@ fun ChatInfoToolbar(
showMenu.value = false
startCall(CallMediaType.Audio)
},
- enabled = chat.chatInfo.contact.ready) {
+ enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
Icon(
painterResource(MR.images.ic_call_500),
stringResource(MR.strings.icon_descr_more_button),
- tint = if (chat.chatInfo.contact.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
+ tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
}
- if (chat.chatInfo.contact.ready) {
+ if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
menuItems.add {
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
showMenu.value = false
@@ -576,7 +581,7 @@ fun ChatInfoToolbar(
}
}
}
- if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready) || chat.chatInfo is ChatInfo.Group) {
+ if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready && chat.chatInfo.contact.active) || chat.chatInfo is ChatInfo.Group) {
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
menuItems.add {
ItemAction(
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
index e1d45ffb3..b5236e249 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt
@@ -352,6 +352,7 @@ fun ChatItemView(
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
+ is CIContent.RcvDirectEventContent -> EventItemView()
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt
index 780e3515d..a5775d369 100644
--- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt
+++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt
@@ -42,7 +42,7 @@ fun ChatPreviewView(
val cInfo = chat.chatInfo
@Composable
- fun groupInactiveIcon() {
+ fun inactiveIcon() {
Icon(
painterResource(MR.images.ic_cancel_filled),
stringResource(MR.strings.icon_descr_group_inactive),
@@ -53,13 +53,19 @@ fun ChatPreviewView(
@Composable
fun chatPreviewImageOverlayIcon() {
- if (cInfo is ChatInfo.Group) {
- when (cInfo.groupInfo.membership.memberStatus) {
- GroupMemberStatus.MemLeft -> groupInactiveIcon()
- GroupMemberStatus.MemRemoved -> groupInactiveIcon()
- GroupMemberStatus.MemGroupDeleted -> groupInactiveIcon()
- else -> {}
+ when (cInfo) {
+ is ChatInfo.Direct ->
+ if (!cInfo.contact.active) {
+ inactiveIcon()
+ }
+ is ChatInfo.Group ->
+ when (cInfo.groupInfo.membership.memberStatus) {
+ GroupMemberStatus.MemLeft -> inactiveIcon()
+ GroupMemberStatus.MemRemoved -> inactiveIcon()
+ GroupMemberStatus.MemGroupDeleted -> inactiveIcon()
+ else -> {}
}
+ else -> {}
}
}
@@ -125,7 +131,7 @@ fun ChatPreviewView(
if (cInfo.contact.verified) {
VerifiedIcon()
}
- chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else MaterialTheme.colors.secondary)
+ chatPreviewTitleText()
}
is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) {
@@ -174,7 +180,7 @@ fun ChatPreviewView(
is ChatInfo.Direct ->
if (cInfo.contact.nextSendGrpInv) {
Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary)
- } else if (!cInfo.ready) {
+ } else if (!cInfo.ready && cInfo.contact.active) {
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
}
is ChatInfo.Group ->
@@ -191,28 +197,32 @@ fun ChatPreviewView(
@Composable
fun chatStatusImage() {
if (cInfo is ChatInfo.Direct) {
- val descr = contactNetworkStatus?.statusString
- when (contactNetworkStatus) {
- is NetworkStatus.Connected ->
- IncognitoIcon(chat.chatInfo.incognito)
+ if (cInfo.contact.active) {
+ val descr = contactNetworkStatus?.statusString
+ when (contactNetworkStatus) {
+ is NetworkStatus.Connected ->
+ IncognitoIcon(chat.chatInfo.incognito)
- is NetworkStatus.Error ->
- Icon(
- painterResource(MR.images.ic_error),
- contentDescription = descr,
- tint = MaterialTheme.colors.secondary,
- modifier = Modifier
- .size(19.dp)
- )
+ is NetworkStatus.Error ->
+ Icon(
+ painterResource(MR.images.ic_error),
+ contentDescription = descr,
+ tint = MaterialTheme.colors.secondary,
+ modifier = Modifier
+ .size(19.dp)
+ )
- else ->
- CircularProgressIndicator(
- Modifier
- .padding(horizontal = 2.dp)
- .size(15.dp),
- color = MaterialTheme.colors.secondary,
- strokeWidth = 1.5.dp
- )
+ else ->
+ CircularProgressIndicator(
+ Modifier
+ .padding(horizontal = 2.dp)
+ .size(15.dp),
+ color = MaterialTheme.colors.secondary,
+ strokeWidth = 1.5.dp
+ )
+ }
+ } else {
+ IncognitoIcon(chat.chatInfo.incognito)
}
} else {
IncognitoIcon(chat.chatInfo.incognito)
diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
index ab0d943f3..26e9948d6 100644
--- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
+++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml
@@ -1105,6 +1105,9 @@
You rejected group invitation
Group invitation expired
+
+ deleted contact
+
invited %1$s
connected