android: ratchet synchronization (#2666)

* android: ratchet synchronization

* member info

* item

* icons

* update contact keeping stats

* update members keep stats

* update texts

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
spaced4ndy
2023-07-10 19:01:51 +04:00
committed by GitHub
parent a6a87cb7de
commit dcedbac379
28 changed files with 656 additions and 64 deletions
@@ -7,7 +7,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.chat.ComposeState
@@ -131,13 +130,36 @@ object ChatModel {
fun updateChatInfo(cInfo: ChatInfo) {
val i = getChatIndex(cInfo.id)
if (i >= 0) chats[i] = chats[i].copy(chatInfo = cInfo)
if (i >= 0) {
val currentCInfo = chats[i].chatInfo
var newCInfo = cInfo
if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) {
val currentStats = currentCInfo.contact.activeConn.connectionStats
val newStats = newCInfo.contact.activeConn.connectionStats
if (currentStats != null && newStats == null) {
newCInfo = newCInfo.copy(
contact = newCInfo.contact.copy(
activeConn = newCInfo.contact.activeConn.copy(
connectionStats = currentStats
)
)
)
}
}
chats[i] = chats[i].copy(chatInfo = newCInfo)
}
}
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
fun updateContactConnectionStats(contact: Contact, connectionStats: ConnectionStats) {
val updatedConn = contact.activeConn.copy(connectionStats = connectionStats)
val updatedContact = contact.copy(activeConn = updatedConn)
updateContact(updatedContact)
}
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
private fun updateChat(cInfo: ChatInfo, addMissing: Boolean = true) {
@@ -436,6 +458,15 @@ object ChatModel {
}
}
fun updateGroupMemberConnectionStats(groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) {
val memberConn = member.activeConn
if (memberConn != null) {
val updatedConn = memberConn.copy(connectionStats = connectionStats)
val updatedMember = member.copy(activeConn = updatedConn)
upsertGroupMember(groupInfo, updatedMember)
}
}
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
networkStatuses[contact.activeConn.agentConnId] = status
}
@@ -747,7 +778,7 @@ data class Contact(
override val id get() = "@$contactId"
override val apiId get() = contactId
override val ready get() = activeConn.connStatus == ConnStatus.Ready
override val sendMsgEnabled get() = true
override val sendMsgEnabled get() = !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false)
override val ntfsEnabled get() = chatSettings.enableNtfs
override val incognito get() = contactConnIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
@@ -827,7 +858,8 @@ data class Connection(
val connLevel: Int,
val viaGroupLink: Boolean,
val customUserProfileId: Long? = null,
val connectionCode: SecurityCode? = null
val connectionCode: SecurityCode? = null,
val connectionStats: ConnectionStats? = null
) {
val id: ChatId get() = ":$connId"
companion object {
@@ -1768,11 +1800,15 @@ sealed class CIContent: ItemContent {
@Serializable
enum class MsgDecryptError {
@SerialName("ratchetHeader") RatchetHeader,
@SerialName("tooManySkipped") TooManySkipped;
@SerialName("tooManySkipped") TooManySkipped,
@SerialName("ratchetEarlier") RatchetEarlier,
@SerialName("other") Other;
val text: String get() = when (this) {
RatchetHeader -> generalGetString(MR.strings.decryption_error)
TooManySkipped -> generalGetString(MR.strings.decryption_error)
RatchetEarlier -> generalGetString(MR.strings.decryption_error)
Other -> generalGetString(MR.strings.decryption_error)
}
}
@@ -2328,18 +2364,33 @@ sealed class SndGroupEvent() {
@Serializable
sealed class RcvConnEvent {
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState): RcvConnEvent()
@Serializable @SerialName("verificationCodeReset") object VerificationCodeReset: RcvConnEvent()
val text: String get() = when (this) {
is SwitchQueue -> when (phase) {
SwitchPhase.Completed -> generalGetString(MR.strings.rcv_conn_event_switch_queue_phase_completed)
else -> generalGetString(MR.strings.rcv_conn_event_switch_queue_phase_changing)
}
is RatchetSync -> ratchetSyncStatusToText(syncStatus)
is VerificationCodeReset -> generalGetString(MR.strings.rcv_conn_event_verification_code_reset)
}
}
fun ratchetSyncStatusToText(ratchetSyncStatus: RatchetSyncState): String {
return when (ratchetSyncStatus) {
RatchetSyncState.Ok -> generalGetString(MR.strings.conn_event_ratchet_sync_ok)
RatchetSyncState.Allowed -> generalGetString(MR.strings.conn_event_ratchet_sync_allowed)
RatchetSyncState.Required -> generalGetString(MR.strings.conn_event_ratchet_sync_required)
RatchetSyncState.Started -> generalGetString(MR.strings.conn_event_ratchet_sync_started)
RatchetSyncState.Agreed -> generalGetString(MR.strings.conn_event_ratchet_sync_agreed)
}
}
@Serializable
sealed class SndConnEvent {
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
@Serializable @SerialName("ratchetSync") class RatchetSync(val syncStatus: RatchetSyncState, val member: GroupMemberRef? = null): SndConnEvent()
val text: String
get() = when (this) {
@@ -2355,6 +2406,19 @@ sealed class SndConnEvent {
else -> generalGetString(MR.strings.snd_conn_event_switch_queue_phase_changing)
}
}
is RatchetSync -> {
member?.profile?.profileViewName?.let {
return when (syncStatus) {
RatchetSyncState.Ok -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_ok), it)
RatchetSyncState.Allowed -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_allowed), it)
RatchetSyncState.Required -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_required), it)
RatchetSyncState.Started -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_started), it)
RatchetSyncState.Agreed -> String.format(generalGetString(MR.strings.snd_conn_event_ratchet_sync_agreed), it)
}
}
ratchetSyncStatusToText(syncStatus)
}
}
}
@@ -717,9 +717,9 @@ object ChatController {
return null
}
suspend fun apiGroupMemberInfo(groupId: Long, groupMemberId: Long): ConnectionStats? {
suspend fun apiGroupMemberInfo(groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats?>? {
val r = sendCmd(CC.APIGroupMemberInfo(groupId, groupMemberId))
if (r is CR.GroupMemberInfo) return r.connectionStats_
if (r is CR.GroupMemberInfo) return Pair(r.member, r.connectionStats_)
Log.e(TAG, "apiGroupMemberInfo bad response: ${r.responseType} ${r.details}")
return null
}
@@ -731,9 +731,9 @@ object ChatController {
return null
}
suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long): ConnectionStats? {
suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats>? {
val r = sendCmd(CC.APISwitchGroupMember(groupId, groupMemberId))
if (r is CR.GroupMemberSwitchStarted) return r.connectionStats
if (r is CR.GroupMemberSwitchStarted) return Pair(r.member, r.connectionStats)
apiErrorAlert("apiSwitchGroupMember", generalGetString(MR.strings.error_changing_address), r)
return null
}
@@ -745,13 +745,27 @@ object ChatController {
return null
}
suspend fun apiAbortSwitchGroupMember(groupId: Long, groupMemberId: Long): ConnectionStats? {
suspend fun apiAbortSwitchGroupMember(groupId: Long, groupMemberId: Long): Pair<GroupMember, ConnectionStats>? {
val r = sendCmd(CC.APIAbortSwitchGroupMember(groupId, groupMemberId))
if (r is CR.GroupMemberSwitchAborted) return r.connectionStats
if (r is CR.GroupMemberSwitchAborted) return Pair(r.member, r.connectionStats)
apiErrorAlert("apiAbortSwitchGroupMember", generalGetString(MR.strings.error_aborting_address_change), r)
return null
}
suspend fun apiSyncContactRatchet(contactId: Long, force: Boolean): ConnectionStats? {
val r = sendCmd(CC.APISyncContactRatchet(contactId, force))
if (r is CR.ContactRatchetSyncStarted) return r.connectionStats
apiErrorAlert("apiSyncContactRatchet", generalGetString(MR.strings.error_synchronizing_connection), r)
return null
}
suspend fun apiSyncGroupMemberRatchet(groupId: Long, groupMemberId: Long, force: Boolean): Pair<GroupMember, ConnectionStats>? {
val r = sendCmd(CC.APISyncGroupMemberRatchet(groupId, groupMemberId, force))
if (r is CR.GroupMemberRatchetSyncStarted) return Pair(r.member, r.connectionStats)
apiErrorAlert("apiSyncGroupMemberRatchet", generalGetString(MR.strings.error_synchronizing_connection), r)
return null
}
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String> {
val r = sendCmd(CC.APIGetContactCode(contactId))
if (r is CR.ContactCode) return r.contact to r.connectionCode
@@ -1569,6 +1583,14 @@ object ChatController {
}
}
}
is CR.ContactSwitch ->
chatModel.updateContactConnectionStats(r.contact, r.switchProgress.connectionStats)
is CR.GroupMemberSwitch ->
chatModel.updateGroupMemberConnectionStats(r.groupInfo, r.member, r.switchProgress.connectionStats)
is CR.ContactRatchetSync ->
chatModel.updateContactConnectionStats(r.contact, r.ratchetSyncProgress.connectionStats)
is CR.GroupMemberRatchetSync ->
chatModel.updateGroupMemberConnectionStats(r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
else ->
Log.d(TAG , "unsupported event: ${r.responseType}")
}
@@ -1789,6 +1811,8 @@ sealed class CC {
class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
class APIAbortSwitchContact(val contactId: Long): CC()
class APIAbortSwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
class APISyncContactRatchet(val contactId: Long, val force: Boolean): CC()
class APISyncGroupMemberRatchet(val groupId: Long, val groupMemberId: Long, val force: Boolean): CC()
class APIGetContactCode(val contactId: Long): CC()
class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC()
class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC()
@@ -1884,6 +1908,8 @@ sealed class CC {
is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId"
is APIAbortSwitchContact -> "/_abort switch @$contactId"
is APIAbortSwitchGroupMember -> "/_abort switch #$groupId $groupMemberId"
is APISyncContactRatchet -> if (force) "/_sync @$contactId force=on" else "/_sync @$contactId"
is APISyncGroupMemberRatchet -> if (force) "/_sync #$groupId $groupMemberId force=on" else "/_sync #$groupId $groupMemberId"
is APIGetContactCode -> "/_get code @$contactId"
is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId"
is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else ""
@@ -1974,6 +2000,8 @@ sealed class CC {
is APISwitchGroupMember -> "apiSwitchGroupMember"
is APIAbortSwitchContact -> "apiAbortSwitchContact"
is APIAbortSwitchGroupMember -> "apiAbortSwitchGroupMember"
is APISyncContactRatchet -> "apiSyncContactRatchet"
is APISyncGroupMemberRatchet -> "apiSyncGroupMemberRatchet"
is APIGetContactCode -> "apiGetContactCode"
is APIGetGroupMemberCode -> "apiGetGroupMemberCode"
is APIVerifyContact -> "apiVerifyContact"
@@ -3161,6 +3189,14 @@ sealed class CR {
@Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("groupMemberSwitchAborted") class GroupMemberSwitchAborted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("contactSwitch") class ContactSwitch(val user: User, val contact: Contact, val switchProgress: SwitchProgress): CR()
@Serializable @SerialName("groupMemberSwitch") class GroupMemberSwitch(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val switchProgress: SwitchProgress): CR()
@Serializable @SerialName("contactRatchetSyncStarted") class ContactRatchetSyncStarted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("groupMemberRatchetSyncStarted") class GroupMemberRatchetSyncStarted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR()
@Serializable @SerialName("contactRatchetSync") class ContactRatchetSync(val user: User, val contact: Contact, val ratchetSyncProgress: RatchetSyncProgress): CR()
@Serializable @SerialName("groupMemberRatchetSync") class GroupMemberRatchetSync(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val ratchetSyncProgress: RatchetSyncProgress): CR()
@Serializable @SerialName("contactVerificationReset") class ContactVerificationReset(val user: User, val contact: Contact): CR()
@Serializable @SerialName("groupMemberVerificationReset") class GroupMemberVerificationReset(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("contactCode") class ContactCode(val user: User, val contact: Contact, val connectionCode: String): CR()
@Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR()
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: User, val verified: Boolean, val expectedCode: String): CR()
@@ -3279,6 +3315,14 @@ sealed class CR {
is GroupMemberSwitchStarted -> "groupMemberSwitchStarted"
is ContactSwitchAborted -> "contactSwitchAborted"
is GroupMemberSwitchAborted -> "groupMemberSwitchAborted"
is ContactSwitch -> "contactSwitch"
is GroupMemberSwitch -> "groupMemberSwitch"
is ContactRatchetSyncStarted -> "contactRatchetSyncStarted"
is GroupMemberRatchetSyncStarted -> "groupMemberRatchetSyncStarted"
is ContactRatchetSync -> "contactRatchetSync"
is GroupMemberRatchetSync -> "groupMemberRatchetSync"
is ContactVerificationReset -> "contactVerificationReset"
is GroupMemberVerificationReset -> "groupMemberVerificationReset"
is ContactCode -> "contactCode"
is GroupMemberCode -> "groupMemberCode"
is ConnectionVerified -> "connectionVerified"
@@ -3394,6 +3438,14 @@ sealed class CR {
is GroupMemberSwitchStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is ContactSwitchAborted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is GroupMemberSwitchAborted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is ContactSwitch -> withUser(user, "contact: ${json.encodeToString(contact)}\nswitchProgress: ${json.encodeToString(switchProgress)}")
is GroupMemberSwitch -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nswitchProgress: ${json.encodeToString(switchProgress)}")
is ContactRatchetSyncStarted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is GroupMemberRatchetSyncStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
is ContactRatchetSync -> withUser(user, "contact: ${json.encodeToString(contact)}\nratchetSyncProgress: ${json.encodeToString(ratchetSyncProgress)}")
is GroupMemberRatchetSync -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nratchetSyncProgress: ${json.encodeToString(ratchetSyncProgress)}")
is ContactVerificationReset -> withUser(user, "contact: ${json.encodeToString(contact)}")
is GroupMemberVerificationReset -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}")
is ContactCode -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionCode: $connectionCode")
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
@@ -3525,7 +3577,19 @@ abstract class TerminalItem {
}
@Serializable
class ConnectionStats(val rcvQueuesInfo: List<RcvQueueInfo>, val sndQueuesInfo: List<SndQueueInfo>)
class ConnectionStats(
val connAgentVersion: Int,
val rcvQueuesInfo: List<RcvQueueInfo>,
val sndQueuesInfo: List<SndQueueInfo>,
val ratchetSyncState: RatchetSyncState,
val ratchetSyncSupported: Boolean
) {
val ratchetSyncAllowed: Boolean get() =
ratchetSyncSupported && listOf(RatchetSyncState.Allowed, RatchetSyncState.Required).contains(ratchetSyncState)
val ratchetSyncSendProhibited: Boolean get() =
listOf(RatchetSyncState.Required, RatchetSyncState.Started, RatchetSyncState.Agreed).contains(ratchetSyncState)
}
@Serializable
class RcvQueueInfo(
@@ -3554,6 +3618,34 @@ enum class SndSwitchStatus {
@SerialName("sending_qtest") SendingQTEST
}
@Serializable
enum class QueueDirection {
@SerialName("rcv") Rcv,
@SerialName("snd") Snd
}
@Serializable
class SwitchProgress(
val queueDirection: QueueDirection,
val switchPhase: SwitchPhase,
val connectionStats: ConnectionStats
)
@Serializable
class RatchetSyncProgress(
val ratchetSyncStatus: RatchetSyncState,
val connectionStats: ConnectionStats
)
@Serializable
enum class RatchetSyncState {
@SerialName("ok") Ok,
@SerialName("allowed") Allowed,
@SerialName("required") Required,
@SerialName("started") Started,
@SerialName("agreed") Agreed
}
@Serializable
class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) {
val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}"
@@ -29,7 +29,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
@@ -84,14 +83,45 @@ fun ChatInfoView(
switchContactAddress = {
showSwitchAddressAlert(switchAddress = {
withApi {
connStats.value = chatModel.controller.apiSwitchContact(contact.contactId)
val cStats = chatModel.controller.apiSwitchContact(contact.contactId)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
close.invoke()
}
})
},
abortSwitchContactAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
connStats.value = chatModel.controller.apiAbortSwitchContact(contact.contactId)
val cStats = chatModel.controller.apiAbortSwitchContact(contact.contactId)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
}
})
},
syncContactConnection = {
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
close.invoke()
}
},
syncContactConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = true)
connStats.value = cStats
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
close.invoke()
}
})
},
@@ -179,6 +209,8 @@ fun ChatInfoLayout(
clearChat: () -> Unit,
switchContactAddress: () -> Unit,
abortSwitchContactAddress: () -> Unit,
syncContactConnection: () -> Unit,
syncContactConnectionForce: () -> Unit,
verifyClicked: () -> Unit,
) {
val cStats = connStats.value
@@ -208,6 +240,11 @@ fun ChatInfoLayout(
VerifyCodeButton(contact.verified, verifyClicked)
}
ContactPreferencesButton(openPreferences)
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncContactConnection)
} else if (developerTools) {
SynchronizeConnectionButtonForce(syncContactConnectionForce)
}
}
SectionDividerSpaced()
@@ -230,12 +267,12 @@ fun ChatInfoLayout(
}
if (cStats != null) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
switchAddress = switchContactAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
abortSwitchAddress = abortSwitchContactAddress
)
}
@@ -421,6 +458,28 @@ fun AbortSwitchAddressButton(disabled: Boolean, abortSwitchAddress: () -> Unit)
}
}
@Composable
fun SynchronizeConnectionButton(syncConnection: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_sync_problem),
stringResource(MR.strings.fix_connection),
click = syncConnection,
textColor = WarningOrange,
iconColor = WarningOrange
)
}
@Composable
fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_warning),
stringResource(MR.strings.renegotiate_encryption),
click = syncConnectionForce,
textColor = Color.Red,
iconColor = Color.Red
)
}
@Composable
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
SettingsActionItem(
@@ -498,6 +557,16 @@ fun showAbortSwitchAddressAlert(abortSwitchAddress: () -> Unit) {
)
}
fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.sync_connection_force_question),
text = generalGetString(MR.strings.sync_connection_force_desc),
confirmText = generalGetString(MR.strings.sync_connection_force_confirm),
onConfirm = syncConnectionForce,
destructive = true,
)
}
@Preview
@Composable
fun PreviewChatInfoLayout() {
@@ -520,6 +589,8 @@ fun PreviewChatInfoLayout() {
clearChat = {},
switchContactAddress = {},
abortSwitchContactAddress = {},
syncContactConnection = {},
syncContactConnectionForce = {},
verifyClicked = {},
)
}
@@ -30,7 +30,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
@@ -167,7 +166,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
@@ -260,6 +260,47 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.controller.allowFeatureToContact(contact, feature, param)
}
},
updateContactStats = { contact ->
withApi {
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
if (r != null) {
chatModel.updateContactConnectionStats(contact, r.first)
}
}
},
updateMemberStats = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
if (r != null) {
val memStats = r.second
if (memStats != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, memStats)
}
}
}
},
syncContactConnection = { contact ->
withApi {
val cStats = chatModel.controller.apiSyncContactRatchet(contact.contactId, force = false)
if (cStats != null) {
chatModel.updateContactConnectionStats(contact, cStats)
}
}
},
syncMemberConnection = { groupInfo, member ->
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
}
}
},
findModelChat = { chatId ->
chatModel.getChat(chatId)
},
findModelMember = { memberId ->
chatModel.groupMembers.find { it.id == memberId }
},
setReaction = { cInfo, cItem, add, reaction ->
withApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
@@ -344,6 +385,12 @@ fun ChatLayout(
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
addMembers: (GroupInfo) -> Unit,
@@ -388,7 +435,9 @@ fun ChatLayout(
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
)
}
}
@@ -561,6 +610,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
@@ -676,11 +731,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} else {
Spacer(Modifier.size(42.dp))
}
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
} else {
Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
}
} else { // direct message
@@ -691,7 +746,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
}
}
@@ -1114,6 +1169,12 @@ fun PreviewChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },
@@ -1176,6 +1237,12 @@ fun PreviewGroupChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },
@@ -61,7 +61,8 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
},
showMemberInfo = { member ->
withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
@@ -102,14 +102,46 @@ fun GroupMemberInfoView(
switchMemberAddress = {
showSwitchAddressAlert(switchAddress = {
withApi {
connStats.value = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
val r = chatModel.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
}
})
},
abortSwitchMemberAddress = {
showAbortSwitchAddressAlert(abortSwitchAddress = {
withApi {
connStats.value = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
val r = chatModel.controller.apiAbortSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
}
})
},
syncMemberConnection = {
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
}
},
syncMemberConnectionForce = {
showSyncConnectionForceAlert(syncConnectionForce = {
withApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(groupInfo.apiId, member.groupMemberId, force = true)
if (r != null) {
connStats.value = r.second
chatModel.updateGroupMemberConnectionStats(groupInfo, r.first, r.second)
close.invoke()
}
}
})
},
@@ -176,6 +208,8 @@ fun GroupMemberInfoLayout(
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
abortSwitchMemberAddress: () -> Unit,
syncMemberConnection: () -> Unit,
syncMemberConnectionForce: () -> Unit,
verifyClicked: () -> Unit,
) {
val cStats = connStats.value
@@ -212,6 +246,11 @@ fun GroupMemberInfoLayout(
if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked)
}
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncMemberConnection)
} else if (developerTools) {
SynchronizeConnectionButtonForce(syncMemberConnectionForce)
}
}
SectionDividerSpaced()
}
@@ -254,12 +293,12 @@ fun GroupMemberInfoLayout(
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
SwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null },
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
switchAddress = switchMemberAddress
)
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
AbortSwitchAddressButton(
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch },
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
abortSwitchAddress = abortSwitchMemberAddress
)
}
@@ -414,6 +453,8 @@ fun PreviewGroupMemberInfoLayout() {
onRoleSelected = {},
switchMemberAddress = {},
abortSwitchMemberAddress = {},
syncMemberConnection = {},
syncMemberConnectionForce = {},
verifyClicked = {},
)
}
@@ -1,25 +1,231 @@
package chat.simplex.app.views.chat.item
import androidx.compose.runtime.Composable
import chat.simplex.app.R
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
@Composable
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
CIMsgError(ci, timedMessagesTTL, showMember) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.decryption_error),
text = when (msgDecryptError) {
MsgDecryptError.RatchetHeader -> String.format(generalGetString(MR.strings.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(MR.strings.alert_text_fragment_permanent_error_reconnect)
MsgDecryptError.TooManySkipped -> String.format(generalGetString(MR.strings.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(MR.strings.alert_text_fragment_permanent_error_reconnect)
fun CIRcvDecryptionError(
msgDecryptError: MsgDecryptError,
msgCount: UInt,
cInfo: ChatInfo,
ci: ChatItem,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
showMember: Boolean
) {
LaunchedEffect(Unit) {
if (cInfo is ChatInfo.Direct) {
updateContactStats(cInfo.contact)
} else if (cInfo is ChatInfo.Group && ci.chatDir is CIDirection.GroupRcv) {
updateMemberStats(cInfo.groupInfo, ci.chatDir.groupMember)
}
}
@Composable
fun BasicDecryptionErrorItem() {
DecryptionErrorItem(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.decryption_error),
text = alertMessage(msgDecryptError, msgCount)
)
}
)
}
if (cInfo is ChatInfo.Direct) {
val modelCInfo = findModelChat(cInfo.id)?.chatInfo
if (modelCInfo is ChatInfo.Direct) {
val modelContactStats = modelCInfo.contact.activeConn.connectionStats
if (modelContactStats != null) {
if (modelContactStats.ratchetSyncAllowed) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.fix_connection_question),
text = alertMessage(msgDecryptError, msgCount),
confirmText = generalGetString(MR.strings.fix_connection_confirm),
onConfirm = { syncContactConnection(cInfo.contact) },
)
},
syncSupported = true
)
} else if (!modelContactStats.ratchetSyncSupported) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.fix_connection_not_supported_by_contact),
text = alertMessage(msgDecryptError, msgCount)
)
},
syncSupported = false
)
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
} else if (cInfo is ChatInfo.Group && ci.chatDir is CIDirection.GroupRcv) {
val modelMember = findModelMember(ci.chatDir.groupMember.id)
val modelMemberStats = modelMember?.activeConn?.connectionStats
if (modelMemberStats != null) {
if (modelMemberStats.ratchetSyncAllowed) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.fix_connection_question),
text = alertMessage(msgDecryptError, msgCount),
confirmText = generalGetString(MR.strings.fix_connection_confirm),
onConfirm = { syncMemberConnection(cInfo.groupInfo, modelMember) },
)
},
syncSupported = true
)
} else if (!modelMemberStats.ratchetSyncSupported) {
DecryptionErrorItemFixButton(
ci,
showMember,
onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.fix_connection_not_supported_by_group_member),
text = alertMessage(msgDecryptError, msgCount)
)
},
syncSupported = false
)
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
} else {
BasicDecryptionErrorItem()
}
}
@Composable
fun DecryptionErrorItemFixButton(
ci: ChatItem,
showMember: Boolean,
onClick: () -> Unit,
syncSupported: Boolean
) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface(
Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = receivedColor,
) {
Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
contentAlignment = Alignment.BottomEnd,
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
buildAnnotatedString {
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
)
Row {
Icon(
painterResource(MR.images.ic_sync_problem),
stringResource(MR.strings.fix_connection),
tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(2.dp))
Text(
buildAnnotatedString {
append(generalGetString(MR.strings.fix_connection))
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
withStyle(reserveTimestampStyle) { append(" ") } // for icon
},
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
}
CIMetaView(ci, timedMessagesTTL = null)
}
}
}
@Composable
fun DecryptionErrorItem(
ci: ChatItem,
showMember: Boolean,
onClick: () -> Unit
) {
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Surface(
Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = receivedColor,
) {
Box(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
contentAlignment = Alignment.BottomEnd,
) {
Text(
buildAnnotatedString {
appendSender(this, if (showMember) ci.memberDisplayName else null, true)
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
)
CIMetaView(ci, timedMessagesTTL = null)
}
}
}
private fun alertMessage(msgDecryptError: MsgDecryptError, msgCount: UInt): String {
return when (msgDecryptError) {
MsgDecryptError.RatchetHeader -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
MsgDecryptError.TooManySkipped -> String.format(generalGetString(MR.strings.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
MsgDecryptError.RatchetEarlier -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
MsgDecryptError.Other -> String.format(generalGetString(MR.strings.alert_text_decryption_error_n_messages_failed_to_decrypt), msgCount.toLong()) + "\n" +
generalGetString(MR.strings.alert_text_fragment_encryption_out_of_sync_old_database)
}
}
@@ -20,7 +20,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
@@ -47,6 +46,12 @@ fun ChatItemView(
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit,
syncMemberConnection: (GroupInfo, GroupMember) -> Unit,
findModelChat: (String) -> Chat?,
findModelMember: (String) -> GroupMember?,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
) {
@@ -302,7 +307,7 @@ fun ChatItemView(
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, showMember = showMember)
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.RcvGroupEventContent -> CIEventView(cItem)
@@ -522,6 +527,12 @@ fun PreviewChatItemView() {
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
)
@@ -545,6 +556,12 @@ fun PreviewChatItemViewDeletedContent() {
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
updateContactStats = { },
updateMemberStats = { _, _ -> },
syncContactConnection = { },
syncMemberConnection = { _, _ -> },
findModelChat = { null },
findModelMember = { null },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
)
@@ -134,8 +134,19 @@ suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: St
suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
val groupMembers = chatModel.controller.apiListMembers(groupInfo.groupId)
val currentMembers = chatModel.groupMembers
val newMembers = groupMembers.map { newMember ->
val currentMember = currentMembers.find { it.id == newMember.id }
val currentMemberStats = currentMember?.activeConn?.connectionStats
val newMemberConn = newMember.activeConn
if (currentMemberStats != null && newMemberConn != null && newMemberConn.connectionStats == null) {
newMember.copy(activeConn = newMemberConn.copy(connectionStats = currentMemberStats))
} else {
newMember
}
}
chatModel.groupMembers.clear()
chatModel.groupMembers.addAll(groupMembers)
chatModel.groupMembers.addAll(newMembers)
}
@Composable