android, desktop: notify contact about contact deletion (#3139)

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
spaced4ndy 2023-09-28 13:52:43 +04:00 committed by GitHub
parent 957f3b3eb0
commit 682dfe503c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 87 additions and 38 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1105,6 +1105,9 @@
<string name="you_rejected_group_invitation">You rejected group invitation</string>
<string name="group_invitation_expired">Group invitation expired</string>
<!-- Direct event chat items -->
<string name="rcv_direct_event_contact_deleted">deleted contact</string>
<!-- Group event chat items -->
<string name="rcv_group_event_member_added">invited %1$s</string>
<string name="rcv_group_event_member_connected">connected</string>