mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 20:45:49 +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)}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
|
||||
<string name="error_changing_address">Error changing address</string>
|
||||
<string name="error_aborting_address_change">Error aborting address change</string>
|
||||
<string name="error_synchronizing_connection">Error synchronizing connection</string>
|
||||
<string name="error_smp_test_failed_at_step">Test failed at step %s.</string>
|
||||
<string name="error_smp_test_server_auth">Server requires authorization to create queues, check password</string>
|
||||
<string name="error_xftp_test_server_auth">Server requires authorization to upload, check password</string>
|
||||
@@ -339,6 +340,9 @@
|
||||
<string name="abort_switch_receiving_address_question">Abort changing address?</string>
|
||||
<string name="abort_switch_receiving_address_desc">Address change will be aborted. Old receiving address will be used.</string>
|
||||
<string name="abort_switch_receiving_address_confirm">Abort</string>
|
||||
<string name="sync_connection_force_question">Renegotiate encryption?</string>
|
||||
<string name="sync_connection_force_desc">The encryption is working and the new encryption agreement is not required. It may result in connection errors!</string>
|
||||
<string name="sync_connection_force_confirm">Renegotiate</string>
|
||||
<string name="view_security_code">View security code</string>
|
||||
<string name="verify_security_code">Verify security code</string>
|
||||
|
||||
@@ -816,10 +820,9 @@
|
||||
<string name="alert_text_msg_bad_hash">The hash of the previous message is different."</string>
|
||||
<string name="alert_title_msg_bad_id">Bad message ID</string>
|
||||
<string name="alert_text_msg_bad_id">The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d messages failed to decrypt.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d messages failed to decrypt.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d messages skipped.</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">It can happen when you or your connection used the old database backup.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">This error is permanent for this connection, please re-connect.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Please report it to the developers.</string>
|
||||
|
||||
<!-- Privacy settings -->
|
||||
@@ -1065,6 +1068,17 @@
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">changing address for %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">you changed address</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
<string name="conn_event_ratchet_sync_ok">encryption ok</string>
|
||||
<string name="conn_event_ratchet_sync_allowed">encryption re-negotiation allowed</string>
|
||||
<string name="conn_event_ratchet_sync_required">encryption re-negotiation required</string>
|
||||
<string name="conn_event_ratchet_sync_started">agreeing encryption…</string>
|
||||
<string name="conn_event_ratchet_sync_agreed">encryption agreed</string>
|
||||
<string name="snd_conn_event_ratchet_sync_ok">encryption ok for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_allowed">encryption re-negotiation allowed for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_required">encryption re-negotiation required for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_started">agreeing encryption for %s</string>
|
||||
<string name="snd_conn_event_ratchet_sync_agreed">encryption agreed for %s</string>
|
||||
<string name="rcv_conn_event_verification_code_reset">security code changed</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_observer">observer</string>
|
||||
@@ -1184,6 +1198,12 @@
|
||||
<string name="network_status">Network status</string>
|
||||
<string name="switch_receiving_address">Change receiving address</string>
|
||||
<string name="abort_switch_receiving_address">Abort changing address</string>
|
||||
<string name="fix_connection">Fix connection</string>
|
||||
<string name="fix_connection_question">Fix connection?</string>
|
||||
<string name="fix_connection_confirm">Fix</string>
|
||||
<string name="fix_connection_not_supported_by_contact">Fix not supported by contact</string>
|
||||
<string name="fix_connection_not_supported_by_group_member">Fix not supported by group member</string>
|
||||
<string name="renegotiate_encryption">Renegotiate encryption</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Create secret group</string>
|
||||
|
||||
@@ -1096,7 +1096,7 @@
|
||||
<string name="alert_text_msg_bad_hash">Hash předchozí zprávy se liší.</string>
|
||||
<string name="alert_text_msg_bad_id">ID další zprávy je nesprávné (menší nebo rovno předchozí).
|
||||
\nMůže se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitováno.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d zprávy se nepodařilo dešifrovat.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d zprávy se nepodařilo dešifrovat.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d zprývy přeskočeny.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Nahlaste to prosím vývojářům.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Tato chyba je pro toto připojení trvalá, připojte se znovu.</string>
|
||||
|
||||
@@ -1173,7 +1173,7 @@
|
||||
<string name="xftp_servers">XFTP-Server</string>
|
||||
<string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der Vorherigen).
|
||||
\nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d Nachrichten konnten nicht entschlüsselt werden.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d Nachrichten konnten nicht entschlüsselt werden.</string>
|
||||
<string name="alert_text_msg_bad_hash">Der Hash der vorherigen Nachricht unterscheidet sich.</string>
|
||||
<string name="you_can_turn_on_lock">Sie können die SimpleX Sperre über die Einstellungen aktivieren.</string>
|
||||
<string name="network_socks_proxy_settings">SOCKS-Proxy Einstellungen</string>
|
||||
|
||||
@@ -1101,7 +1101,7 @@
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.</string>
|
||||
<string name="alert_text_msg_bad_hash">El hash del mensaje anterior es diferente.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">El error es permanente para esta conexión, por favor vuelve a conectarte.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d mensajes no pudieron ser descifrados.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mensajes no pudieron ser descifrados.</string>
|
||||
<string name="no_spaces">¡Sin espacios!</string>
|
||||
<string name="stop_file__action">Detener archivo</string>
|
||||
<string name="revoke_file__message">El archivo será eliminado de los servidores.</string>
|
||||
|
||||
@@ -1244,7 +1244,7 @@
|
||||
<string name="alert_text_decryption_error_too_many_skipped"> %1$d viestit ohitettu.</string>
|
||||
<string name="alert_text_msg_bad_id">Seuraavan viestin tunnus on väärä (pienempi tai yhtä suuri kuin edellisen).
|
||||
\nTämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d viestien salauksen purku epäonnistui.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d viestien salauksen purku epäonnistui.</string>
|
||||
<string name="user_unhide">Näytä</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen.</string>
|
||||
<string name="update_database_passphrase">Päivitä tietokannan tunnuslause</string>
|
||||
|
||||
@@ -1119,7 +1119,7 @@
|
||||
<string name="stop_rcv_file__title">Arrêter de recevoir le fichier \?</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Cette erreur est persistante pour cette connexion, veuillez vous reconnecter.</string>
|
||||
<string name="la_could_not_be_verified">Vous n\'avez pas pu être vérifié·e ; veuillez réessayer.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d messages n\'ont pas pu être déchiffrés.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d messages n\'ont pas pu être déchiffrés.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d messages sautés.</string>
|
||||
<string name="you_can_turn_on_lock">Vous pouvez activer SimpleX Lock dans les Paramètres.</string>
|
||||
<string name="v5_0_polish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-284.5q-11.5 0-20-8.75t-8.5-20.25q0-11 8.5-19.75t20-8.75q11.5 0 20.25 8.75T509-313.5q0 11.5-8.75 20.25T480-284.5Zm2.325-145.5q-12.325 0-20.575-8.375T453.5-459v-189q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T511-648v189q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375ZM190.5-477q0 65.183 28 118.591 28 53.409 90 93.409v-96q0-11.75 8.425-20.125 8.426-8.375 20.75-8.375 11.825 0 20.075 8.375T366-361v168.5q0 12.25-8.375 20.625T337.5-163.5H169q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.75 0-11.825 8.375-20.075T169-221h101.5q-64.5-48-101-108T133-477q0-88 46-165.75T311-762.5q10-4.5 20.25-.25T346-748q4 11 .25 21.75T332.5-711q-62.5 32-102.25 94.881Q190.5-553.237 190.5-477Zm589.5-6.5q0-64.683-28.25-118.091Q723.5-655 661.5-695v95.5q0 12.25-8.425 20.625-8.426 8.375-20.25 8.375-12.325 0-20.575-8.375T604-599.5V-768q0-11.75 8.375-20.125T633-796.5h168.5q11.75 0 20.125 8.425 8.375 8.426 8.375 20.25 0 12.325-8.375 20.575T801.5-739H700q64 48 100.75 108t36.75 147.5q0 88.5-46.25 166.75T659-197q-10 5-20.25.25T624.5-212q-4.5-10.5-.75-21.5t14.25-16q62-31.5 102-94.381 40-62.882 40-139.619Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M96.63-124.5q-8.63 0-15.282-4.125Q74.696-132.75 71.25-139q-3.917-6.1-4.333-13.55Q66.5-160 71.5-168l383.936-661.885Q460-837.5 466.25-841t13.75-3.5q7.5 0 13.75 3.5T505-830l384 662q4.5 8 4.083 15.45-.416 7.45-4.333 13.55-3.446 6.25-10.098 10.375Q872-124.5 863.37-124.5H96.63ZM146.5-182h667L480-758 146.5-182Zm337.728-57.5q12.272 0 20.522-8.478 8.25-8.478 8.25-20.75t-8.478-20.522q-8.478-8.25-20.75-8.25t-20.522 8.478q-8.25 8.478-8.25 20.75t8.478 20.522q8.478 8.25 20.75 8.25Zm.197-108.5q12.075 0 20.325-8.375T513-377v-165q0-11.675-8.463-20.088-8.463-8.412-20.212-8.412-12.325 0-20.575 8.412-8.25 8.413-8.25 20.088v165q0 12.25 8.425 20.625 8.426 8.375 20.5 8.375ZM480-470Z"/></svg>
|
||||
|
After Width: | Height: | Size: 774 B |
@@ -1097,7 +1097,7 @@
|
||||
<string name="alert_text_msg_bad_id">L\'ID del messaggio successivo non è corretto (inferiore o uguale al precedente).
|
||||
\nPuò accadere a causa di qualche bug o quando la connessione è compromessa.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">L\'errore è permanente per questa connessione, riconnettiti.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d messaggi non decifrati.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d messaggi non decifrati.</string>
|
||||
<string name="alert_title_msg_bad_hash">Hash del messaggio errato</string>
|
||||
<string name="alert_text_msg_bad_hash">L\'hash del messaggio precedente è diverso.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Si prega di segnalarlo agli sviluppatori.</string>
|
||||
|
||||
@@ -1197,7 +1197,7 @@
|
||||
<string name="call_connection_via_relay">דרך ממסר</string>
|
||||
<string name="icon_descr_video_off">וידאו כבוי</string>
|
||||
<string name="icon_descr_video_on">וידאו פעיל</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d הודעות לא הצליחו לעבור פענוח.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d הודעות לא הצליחו לעבור פענוח.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">עליכם להשתמש בגרסה העדכנית ביותר של מסד הנתונים שלכם במכשיר אחד בלבד, אחרת אתם עלולים להפסיק לקבל הודעות מאנשי קשר מסוימים.</string>
|
||||
<string name="wrong_passphrase_title">סיסמה שגויה!</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">הוזמנתם לקבוצה. הצטרפו כדי ליצור קשר עם חברי הקבוצה.</string>
|
||||
|
||||
@@ -1072,7 +1072,7 @@
|
||||
<string name="alert_text_fragment_please_report_to_developers">開発者に報告してください。</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">このエラーはこの接続では永続的なものです。再接続してください。</string>
|
||||
<string name="v5_0_large_files_support">1GBまでのビデオとファイル</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d メッセージの復号化に失敗しました。</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d メッセージの復号化に失敗しました。</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d メッセージをスキップしました</string>
|
||||
<string name="error_loading_smp_servers">SMP サーバーのロード中にエラーが発生しました</string>
|
||||
<string name="error_saving_user_password">ユーザーパスワード保存エラー</string>
|
||||
|
||||
@@ -1099,7 +1099,7 @@
|
||||
<string name="alert_title_msg_bad_id">Onjuiste bericht ID</string>
|
||||
<string name="alert_text_msg_bad_id">De ID van het volgende bericht is onjuist (minder of gelijk aan het vorige).
|
||||
\nHet kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d-berichten konden niet worden ontsleuteld.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d-berichten konden niet worden ontsleuteld.</string>
|
||||
<string name="no_spaces">Geen spaties!</string>
|
||||
<string name="stop_rcv_file__message">Het ontvangen van het bestand wordt gestopt.</string>
|
||||
<string name="revoke_file__confirm">Intrekken</string>
|
||||
|
||||
@@ -1096,7 +1096,7 @@
|
||||
<string name="alert_text_msg_bad_hash">Hash poprzedniej wiadomości jest inny.</string>
|
||||
<string name="alert_text_msg_bad_id">Identyfikator następnej wiadomości jest nieprawidłowy (mniejszy lub równy poprzedniej).
|
||||
\nMoże się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skompromitowane.</string>
|
||||
<string name="alert_text_decryption_error_header">Nie udało się odszyfrować %1$d wiadomości.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nie udało się odszyfrować %1$d wiadomości.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Ten błąd jest trwały dla tego połączenia, proszę o ponowne połączenie.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d pominiętych wiadomości.</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Może się to zdarzyć, gdy Ty lub Twoje połączenie użyło starej kopii zapasowej bazy danych.</string>
|
||||
|
||||
@@ -1088,7 +1088,7 @@
|
||||
<string name="you_can_turn_on_lock">Você pode ativar o bloqueio SimpleX via Configurações.</string>
|
||||
<string name="alert_title_msg_bad_hash">Hash de mensagem incorreta</string>
|
||||
<string name="alert_text_msg_bad_hash">O hash da mensagem anterior é diferente.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d descriptografia das mensagens falhou</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d descriptografia das mensagens falhou</string>
|
||||
<string name="alert_title_msg_bad_id">ID de mensagem incorreta</string>
|
||||
<string name="alert_text_msg_bad_id">A ID da próxima mensagem está incorreta (menor ou igual à anterior).
|
||||
\nIsso pode acontecer por causa de algum bug ou quando a conexão está comprometida.</string>
|
||||
|
||||
@@ -1156,7 +1156,7 @@
|
||||
<string name="authentication_cancelled">Аутентификация отменена</string>
|
||||
<string name="la_mode_system">Системная</string>
|
||||
<string name="la_seconds">%d секунд</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d сообщений не удалось расшифровать.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d сообщений не удалось расшифровать.</string>
|
||||
<string name="decryption_error">Ошибка расшифровки</string>
|
||||
<string name="lock_not_enabled">Блокировка SimpleX не включена!</string>
|
||||
<string name="alert_title_msg_bad_hash">Ошибка хэш сообщения</string>
|
||||
|
||||
@@ -1052,7 +1052,7 @@
|
||||
<string name="v4_4_verify_connection_security">ตรวจสอบความปลอดภัยในการเชื่อมต่อ</string>
|
||||
<string name="incognito_info_share">เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ</string>
|
||||
<string name="contact_wants_to_connect_via_call">%1$s ต้องการเชื่อมต่อกับคุณผ่านทาง</string>
|
||||
<string name="alert_text_decryption_error_header">ข้อความ %1$d ไม่สามารถ decrypt ได้</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">ข้อความ %1$d ไม่สามารถ decrypt ได้</string>
|
||||
<string name="group_info_section_title_num_members">%1$s สมาชิก</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[คุณสามารถ <font color="#0088ff">เชื่อมต่อกับ SimpleX Chat นักพัฒนาแอปเพื่อถามคำถามและรับการอัปเดต</font>]]></string>
|
||||
<string name="you_can_share_your_address">คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้</string>
|
||||
|
||||
@@ -874,7 +874,7 @@
|
||||
<string name="show_call_on_lock_screen">Показати</string>
|
||||
<string name="no_call_on_lock_screen">Вимкнути</string>
|
||||
<string name="relay_server_if_necessary">Сервер ретрансляції використовується лише за необхідності. Інша сторона може спостерігати за вашою IP-адресою.</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d повідомлення не вдалося розшифрувати.</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d повідомлення не вдалося розшифрувати.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d повідомлення пропущені.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Будь ласка, повідомте про це розробникам.</string>
|
||||
<string name="send_link_previews">Надіслати попередній перегляд за посиланням</string>
|
||||
|
||||
@@ -1092,7 +1092,7 @@
|
||||
<string name="la_immediately">立即</string>
|
||||
<string name="alert_title_msg_bad_hash">错误消息散列</string>
|
||||
<string name="alert_title_msg_bad_id">错误消息 ID</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d 消息解密失败。</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d 消息解密失败。</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped">%1$d 已跳过消息。</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">此错误对于此连接是永久性的,请重新连接。</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">当您或您的连接使用旧数据库备份时,可能会发生这种情况。</string>
|
||||
|
||||
@@ -1120,7 +1120,7 @@
|
||||
<string name="la_mode_system">系統</string>
|
||||
<string name="alert_text_msg_bad_id">此ID的下一則訊息是錯誤(小於或等於上一則的)。
|
||||
\n當一些錯誤出現或你的連結被破壞時會發生。</string>
|
||||
<string name="alert_text_decryption_error_header">%1$d 訊息解密失敗。</string>
|
||||
<string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d 訊息解密失敗。</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">使用SOCKS 代理伺服器</string>
|
||||
<string name="your_XFTP_servers">你的 XFTP 伺服器</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">這個連結錯誤是永久性的,請重新連接。</string>
|
||||
|
||||
Reference in New Issue
Block a user