mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 10:55:33 +00:00
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:
@@ -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)}"
|
||||
|
||||
+76
-5
@@ -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 = { _ -> },
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
+45
-4
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
||||
+221
-15
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -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 = { _, _ -> },
|
||||
)
|
||||
|
||||
+12
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user