diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46a38d2f47..5ca8fef810 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,12 +119,6 @@ jobs: cabal build --enable-tests echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)" - - name: Unix test - if: matrix.os != 'windows-latest' - timeout-minutes: 30 - shell: bash - run: cabal test --test-show-details=direct - - name: Unix upload binary to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' uses: svenstaro/upload-release-action@v2 @@ -134,6 +128,12 @@ jobs: asset_name: ${{ matrix.asset_name }} tag: ${{ github.ref }} + - name: Unix test + if: matrix.os != 'windows-latest' + timeout-minutes: 30 + shell: bash + run: cabal test --test-show-details=direct + # Unix / # / Windows diff --git a/README.md b/README.md index 4c2c947064..47c825f8da 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ You can join an English-speaking users group if you want to ask any questions: [ There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users: -[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking). +[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking). You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code. @@ -101,8 +101,10 @@ Join our translators to help SimpleX grow! |🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)|| |🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)| |🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)|| +|🇯🇵 ja|Japanese ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)||| |🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)|| |🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)||| +|🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)
-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)|| |🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)||| |🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)

[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)
 |

[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| @@ -248,13 +250,15 @@ See [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md) for the format of m SimpleX Chat is a work in progress – we are releasing improvements as they are ready. You have to decide if the current state is good enough for your usage scenario. +We compiled a [glossary of terms](./docs/GLOSSARY.md) used to describe communication systems to help understand some terms below and to help compare advantages and disadvantages of various communication systems. + What is already implemented: -1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues. -2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation. -3. [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with forward secrecy (each message is encrypted by its own ephemeral key), break-in recovery (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial key agreement, initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message. +1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues. +2. [End-to-end encryption](./docs/GLOSSARY.md#end-to-end-encryption) in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation. +3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message. 4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well). -5. Several levels of content padding to frustrate message size attacks. +5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks. 6. Starting from v2 of SMP protocol (the current version is v4) all message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed. 7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key. diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 80b44bc00a..e2735b9470 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -13,8 +13,8 @@ android { targetSdk 32 // !!! // skip version code after release to F-Droid, as it uses two version codes - versionCode 121 - versionName "5.1" + versionCode 123 + versionName "5.1.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 57ff629426..721fc48df6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -545,9 +545,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a throw Error("failed to export archive: ${r.responseType} ${r.details}") } - suspend fun apiImportArchive(config: ArchiveConfig) { + suspend fun apiImportArchive(config: ArchiveConfig): List { val r = sendCmd(CC.ApiImportArchive(config)) - if (r is CR.CmdOk) return + if (r is CR.ArchiveImported) return r.archiveErrors throw Error("failed to import archive: ${r.responseType} ${r.details}") } @@ -3369,6 +3369,7 @@ sealed class CR { @Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR() @Serializable @SerialName("chatError") class ChatRespError(val user_: User?, val chatError: ChatError): CR() + @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() @Serializable class Response(val type: String, val json: String): CR() @Serializable class Invalid(val str: String): CR() @@ -3478,6 +3479,7 @@ sealed class CR { is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" + is ArchiveImported -> "archiveImported" is Response -> "* $type" is Invalid -> "* invalid json" } @@ -3590,6 +3592,7 @@ sealed class CR { is CmdOk -> withUser(user, noDetails()) is ChatCmdError -> withUser(user_, chatError.string) is ChatRespError -> withUser(user_, chatError.string) + is ArchiveImported -> "${archiveErrors.map { it.string } }" is Response -> json is Invalid -> str } @@ -3678,6 +3681,7 @@ sealed class ChatErrorType { is InvalidConnReq -> "invalidConnReq" is FileAlreadyReceiving -> "fileAlreadyReceiving" is СommandError -> "commandError $message" + is CEException -> "exception $message" } @Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType() @Serializable @SerialName("differentActiveUser") class DifferentActiveUser: ChatErrorType() @@ -3685,6 +3689,7 @@ sealed class ChatErrorType { @Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType() @Serializable @SerialName("fileAlreadyReceiving") class FileAlreadyReceiving: ChatErrorType() @Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType() + @Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType() } @Serializable @@ -3896,3 +3901,13 @@ sealed class XFTPErrorType { @Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType() @Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType() } + +@Serializable +sealed class ArchiveError { + val string: String get() = when (this) { + is ArchiveErrorImport -> "import ${chatError.string}" + is ArchiveErrorImportFile -> "importFile $file ${chatError.string}" + } + @Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError() + @Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError() +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 1643490138..cd9dad7086 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -288,6 +288,14 @@ fun ChatItemView( CICallItemView(cInfo, cItem, status, duration, acceptCall) } + @Composable + fun ModeratedItem() { + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + DefaultDropdownMenu(showMenu) { + DeleteItemAction(cItem, showMenu, questionText = generalGetString(R.string.delete_message_cannot_be_undone_warning), deleteMessage) + } + } + when (val c = cItem.content) { is CIContent.SndMsgContent -> ContentItem() is CIContent.RcvMsgContent -> ContentItem() @@ -314,8 +322,8 @@ fun ChatItemView( is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor) is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red) is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red) - is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) - is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + is CIContent.SndModerated -> ModeratedItem() + is CIContent.RcvModerated -> ModeratedItem() is CIContent.InvalidJSON -> CIInvalidJSONView(c.json) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt index a9cfc1b906..99293f76ef 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/database/DatabaseView.kt @@ -178,8 +178,7 @@ fun DatabaseLayout( val unencrypted = chatDbEncrypted == false SettingsActionItem( if (unencrypted) painterResource(R.drawable.ic_lock_open) else if (useKeyChain) painterResource(R.drawable.ic_vpn_key_filled) - else painterResource(R - .drawable.ic_lock), + else painterResource(R.drawable.ic_lock), stringResource(R.string.database_passphrase), click = showSettingsModal() { DatabaseEncryptionView(it) }, iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary, @@ -574,11 +573,17 @@ private fun importArchive( m.controller.apiDeleteStorage() try { val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString()) - m.controller.apiImportArchive(config) + val archiveErrors = m.controller.apiImportArchive(config) DatabaseUtils.ksDatabasePassword.remove() appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context)) - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) + if (archiveErrors.isEmpty()) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), text = generalGetString(R.string.restart_the_app_to_use_imported_chat_database)) + } + } else { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), text = generalGetString(R.string.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(R.string.non_fatal_errors_occured_during_import)) + } } } catch (e: Error) { operationEnded(m, progressIndicator) { diff --git a/apps/android/app/src/main/res/values-cs/strings.xml b/apps/android/app/src/main/res/values-cs/strings.xml index f7e5bedb72..dc17bd2d53 100644 --- a/apps/android/app/src/main/res/values-cs/strings.xml +++ b/apps/android/app/src/main/res/values-cs/strings.xml @@ -78,7 +78,7 @@ %ds %d min %d hodinu - offered %s: %2s + nabízeno %s: %2s Odkazy na skupiny Hlasové zprávy Vaše kontakty mohou povolit úplné mazání zpráv. @@ -809,7 +809,7 @@ Připojili jste se k této skupině Odmítli jste pozvánku do skupiny Platnost pozvánky do skupiny vypršela - pozvaný %1$s + pozval %1$s připojen změnil roli %s na %s změnil svou roli na %s @@ -1168,11 +1168,11 @@ Sdílet adresu Upraveno v Přijato v - Přijato v: %s - Záznam aktualizován v: %s - Posláno v: %s - Zmizí v: %s - Upraveno v: %s + Přijato: %s + Záznam aktualizován: %s + Posláno: %s + Zmizí: %s + Upraveno: %s %s (aktuální) Tmavý motiv Import motivu @@ -1245,7 +1245,7 @@ (aktuální) ID databáze: %d Smazáno v - Smazáno v: %s + Smazáno: %s Zmizí v Zadat uvítací zprávu… Chyba importu motivu @@ -1261,4 +1261,5 @@ Svou adresu můžete sdílet jako odkaz nebo QR kód - kdokoli se k vám může připojit. Pokud později adresu odstraníte, o kontakty nepřijdete. Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem. + žádný text \ No newline at end of file diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index 9d1de5fa05..bbbe6f3a7a 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -1341,4 +1341,5 @@ Verschwindet um: %s Vergangenheit In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt. + Kein Text \ No newline at end of file diff --git a/apps/android/app/src/main/res/values-es/strings.xml b/apps/android/app/src/main/res/values-es/strings.xml index 4dcde92857..1a6f3d8818 100644 --- a/apps/android/app/src/main/res/values-es/strings.xml +++ b/apps/android/app/src/main/res/values-es/strings.xml @@ -27,7 +27,7 @@ Se puede desactivar en Configuración – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento. Siempre activo Permitir - arriba y luego: + y después: Añadir nuevo contacto: para crear tu código QR de un solo uso para tu contacto. ¿Aceptar solicitud de conexión\? Aceptar incógnito @@ -37,9 +37,9 @@ Todos tus contactos permanecerán conectados. Apariencia Versión - Se usará una conexión TCP independiente (y credencial SOCKS) por cada perfil de chat que tengas en la aplicación. - Se usará una conexión TCP independiente (y credencial SOCKS) por cada contacto y miembro del grupo. -\nAtención: si tienes muchas conexiones, tu consumo de batería y tráfico pueden ser sustancialmente mayor y algunas conexiones pueden fallar. + Se usará una conexión TCP (y credenciales SOCKS) independiente por cada perfil que tengas en la aplicación. + Se usará una conexión TCP (y credenciales SOCKS) independiente por cada contacto y miembro del grupo. +\nAtención: si tienes muchas conexiones, el consumo de batería y tráfico pueden ser sustancialmente mayores y algunas conexiones pueden fallar. a + b Acerca de SimpleX negrita @@ -83,10 +83,10 @@ Tanto tú como tu contacto podéis enviar mensajes temporales. Escanear código QR: para conectar con tu contacto mediante su código QR. Crear - Crear enlace de invitación de un solo uso. - Crear grupo secreto + Crea enlace de invitación de un uso + Crea grupo secreto La contraseña de cifrado de la base de datos será actualizada. - ID de base de datos + ID base de datos Los mensajes directos entre miembros del grupo no están permitidos. La contraseña es distinta a la almacenada en Keystore La base de datos será cifrada y la contraseña se guardará en Keystore. @@ -95,7 +95,7 @@ ¿Eliminar el perfil de chat\? grupo eliminado ¿Eliminar grupo\? - Eliminar después de + Eliminar en Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo. Desactivar Los mensajes temporales no están permitidos en este chat. @@ -142,12 +142,12 @@ conectando (aceptado) conectando (anunciado) conexión %1$d - Conectar mediante enlace / Código QR + Conecta vía enlace / Código QR El contacto y todos los mensajes serán eliminados. ¡No se puede deshacer! Contacto verificado el contacto dispone de cifrado de extremo a extremo Desconectar - Nombre del contacto + Contacto Copiar Crear tu perfil Siempre usar relay @@ -156,7 +156,7 @@ ¿Activar eliminación automática de mensajes\? Preferencias de contacto %ds - Eliminar después de + Eliminar en %d seg El contácto ya existe Error de conexión (Autenticación) @@ -164,15 +164,15 @@ Desconectado Conectado Copiado en portapapeles - Crear enlace de invitación de un solo uso. + Crea enlace de invitación de un uso. 💻 PC: escanéa el código QR desde la aplicación mediante Escanear código QR Eliminar Eliminar ¡El contacto aun no se ha conectado! - ¿Eliminar conexion pendiente\? + ¿Eliminar la conexion pendiente\? Botón cerrar ¡Solicitud de conexión enviada! - Conexión + por conexión conectando llamada… Activar llamadas desde la pantalla de bloqueo en Configuración La contraseña de cifrado de la base de datos será actualizada y almacenada en Keystore. @@ -276,7 +276,7 @@ No se puede recibir el archivo No se puede iniciar la base de datos ¿Vaciar chat\? - Perfil de Chat + por perfil Chat está detenido rol de %s cambiado a %s Cambiar rol @@ -382,7 +382,7 @@ Error al eliminar grupo Error al eliminar conexión de contacto pendiente Ocultar - Oculta + Sin vista previa Se ocultan tanto el contacto como el mensaje Ocultar Para todos @@ -471,7 +471,7 @@ \n1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días. \n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos. \n3. La conexión ha sido comprometida. - Texto del mensaje + Contacto y texto MIEMBRO nunca Se requieren hosts .onion para la conexión @@ -481,7 +481,7 @@ ha salido Mensajes en vivo Notificación instantánea - Servicio de notificaciones + Servicio El mensaje se marcará para eliminar. El destinatario o destinatarios podrán revelar este mensaje. ¡Mensaje en vivo! 📱 móvil: pulse Abrir en aplicación móvil, después pulse Conectar en la aplicación. @@ -600,7 +600,7 @@ Sólo tú puedes enviar mensajes temporales. Sólo tu contacto puede enviar mensajes temporales. No se permiten mensajes de voz. - Se ejecuta sólo cuando la aplicación está abierta + Sólo con aplicación abierta Abrir consola de chat Guardar Restaurar @@ -780,7 +780,7 @@ El servidor requiere autorización para crear colas, comprueba la contraseña Para recibir notificaciones, introduce la contraseña de la base de datos Llamadas de chat SimpleX - Se ejecuta periódicamente + Cíclico Se muestran el nombre del contacto y el mensaje Se muestra sólo el nombre del contacto Bloqueo SimpleX activado @@ -884,7 +884,7 @@ También puedes conectarte pulsando el enlace. Si se abre en el navegador, pulsa en Abrir en aplicación móvil. Puedes ponerte en contacto con los desarrolladores de SimpleX Chat para consultas y para recibir actualizaciones. ¡No puedes enviar mensajes! - Puedes usar la sintaxis markdown para dar formato a los mensajes: + Puedes usar la sintaxis markdown para dar formato a tus mensajes: Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos. Tu contacto debe estar en línea para que se complete la conexión. \nPuedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo). @@ -903,7 +903,7 @@ Esperando archivo Esperando imagen Mensaje de voz (%1$s ) - Si disponible + Si disponibles No almacenamos ninguno de tus contactos o mensajes (una vez entregados) en los servidores. Has sido invitado al grupo. Únete para conectarte con los miembros del grupo. has expulsado a %1$s @@ -1003,15 +1003,15 @@ Reducción consumo de batería Mensaje de bienvenida en grupos ¡Más mejoras en camino! - Contraseña de perfil oculto + Contraseña del perfil oculto ¿Guardar mensaje de bienvenida\? Activar audio Puedes ocultar o silenciar un perfil. Mantenlo pulsado para abrir el menú. Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. Ahora los administradores pueden: \n- borrar mensajes de los miembros. -\n- desactivar el rol a miembros (pasando a rol \"observador\") - Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Tus perfiles Chat. +\n- desactivar el rol miembro (a rol \"observador\") + Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Mis perfiles. Actualización de la base de datos Volviendo a versión anterior de la base de datos Confirmación de migración no válida @@ -1057,7 +1057,7 @@ Mis servidores XFTP Puerto puerto %d - Establece Usar hosts .onion en No si el proxy SOCKS no los admite. + Configura Usar hosts .onion a No si el proxy SOCKS no los admite. Descargar archivo Usar proxy SOCKS Host @@ -1083,8 +1083,8 @@ Puedes activar el Bloqueo SimpleX a través de Configuración. Confirma código Activar bloqueo - Bloquear después de - Modo de bloqueo + Bloquear en + Modo bloqueo Enviar Código de acceso incorrecto Código nuevo @@ -1186,7 +1186,7 @@ Tus contactos en SimpleX lo verán. \nPuedes cambiarlo en Configuración. Tus contactos permanecerán conectados. - Si más tarde decides borrar tu dirección los contactos no se perderán. + Si más tarde decides eliminar tu dirección los contactos no se perderán. Todos los datos de la aplicación se eliminarán. Se creará un perfil vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre. Autodestrucción @@ -1213,19 +1213,19 @@ Enviar mensaje temporal Tiempo personalizado Enviar - Moderado el - Recibido el - Enviado el + Moderado + Recibido + Enviado (actual) - ID de base de datos: %d - Borrado el: %s - Desaparecerá el: %s - Moderado el: %s - Recibido el: %s - Registro actualizado: %s + ID base de datos: %d + Eliminado: %s + Desaparecerá: %s + Moderado: %s + Recibido: %s + Registro actualiz: %s %s (actual) - Enviado el: %s - Desaparecerá el + Enviado: %s + Desaparecerá Reacciones a los mensajes No se permiten reacciones a los mensajes. Permitir reacciones a los mensajes. @@ -1243,11 +1243,11 @@ Las reacciones a los mensajes no están permitidas en este grupo. Sólo tu contacto puede añadir reacciones a los mensajes. 1 minuto - Registro actualizado + Registro actualiz 30 segundos 5 minutos Permitir que tus contactos añadan reacciones a los mensajes. - Eliminado el + Eliminado Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite. Temas personalizados Código de autodestrucción @@ -1256,10 +1256,11 @@ \n- historial de edición. ¡Gracias a los colaboradores! Contribuye a través de Weblate. Mensajes mejorados - Al introducirlo todos los datos son borrados. + Al introducirlo todos los datos son eliminados. Personalizar y compartir temas de color. ¡Por fin los tenemos! 🚀 Reacciones a los mensajes Saber más Interfaz en japonés y portugués + sin texto \ No newline at end of file diff --git a/apps/android/app/src/main/res/values-fr/strings.xml b/apps/android/app/src/main/res/values-fr/strings.xml index 0efdb91834..1f2bff2a09 100644 --- a/apps/android/app/src/main/res/values-fr/strings.xml +++ b/apps/android/app/src/main/res/values-fr/strings.xml @@ -1247,8 +1247,8 @@ Envoyé le ID de base de données : %d Supprimé à : %s - Disparaît à : %s - Disparaît à + Disparaîtra le : %s + Disparaîtra le Interface en japonais et en portugais Merci aux utilisateurs - contribuez via Weblate ! Enfin, les voilà ! 🚀 @@ -1261,4 +1261,5 @@ Code d\'autodestruction Envoyé le : %s Message envoyé + aucun texte \ No newline at end of file diff --git a/apps/android/app/src/main/res/values-nl/strings.xml b/apps/android/app/src/main/res/values-nl/strings.xml index 6d4a34a82b..39b2a99d60 100644 --- a/apps/android/app/src/main/res/values-nl/strings.xml +++ b/apps/android/app/src/main/res/values-nl/strings.xml @@ -910,7 +910,7 @@ Deze string is geen verbinding link! Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren. Deze functie is experimenteel! Het werkt alleen als op de andere client versie 4.2 is geïnstalleerd. U zou het bericht in het gesprek moeten zien zodra de adreswijziging is voltooid. Controleer of u nog steeds berichten van dit contact (of groepslid) kunt ontvangen. - Om je privacy te behouden, heeft de app in plaats van push meldingen een SimpleX achtergrondservice – deze gebruikt een paar procent van de batterij per dag. + Om je privacy te behouden, heeft de app in plaats van push meldingen een SimpleX achtergrond service - deze gebruikt een paar procent van de batterij per dag. Jij staat toe Je bent uitgenodigd voor de groep U kunt verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen. @@ -1260,4 +1260,5 @@ - spraakberichten tot 5 minuten. \n- aangepaste tijd om te verdwijnen. \n- bewerkingsgeschiedenis. + geen tekst \ No newline at end of file diff --git a/apps/android/app/src/main/res/values-pl/strings.xml b/apps/android/app/src/main/res/values-pl/strings.xml index 174a14c324..e3d4f5f894 100644 --- a/apps/android/app/src/main/res/values-pl/strings.xml +++ b/apps/android/app/src/main/res/values-pl/strings.xml @@ -446,7 +446,7 @@ Użyj czatu Gdy aplikacja jest uruchomiona Kontrolujesz przez który serwer(y) odbierać wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości. - Zużywa więcej baterii! Usługa w tle jest zawsze uruchomiona - powiadomienia będą wyświetlane, od razu gdy wiadomości będą dostępne. + Zużywa więcej baterii! Usługa zawsze działa w tle - powiadomienia są wyświetlane, gdy tylko wiadomości są dostępne. Przychodzące połączenie audio Przychodzące połączenie wideo Wklej otrzymany link @@ -925,8 +925,8 @@ Dodaj nowy kontakt: aby stworzyć swój jednorazowy kod QR dla kontaktu. Optymalizacja baterii jest aktywna, wyłącza usługi w tle i okresowe żądania nowych wiadomości. Możesz je ponownie włączyć za pośrednictwem ustawień. Można je wyłączyć poprzez ustawienia - powiadomienia nadal będą pokazywane podczas działania aplikacji. - Najlepsze dla baterii. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona, usługi w tle NIE będą używane. - Dobre dla baterii. Usługa w tle sprawdza nowe wiadomości co 10 minut. Możesz przegapić połączenia i pilne wiadomości. + Najlepsze dla baterii. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona (NIE w tle). + Dobry dla baterii. Usługa w tle sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości. Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. Według profilu czatu (domyślnie) lub połączenia (BETA). Nie można zaprosić kontaktów! @@ -1261,4 +1261,5 @@ - wiadomości głosowe do 5 minut. \n- niestandardowy czas zniknięcia. \n- historia edycji. + brak tekstu \ No newline at end of file diff --git a/apps/android/app/src/main/res/values-pt-rBR/strings.xml b/apps/android/app/src/main/res/values-pt-rBR/strings.xml index 9df7a3b43c..8d3f536fe7 100644 --- a/apps/android/app/src/main/res/values-pt-rBR/strings.xml +++ b/apps/android/app/src/main/res/values-pt-rBR/strings.xml @@ -733,7 +733,7 @@ acima, então: Para conectar via link QR Code - Marcado como verificado + Marcar como verificado Para verificar a criptografia de ponta-a-ponta com seu contato, compare (ou escaneie) o código em seus dispositivos. Salvar servidores\? Os servidores para novas conexões do seu perfil de chat atual @@ -807,7 +807,7 @@ sem detalhes Link de convite de uso único Seu endereço de servidor - Suas configurações + Configurações %s é verificado %s não é verificado Começar novo chat @@ -1258,4 +1258,5 @@ Enviado em Alterar senha de auto-destruição Se você digitar sua senha de auto-destruição ao abrir o aplicativo: + Sem texto \ No newline at end of file diff --git a/apps/android/app/src/main/res/values-zh-rCN/strings.xml b/apps/android/app/src/main/res/values-zh-rCN/strings.xml index 78e1e06846..686f79d04d 100644 --- a/apps/android/app/src/main/res/values-zh-rCN/strings.xml +++ b/apps/android/app/src/main/res/values-zh-rCN/strings.xml @@ -159,8 +159,8 @@ 更改群组角色? 取消链接预览 正在为 %s 更改地址…… - 更改地址…… - 正在更改地址…… + 更改地址中…… + 更改地址中…… 创建您的资料 聊天数据库已删除 聊天数据库已导入 @@ -325,7 +325,7 @@ 连接通话中…… 联系人和所有的消息都将被删除——这是不可逆回的! 联系人姓名 - 连接(介绍邀请) + 连接中(介绍邀请) 连接中…… 联系人可以将信息标记为删除;您将可以查看这些信息。 贡献 @@ -847,7 +847,7 @@ 使用聊天 在我们的 GitHub 存储库 中阅读更多内容。 打开聊天控制台 - Stop chat + 停止聊天程序 权限被拒绝! 点击按钮 感谢您安装 SimpleX Chat @@ -1056,8 +1056,8 @@ 确保 XFTP 服务器地址格式正确、行分隔且不重复。 创建文件 下载文件 - 加载 SMP 服务器时出错 - 加载 XFTP 服务器时出错 + 加载 SMP 服务器错误 + 加载 XFTP 服务器错误 保存 SMP 服务器时出错 端口 %d SOCKS 代理设置 @@ -1108,7 +1108,7 @@ 即将停止发送文件。 停止接收文件? 即将停止接收文件。 - Stop + 停止 撤销文件 撤销文件? 文件将从服务器中删除。 @@ -1159,7 +1159,7 @@ 启用自毁密码 自毁 数据库 ID:%d - 消失在 + 消失于 设置地址错误 自定义主题 创建一个地址,让人们与您联系。 @@ -1176,7 +1176,90 @@ 自定义 (当前) - 已删除在 - 已删除在:%s - 消失在:%s + 已删除于 + 已删除于:%s + 消失于:%s + 禁止消息回应。 + 只有您可以添加消息回应。 + 群组成员可以添加信息回应。 + 该群组禁用了消息回应。 + 自毁密码已更改! + 自毁密码已启用! + 设置密码 + 信息 + 已发信息 + 历史记录 + 发送 + 如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除! + 新的显示名: + 已被管理员移除于 + 已发送于 + 已收到于: %s + 记录更新于: %s + 已被管理员移除于: %s + %s (当前) + 发送于 %s + 收到的信息 + 只有您的联系人可以添加消息回应。 + 打开数据库中…… + 打开聊天资料 + 您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。 + 您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 + 如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。 + 了解更多 + 当人们请求连接时,您可以接受或拒绝它。 + 如果您以后删除您的地址,您不会丢失您的联系人。 + 用户指南中阅读更多。 + 主题颜色 + 与您的联系人保持连接。 + 与联系人分享 + 邀请朋友 + 保存自动接受设置 + 保存设置? + 停止分享 + 你好! +\n用 SimpleX Chat 与我联系:%s + 让我们一起在 SimpleX Chat 里聊天 + 您的 SimpleX 的联系人会看到它。 +\n您可以在设置中更改它。 + 您可以以后创建它 + 分享地址 + 您可以与您的联系人分享该地址,让他们与 %s 联系。 + 预览 + 导入主题 + SimpleX + 请确保该文件具有正确的YAML语法。导出主题,以便有一个主题文件结构的例子。 + 第二选项 + 标题 + 阅读更多 + 终于我们有它们了! 🚀 + 消息回应 + 自毁密码 + 小时 + 感谢用户——通过 Weblate 做出贡献! + 日语和葡萄牙语用户界面 + 分钟 + + + 选择 + + 无文本 + 导入主题错误 + 菜单和警报 + 已发信息 + 消息回应 + 该聊天禁用了消息回应。 + 如果您在打开应用程序时输入自毁密码: + 个人资料更新将被发送给您的联系人。 + 记录更新于 + 禁止消息回应。 + 已收到于 + 收到的信息 + 发送限时消息中 + 与联系人分享地址? + 停止分享地址? + SimpleX 地址 + - 语音消息最长5分钟。 +\n- 自定义限时消息。 +\n- 编辑消息历史。 \ No newline at end of file diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 6c6be6e764..2017bae844 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -892,6 +892,7 @@ Error importing chat database Chat database imported Restart the app to use imported chat database. + Some non-fatal errors occurred during import - you may see Chat console for more details. Delete chat profile? This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Chat database deleted diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 90dfda8d3b..a84f847f01 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -243,8 +243,10 @@ func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } -func apiImportArchive(config: ArchiveConfig) async throws { - try await sendCommandOkResp(.apiImportArchive(config: config)) +func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { + let r = await chatSendCmd(.apiImportArchive(config: config)) + if case let .archiveImported(archiveErrors) = r { return archiveErrors } + throw r } func apiDeleteStorage() async throws { @@ -538,7 +540,6 @@ func apiConnect_(connReq: String) async -> (ConnReqType?, Alert?) { return (nil, nil) } let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq)) - let am = AlertManager.shared switch r { case .sentConfirmation: return (.invitation, nil) case .sentInvitation: return (.contact, nil) diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 15efd5955d..65ec9ef944 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -14,6 +14,7 @@ enum DatabaseAlert: Identifiable { case exportProhibited case importArchive case archiveImported + case archiveImportedWithErrors(archiveErrors: [ArchiveError]) case deleteChat case chatDeleted case deleteLegacyDatabase @@ -27,6 +28,7 @@ enum DatabaseAlert: Identifiable { case .exportProhibited: return "exportProhibited" case .importArchive: return "importArchive" case .archiveImported: return "archiveImported" + case .archiveImportedWithErrors: return "archiveImportedWithErrors" case .deleteChat: return "deleteChat" case .chatDeleted: return "chatDeleted" case .deleteLegacyDatabase: return "deleteLegacyDatabase" @@ -251,7 +253,11 @@ struct DatabaseView: View { title: Text("Chat database imported"), message: Text("Restart the app to use imported chat database") ) - + case .archiveImportedWithErrors: + return Alert( + title: Text("Chat database imported"), + message: Text("Restart the app to use imported chat database") + Text("\n") + Text("Some non-fatal errors occurred during import - you may see Chat console for more details.") + ) case .deleteChat: return Alert( title: Text("Delete chat profile?"), @@ -351,9 +357,13 @@ struct DatabaseView: View { try await apiDeleteStorage() do { let config = ArchiveConfig(archivePath: archivePath.path) - try await apiImportArchive(config: config) + let archiveErrors = try await apiImportArchive(config: config) _ = kcDatabasePassword.remove() - await operationEnded(.archiveImported) + if archiveErrors.isEmpty { + await operationEnded(.archiveImported) + } else { + await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors)) + } } catch let error { await operationEnded(.error(title: "Error importing chat database", error: responseError(error))) } diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index 79b5ea52c8..17b8faacb0 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -203,7 +203,7 @@ struct MigrateToAppGroupView: View { dbContainerGroupDefault.set(.group) resetChatCtrl() try await MainActor.run { try initializeChat(start: false) } - try await apiImportArchive(config: config) + let _ = try await apiImportArchive(config: config) await MainActor.run { setV3DBMigration(.migrated) } } catch let error { dbContainerGroupDefault.set(.documents) diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 16604349ef..5c7e0f8a16 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -5498,6 +5498,7 @@ Servery SimpleX nevidí váš profil. no text + žádný text copied message info in history diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 924e8d978b..d8dbb08e8a 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -5498,6 +5498,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. no text + Kein Text copied message info in history diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index add4477595..95cd6b972a 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -503,7 +503,7 @@ All data is erased when it is entered. - Al introducirlo todos los datos son borrados. + Al introducirlo todos los datos son eliminados. No comment provided by engineer. @@ -1019,7 +1019,7 @@ Connect via link / QR code - Conectar mediante enlace / Código QR + Conecta vía enlace / Código QR No comment provided by engineer. @@ -1099,7 +1099,7 @@ Contact name - Nombre del contacto + Contacto No comment provided by engineer. @@ -1159,7 +1159,7 @@ Create one-time invitation link - Crear enlace de invitación de un solo uso + Crea enlace de invitación de un uso No comment provided by engineer. @@ -1169,7 +1169,7 @@ Create secret group - Crear grupo secreto + Crea grupo secreto No comment provided by engineer. @@ -1209,12 +1209,12 @@ Database ID - ID de base de datos + ID base de datos No comment provided by engineer. Database ID: %d - ID de base de datos: %d + ID base de datos: %d copied message info @@ -1337,7 +1337,7 @@ Delete after - Eliminar después de + Eliminar en No comment provided by engineer. @@ -1452,7 +1452,7 @@ Delete messages after - Eliminar después de + Eliminar en No comment provided by engineer. @@ -1472,7 +1472,7 @@ Delete pending connection? - ¿Eliminar conexion pendiente? + ¿Eliminar la conexion pendiente? No comment provided by engineer. @@ -1492,12 +1492,12 @@ Deleted at - Eliminado el + Eliminado No comment provided by engineer. Deleted at: %@ - Eliminado el: %@ + Eliminado: %@ copied message info @@ -1572,12 +1572,12 @@ Disappears at - Desaparecerá el + Desaparecerá No comment provided by engineer. Disappears at: %@ - Desaparecerá el: %@ + Desaparecerá: %@ copied message info @@ -2237,12 +2237,12 @@ Hidden profile password - Contraseña de perfil oculto + Contraseña del perfil oculto No comment provided by engineer. Hide - Ocultar + Sin vista previa chat item action @@ -2630,12 +2630,12 @@ Lock after - Bloquear después de + Bloquear en No comment provided by engineer. Lock mode - Modo de bloqueo + Modo bloqueo No comment provided by engineer. @@ -2735,7 +2735,7 @@ Message text - Texto del mensaje + Contacto y texto No comment provided by engineer. @@ -2780,12 +2780,12 @@ Moderated at - Moderado el + Moderado No comment provided by engineer. Moderated at: %@ - Moderado el: %@ + Moderado: %@ copied message info @@ -2933,8 +2933,8 @@ - delete members' messages. - disable members ("observer" role) Ahora los administradores pueden: -- borrar mensajes de los miembros. -- desactivar el rol a miembros (pasando a rol "observador") +- eliminar mensajes de los miembros. +- desactivar el rol miembro (a rol "observador") No comment provided by engineer. @@ -3379,12 +3379,12 @@ Received at - Recibido el + Recibido No comment provided by engineer. Received at: %@ - Recibido el: %@ + Recibido: %@ copied message info @@ -3414,12 +3414,12 @@ Record updated at - Registro actualizado + Registro actualiz. No comment provided by engineer. Record updated at: %@ - Registro actualizado: %@ + Registro actualiz: %@ copied message info @@ -3789,12 +3789,12 @@ Sent at - Enviado el + Enviado No comment provided by engineer. Sent at: %@ - Enviado el: %@ + Enviado: %@ copied message info @@ -4353,7 +4353,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Tus perfiles Chat**. + Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Mis perfiles**. No comment provided by engineer. @@ -4676,7 +4676,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb When available - Si disponible + Si disponibles No comment provided by engineer. @@ -4803,7 +4803,7 @@ Bloqueo SimpleX debe estar activado. You can use markdown to format messages: - Puedes usar la sintaxis markdown para dar formato a los mensajes: + Puedes usar la sintaxis markdown para dar formato a tus mensajes: No comment provided by engineer. @@ -5089,7 +5089,7 @@ Los servidores de SimpleX no pueden ver tu perfil. above, then choose: - arriba y luego elige: + y después elige: No comment provided by engineer. @@ -5499,6 +5499,7 @@ Los servidores de SimpleX no pueden ver tu perfil. no text + sin texto copied message info in history diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 42ec2ace15..332ab0422f 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1572,12 +1572,12 @@ Disappears at - Disparaît à + Disparaîtra le No comment provided by engineer. Disappears at: %@ - Disparaît à : %@ + Disparaîtra le : %@ copied message info @@ -5498,6 +5498,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. no text + aucun texte copied message info in history diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 643bc75bcb..349f0c1ca8 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -5497,6 +5497,7 @@ SimpleX servers kunnen uw profiel niet zien. no text + geen tekst copied message info in history diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 1378c98d27..fabc58d013 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -5498,6 +5498,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. no text + brak tekstu copied message info in history diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 73b53d84fb..0b032e2852 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -59,10 +59,12 @@ %@ (current) + %@(当前) No comment provided by engineer. %@ (current): + %@(当前): copied message info @@ -97,6 +99,7 @@ %@: + %@: copied message info @@ -298,6 +301,9 @@ - voice messages up to 5 minutes. - custom time to disappear. - editing history. + - 语音消息最长5分钟。 +- 自定义限时消息。 +- 编辑消息历史。 No comment provided by engineer. @@ -307,6 +313,7 @@ 0s + 0秒 No comment provided by engineer. @@ -341,6 +348,7 @@ 5 minutes + 5分钟 No comment provided by engineer. @@ -350,6 +358,7 @@ 30 seconds + 30秒 No comment provided by engineer. @@ -360,6 +369,8 @@ <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> + <p>你好!</p> +<p><a href="%@">通过 SimpleX Chat </a></p>与我联系 email text @@ -401,6 +412,7 @@ About SimpleX address + 关于 SimpleX 地址 No comment provided by engineer. @@ -431,6 +443,7 @@ Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + 将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。 No comment provided by engineer. @@ -465,6 +478,7 @@ Address + 地址 No comment provided by engineer. @@ -479,6 +493,7 @@ All app data is deleted. + 已删除所有应用程序数据。 No comment provided by engineer. @@ -488,6 +503,7 @@ All data is erased when it is entered. + 所有数据在输入后将被删除。 No comment provided by engineer. @@ -507,6 +523,7 @@ All your contacts will remain connected. Profile update will be sent to your contacts. + 您的所有联系人将保持连接。个人资料更新将发送给您的联系人。 No comment provided by engineer. @@ -531,10 +548,12 @@ Allow message reactions only if your contact allows them. + 只有您的联系人允许时才允许消息回应。 No comment provided by engineer. Allow message reactions. + 允许消息回应。 No comment provided by engineer. @@ -569,6 +588,7 @@ Allow your contacts adding message reactions. + 允许您的联系人添加消息回应。 No comment provided by engineer. @@ -603,6 +623,7 @@ An empty chat profile with the provided name is created, and the app opens as usual. + 已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。 No comment provided by engineer. @@ -627,6 +648,7 @@ App passcode is replaced with self-destruct passcode. + 应用程序密码被替换为自毁密码。 No comment provided by engineer. @@ -691,6 +713,7 @@ Auto-accept + 自动接受 No comment provided by engineer. @@ -720,10 +743,12 @@ Better messages + 更好的消息 No comment provided by engineer. Both you and your contact can add message reactions. + 您和您的联系人都可以添加消息回应。 No comment provided by engineer. @@ -833,10 +858,12 @@ Change self-destruct mode + 更改自毁模式 authentication reason Change self-destruct passcode + 更改自毁密码 authentication reason set passcode view @@ -1087,6 +1114,7 @@ Continue + 继续 No comment provided by engineer. @@ -1106,10 +1134,12 @@ Create SimpleX address + 创建 SimpleX 地址 No comment provided by engineer. Create an address to let people connect with you. + 创建一个地址,让人们与您联系。 No comment provided by engineer. @@ -1169,6 +1199,7 @@ Custom time + 自定义时间 No comment provided by engineer. @@ -1183,6 +1214,7 @@ Database ID: %d + 数据库 ID:%d copied message info @@ -1460,10 +1492,12 @@ Deleted at + 已删除于 No comment provided by engineer. Deleted at: %@ + 已删除于:%@ copied message info @@ -1518,6 +1552,7 @@ Disappearing message + 限时消息 No comment provided by engineer. @@ -1537,10 +1572,12 @@ Disappears at + 消失于 No comment provided by engineer. Disappears at: %@ + 消失于:%@ copied message info @@ -1570,6 +1607,7 @@ Don't create address + 不创建地址 No comment provided by engineer. @@ -1594,6 +1632,7 @@ Duration + 时长 No comment provided by engineer. @@ -1648,10 +1687,12 @@ Enable self-destruct + 启用自毁功能 No comment provided by engineer. Enable self-destruct passcode + 启用自毁密码 set passcode view @@ -1726,10 +1767,12 @@ Enter welcome message… + 输入欢迎消息…… placeholder Enter welcome message… (optional) + 输入欢迎消息……(可选) placeholder @@ -1899,6 +1942,7 @@ Error sending email + 发送电邮错误 No comment provided by engineer. @@ -2023,6 +2067,7 @@ Finally, we have them! 🚀 + 终于我们有它们了! 🚀 No comment provided by engineer. @@ -2112,6 +2157,7 @@ Group members can add message reactions. + 群组成员可以添加信息回应。 No comment provided by engineer. @@ -2216,6 +2262,7 @@ History + 历史记录 copied message info @@ -2250,6 +2297,7 @@ If you can't meet in person, show QR code in a video call, or share the link. + 如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。 No comment provided by engineer. @@ -2259,10 +2307,12 @@ If you enter this passcode when opening the app, all app data will be irreversibly removed! + 如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除! No comment provided by engineer. If you enter your self-destruct passcode while opening the app: + 如果您在打开应用程序时输入自毁密码: No comment provided by engineer. @@ -2372,6 +2422,7 @@ Info + 信息 chat item action @@ -2418,6 +2469,7 @@ Invite friends + 邀请朋友 No comment provided by engineer. @@ -2478,6 +2530,7 @@ Japanese interface + 日语界面 No comment provided by engineer. @@ -2522,6 +2575,7 @@ Learn more + 了解更多 No comment provided by engineer. @@ -2541,6 +2595,7 @@ Let's talk in SimpleX Chat + 让我们一起在 SimpleX Chat 里聊天 email subject @@ -2665,14 +2720,17 @@ Message reactions + 消息回应 chat feature Message reactions are prohibited in this chat. + 该聊天禁用了消息回应。 No comment provided by engineer. Message reactions are prohibited in this group. + 该群组禁用了消息回应。 No comment provided by engineer. @@ -2722,10 +2780,12 @@ Moderated at + 已被管理员移除于 No comment provided by engineer. Moderated at: %@ + 已被管理员移除于:%@ copied message info @@ -2795,6 +2855,7 @@ New display name + 新显示名 No comment provided by engineer. @@ -2938,6 +2999,7 @@ Only you can add message reactions. + 只有您可以添加消息回应。 No comment provided by engineer. @@ -2962,6 +3024,7 @@ Only your contact can add message reactions. + 只有您的联系人可以添加消息回应。 No comment provided by engineer. @@ -3011,6 +3074,7 @@ Opening database… + 打开数据库中…… No comment provided by engineer. @@ -3180,6 +3244,7 @@ Preview + 预览 No comment provided by engineer. @@ -3214,6 +3279,7 @@ Profile update will be sent to your contacts. + 个人资料更新将被发送给您的联系人。 No comment provided by engineer. @@ -3228,10 +3294,12 @@ Prohibit message reactions. + 禁止消息回应。 No comment provided by engineer. Prohibit messages reactions. + 禁止消息回应。 No comment provided by engineer. @@ -3276,6 +3344,7 @@ React... + 回应…… chat item menu @@ -3285,14 +3354,17 @@ Read more + 阅读更多 No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). + 在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。 No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). + 在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。 No comment provided by engineer. @@ -3307,10 +3379,12 @@ Received at + 已收到于 No comment provided by engineer. Received at: %@ + 已收到于:%@ copied message info @@ -3320,6 +3394,7 @@ Received message + 收到的信息 message info title @@ -3339,10 +3414,12 @@ Record updated at + 记录更新于 No comment provided by engineer. Record updated at: %@ + 记录更新于:%@ copied message info @@ -3522,6 +3599,7 @@ Save auto-accept settings + 保存自动接受设置 No comment provided by engineer. @@ -3561,6 +3639,7 @@ Save settings? + 保存设置? No comment provided by engineer. @@ -3615,22 +3694,27 @@ Select + 选择 No comment provided by engineer. Self-destruct + 自毁 No comment provided by engineer. Self-destruct passcode + 自毁密码 No comment provided by engineer. Self-destruct passcode changed! + 自毁密码已更改! No comment provided by engineer. Self-destruct passcode enabled! + 自毁密码已启用! No comment provided by engineer. @@ -3650,6 +3734,7 @@ Send disappearing message + 发送限时消息中 No comment provided by engineer. @@ -3704,10 +3789,12 @@ Sent at + 已发送于 No comment provided by engineer. Sent at: %@ + 已发送于:%@ copied message info @@ -3717,6 +3804,7 @@ Sent message + 已发信息 message info title @@ -3766,6 +3854,7 @@ Set passcode + 设置密码 No comment provided by engineer. @@ -3800,10 +3889,12 @@ Share address + 分享地址 No comment provided by engineer. Share address with contacts? + 与联系人分享地址? No comment provided by engineer. @@ -3818,6 +3909,7 @@ Share with contacts + 与联系人分享 No comment provided by engineer. @@ -3842,6 +3934,7 @@ SimpleX Address + SimpleX 地址 No comment provided by engineer. @@ -3871,6 +3964,7 @@ SimpleX address + SimpleX 地址 No comment provided by engineer. @@ -3970,10 +4064,12 @@ Stop sharing + 停止分享 No comment provided by engineer. Stop sharing address? + 停止分享地址? No comment provided by engineer. @@ -4220,6 +4316,7 @@ It can happen because of some bug or when the connection is compromised. To connect, your contact can scan QR code or use the link in the app. + 您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。 No comment provided by engineer. @@ -4331,6 +4428,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unit + 单位 No comment provided by engineer. @@ -4582,6 +4680,7 @@ To connect, please ask your contact to create another connection link and check When people request to connect, you can accept or reject it. + 当人们请求连接时,您可以接受或拒绝它。 No comment provided by engineer. @@ -4656,6 +4755,7 @@ To connect, please ask your contact to create another connection link and check You can create it later + 您可以以后创建它 No comment provided by engineer. @@ -4682,6 +4782,7 @@ SimpleX Lock must be enabled. You can share this address with your contacts to let them connect with **%@**. + 您可以与您的联系人分享该地址,让他们与 **%@** 联系。 No comment provided by engineer. @@ -4801,7 +4902,7 @@ SimpleX Lock must be enabled. You won't lose your contacts if you later delete your address. - 如果您以后删除它,您不会丢失您的联系人。 + 如果您以后删除您的地址,您不会丢失您的联系人。 No comment provided by engineer. @@ -4894,10 +4995,13 @@ You can cancel this connection and remove the contact (and try later with a new Your contacts in SimpleX will see it. You can change it in Settings. + 您的 SimpleX 的联系人会看到它。 +您可以在设置中更改它。 No comment provided by engineer. Your contacts will remain connected. + 与您的联系人保持连接。 No comment provided by engineer. @@ -5109,7 +5213,7 @@ SimpleX 服务器无法看到您的资料。 connecting (introduction invitation) - 连接(介绍邀请) + 连接中(介绍邀请) No comment provided by engineer. @@ -5149,6 +5253,7 @@ SimpleX 服务器无法看到您的资料。 custom + 自定义 dropdown time picker choice @@ -5158,6 +5263,7 @@ SimpleX 服务器无法看到您的资料。 days + time unit @@ -5237,6 +5343,7 @@ SimpleX 服务器无法看到您的资料。 hours + 小时 time unit @@ -5346,6 +5453,7 @@ SimpleX 服务器无法看到您的资料。 minutes + 分钟 time unit @@ -5365,6 +5473,7 @@ SimpleX 服务器无法看到您的资料。 months + time unit @@ -5389,6 +5498,7 @@ SimpleX 服务器无法看到您的资料。 no text + 无文本 copied message info in history @@ -5469,6 +5579,7 @@ SimpleX 服务器无法看到您的资料。 seconds + time unit @@ -5548,6 +5659,7 @@ SimpleX 服务器无法看到您的资料。 weeks + time unit diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index f4398f3938..b7b1381967 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -1470,7 +1470,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1491,7 +1491,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.1; + MARKETING_VERSION = 5.1.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1512,7 +1512,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1533,7 +1533,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.1; + MARKETING_VERSION = 5.1.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1592,7 +1592,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1605,7 +1605,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.1; + MARKETING_VERSION = 5.1.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1624,7 +1624,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 147; + CURRENT_PROJECT_VERSION = 148; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1637,7 +1637,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.1; + MARKETING_VERSION = 5.1.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 8e4f87a408..f2759e8cec 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -495,6 +495,7 @@ public enum ChatResponse: Decodable, Error { case cmdOk(user: User?) case chatCmdError(user_: User?, chatError: ChatError) case chatError(user_: User?, chatError: ChatError) + case archiveImported(archiveErrors: [ArchiveError]) public var responseType: String { get { @@ -609,6 +610,7 @@ public enum ChatResponse: Decodable, Error { case .cmdOk: return "cmdOk" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" + case .archiveImported: return "archiveImported" } } } @@ -726,6 +728,7 @@ public enum ChatResponse: Decodable, Error { case .cmdOk: return noDetails case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) + case let .archiveImported(archiveErrors): return String(describing: archiveErrors) } } } @@ -1248,6 +1251,7 @@ public enum ChatErrorType: Decodable { case invalidChatItemDelete case agentVersion case commandError(message: String) + case exception(message: String) } public enum StoreError: Decodable { @@ -1386,3 +1390,8 @@ public enum SMPAgentError: Decodable { case A_VERSION case A_ENCRYPTION } + +public enum ArchiveError: Decodable { + case `import`(chatError: ChatError) + case importFile(file: String, chatError: ChatError) +} diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index c62ceb3d35..1a70b7cdc7 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1995,6 +1995,9 @@ /* No comment provided by engineer. */ "No received or sent files" = "Žádné přijaté ani odeslané soubory"; +/* copied message info in history */ +"no text" = "žádný text"; + /* No comment provided by engineer. */ "Notifications" = "Oznámení"; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index a29ebc64b5..6f815f0626 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -1995,6 +1995,9 @@ /* No comment provided by engineer. */ "No received or sent files" = "Keine empfangenen oder gesendeten Dateien"; +/* copied message info in history */ +"no text" = "Kein Text"; + /* No comment provided by engineer. */ "Notifications" = "Benachrichtigungen"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index e87a9eeb66..ddcf9c7e8e 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -257,7 +257,7 @@ "About SimpleX Chat" = "Sobre SimpleX Chat"; /* No comment provided by engineer. */ -"above, then choose:" = "arriba y luego elige:"; +"above, then choose:" = "y después elige:"; /* No comment provided by engineer. */ "Accent color" = "Color"; @@ -318,7 +318,7 @@ "All chats and messages will be deleted - this cannot be undone!" = "Se eliminarán todos los chats y mensajes. ¡No se puede deshacer!"; /* No comment provided by engineer. */ -"All data is erased when it is entered." = "Al introducirlo todos los datos son borrados."; +"All data is erased when it is entered." = "Al introducirlo todos los datos son eliminados."; /* No comment provided by engineer. */ "All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; @@ -679,7 +679,7 @@ "Connect via link" = "Conectar mediante enlace"; /* No comment provided by engineer. */ -"Connect via link / QR code" = "Conectar mediante enlace / Código QR"; +"Connect via link / QR code" = "Conecta vía enlace / Código QR"; /* No comment provided by engineer. */ "Connect via one-time link?" = "¿Conectar mediante enlace de un uso?"; @@ -763,7 +763,7 @@ "Contact is not connected yet!" = "¡El contacto aun no se ha conectado!"; /* No comment provided by engineer. */ -"Contact name" = "Nombre del contacto"; +"Contact name" = "Contacto"; /* No comment provided by engineer. */ "Contact preferences" = "Preferencias de contacto"; @@ -796,13 +796,13 @@ "Create link" = "Crear enlace"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Crear enlace de invitación de un solo uso"; +"Create one-time invitation link" = "Crea enlace de invitación de un uso"; /* server test step */ "Create queue" = "Crear cola"; /* No comment provided by engineer. */ -"Create secret group" = "Crear grupo secreto"; +"Create secret group" = "Crea grupo secreto"; /* No comment provided by engineer. */ "Create SimpleX address" = "Crear dirección SimpleX"; @@ -850,10 +850,10 @@ "Database error" = "Error de la base de datos"; /* No comment provided by engineer. */ -"Database ID" = "ID de base de datos"; +"Database ID" = "ID base de datos"; /* copied message info */ -"Database ID: %d" = "ID de base de datos: %d"; +"Database ID: %d" = "ID base de datos: %d"; /* No comment provided by engineer. */ "Database IDs and Transport isolation option." = "IDs de la base de datos y opciónes de aislamiento de transporte."; @@ -913,7 +913,7 @@ "Delete address?" = "¿Eliminar la dirección?"; /* No comment provided by engineer. */ -"Delete after" = "Eliminar después de"; +"Delete after" = "Eliminar en"; /* No comment provided by engineer. */ "Delete all files" = "Eliminar todos los archivos"; @@ -985,7 +985,7 @@ "Delete messages" = "Eliminar mensaje"; /* No comment provided by engineer. */ -"Delete messages after" = "Eliminar después de"; +"Delete messages after" = "Eliminar en"; /* No comment provided by engineer. */ "Delete old database" = "Eliminar base de datos antigua"; @@ -997,7 +997,7 @@ "Delete pending connection" = "Eliminar conexión pendiente"; /* No comment provided by engineer. */ -"Delete pending connection?" = "¿Eliminar conexion pendiente?"; +"Delete pending connection?" = "¿Eliminar la conexion pendiente?"; /* No comment provided by engineer. */ "Delete profile" = "Eliminar perfil"; @@ -1012,10 +1012,10 @@ "deleted" = "eliminado"; /* No comment provided by engineer. */ -"Deleted at" = "Eliminado el"; +"Deleted at" = "Eliminado"; /* copied message info */ -"Deleted at: %@" = "Eliminado el: %@"; +"Deleted at: %@" = "Eliminado: %@"; /* rcv group event chat item */ "deleted group" = "grupo eliminado"; @@ -1069,10 +1069,10 @@ "Disappearing messages are prohibited in this group." = "Los mensajes temporales no están permitidos en este grupo."; /* No comment provided by engineer. */ -"Disappears at" = "Desaparecerá el"; +"Disappears at" = "Desaparecerá"; /* copied message info */ -"Disappears at: %@" = "Desaparecerá el: %@"; +"Disappears at: %@" = "Desaparecerá: %@"; /* server test step */ "Disconnect" = "Desconectar"; @@ -1498,10 +1498,10 @@ "Hidden chat profiles" = "Perfiles Chat ocultos"; /* No comment provided by engineer. */ -"Hidden profile password" = "Contraseña de perfil oculto"; +"Hidden profile password" = "Contraseña del perfil oculto"; /* chat item action */ -"Hide" = "Ocultar"; +"Hide" = "Sin vista previa"; /* No comment provided by engineer. */ "Hide app screen in the recent apps." = "Ocultar pantalla de aplicaciones en aplicaciones recientes."; @@ -1783,10 +1783,10 @@ "Local profile data only" = "Sólo datos del perfil local"; /* No comment provided by engineer. */ -"Lock after" = "Bloquear después de"; +"Lock after" = "Bloquear en"; /* No comment provided by engineer. */ -"Lock mode" = "Modo de bloqueo"; +"Lock mode" = "Modo bloqueo"; /* No comment provided by engineer. */ "Make a private connection" = "Establecer una conexión privada"; @@ -1858,7 +1858,7 @@ "message received" = "mensaje recibido"; /* No comment provided by engineer. */ -"Message text" = "Texto del mensaje"; +"Message text" = "Contacto y texto"; /* No comment provided by engineer. */ "Messages" = "Mensajes"; @@ -1894,10 +1894,10 @@ "moderated" = "moderado"; /* No comment provided by engineer. */ -"Moderated at" = "Moderado el"; +"Moderated at" = "Moderado"; /* copied message info */ -"Moderated at: %@" = "Moderado el: %@"; +"Moderated at: %@" = "Moderado: %@"; /* No comment provided by engineer. */ "moderated by %@" = "moderado por %@"; @@ -1995,6 +1995,9 @@ /* No comment provided by engineer. */ "No received or sent files" = "Sin archivos recibidos o enviados"; +/* copied message info in history */ +"no text" = "sin texto"; + /* No comment provided by engineer. */ "Notifications" = "Notificaciones"; @@ -2002,7 +2005,7 @@ "Notifications are disabled!" = "¡Las notificaciones están desactivadas!"; /* No comment provided by engineer. */ -"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ahora los administradores pueden:\n- borrar mensajes de los miembros.\n- desactivar el rol a miembros (pasando a rol \"observador\")"; +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Ahora los administradores pueden:\n- eliminar mensajes de los miembros.\n- desactivar el rol miembro (a rol \"observador\")"; /* member role */ "observer" = "observador"; @@ -2297,10 +2300,10 @@ "received answer…" = "respuesta recibida…"; /* No comment provided by engineer. */ -"Received at" = "Recibido el"; +"Received at" = "Recibido"; /* copied message info */ -"Received at: %@" = "Recibido el: %@"; +"Received at: %@" = "Recibido: %@"; /* No comment provided by engineer. */ "received confirmation…" = "confirmación recibida…"; @@ -2321,10 +2324,10 @@ "Recipients see updates as you type them." = "Los destinatarios ven la actualizacion mientras escribes."; /* No comment provided by engineer. */ -"Record updated at" = "Registro actualizado"; +"Record updated at" = "Registro actualiz."; /* copied message info */ -"Record updated at: %@" = "Registro actualizado: %@"; +"Record updated at: %@" = "Registro actualiz: %@"; /* No comment provided by engineer. */ "Reduced battery usage" = "Reducción del uso de la batería"; @@ -2564,10 +2567,10 @@ "Sending via" = "Enviando vía"; /* No comment provided by engineer. */ -"Sent at" = "Enviado el"; +"Sent at" = "Enviado"; /* copied message info */ -"Sent at: %@" = "Enviado el: %@"; +"Sent at: %@" = "Enviado: %@"; /* notification */ "Sent file event" = "Evento de archivo enviado"; @@ -2912,7 +2915,7 @@ "To record voice message please grant permission to use Microphone." = "Para grabar el mensaje de voz concede permiso para usar el micrófono."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Tus perfiles Chat**."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Mis perfiles**."; /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar."; @@ -3140,7 +3143,7 @@ "What's new" = "Novedades"; /* No comment provided by engineer. */ -"When available" = "Si disponible"; +"When available" = "Si disponibles"; /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud."; @@ -3224,7 +3227,7 @@ "You can turn on SimpleX Lock via Settings." = "Puedes activar el Bloqueo SimpleX a través de Configuración."; /* No comment provided by engineer. */ -"You can use markdown to format messages:" = "Puedes usar la sintaxis markdown para dar formato a los mensajes:"; +"You can use markdown to format messages:" = "Puedes usar la sintaxis markdown para dar formato a tus mensajes:"; /* No comment provided by engineer. */ "You can't send messages!" = "¡No puedes enviar mensajes!"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index c9ed230b01..49021c4be8 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1069,10 +1069,10 @@ "Disappearing messages are prohibited in this group." = "Les messages éphémères sont interdits dans ce groupe."; /* No comment provided by engineer. */ -"Disappears at" = "Disparaît à"; +"Disappears at" = "Disparaîtra le"; /* copied message info */ -"Disappears at: %@" = "Disparaît à : %@"; +"Disappears at: %@" = "Disparaîtra le : %@"; /* server test step */ "Disconnect" = "Se déconnecter"; @@ -1995,6 +1995,9 @@ /* No comment provided by engineer. */ "No received or sent files" = "Aucun fichier reçu ou envoyé"; +/* copied message info in history */ +"no text" = "aucun texte"; + /* No comment provided by engineer. */ "Notifications" = "Notifications"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 9d86867ee5..381369d3d2 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1995,6 +1995,9 @@ /* No comment provided by engineer. */ "No received or sent files" = "Geen ontvangen of verzonden bestanden"; +/* copied message info in history */ +"no text" = "geen tekst"; + /* No comment provided by engineer. */ "Notifications" = "Meldingen"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 1982aaafb8..d134e82113 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1995,6 +1995,9 @@ /* No comment provided by engineer. */ "No received or sent files" = "Brak odebranych lub wysłanych plików"; +/* copied message info in history */ +"no text" = "brak tekstu"; + /* No comment provided by engineer. */ "Notifications" = "Powiadomienia"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 7699d8c6e2..996011688e 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_斜体_"; +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 语音消息最长5分钟。\n- 自定义限时消息。\n- 编辑消息历史。"; + /* No comment provided by engineer. */ ", " = ", "; @@ -88,6 +91,12 @@ /* No comment provided by engineer. */ "%@" = "%@"; +/* No comment provided by engineer. */ +"%@ (current)" = "%@(当前)"; + +/* copied message info */ +"%@ (current):" = "%@(当前):"; + /* No comment provided by engineer. */ "%@ / %@" = "%@ / %@"; @@ -109,6 +118,9 @@ /* notification title */ "%@ wants to connect!" = "%@ 要连接!"; +/* copied message info */ +"%@:" = "%@:"; + /* time interval */ "%d days" = "%d 天"; @@ -184,9 +196,15 @@ /* No comment provided by engineer. */ "`a + b`" = "\\`a + b`"; +/* email text */ +"

Hi!

\n

Connect to me via SimpleX Chat

" = "

你好!

\n

通过 SimpleX Chat

与我联系"; + /* No comment provided by engineer. */ "~strike~" = "\\~删去~"; +/* No comment provided by engineer. */ +"0s" = "0秒"; + /* time interval */ "1 day" = "1天"; @@ -205,9 +223,15 @@ /* No comment provided by engineer. */ "1-time link" = "一次性链接"; +/* No comment provided by engineer. */ +"5 minutes" = "5分钟"; + /* No comment provided by engineer. */ "6" = "6"; +/* No comment provided by engineer. */ +"30 seconds" = "30秒"; + /* notification title */ "A new contact" = "新联系人"; @@ -226,6 +250,9 @@ /* No comment provided by engineer. */ "About SimpleX" = "关于SimpleX"; +/* No comment provided by engineer. */ +"About SimpleX address" = "关于 SimpleX 地址"; + /* No comment provided by engineer. */ "About SimpleX Chat" = "关于SimpleX Chat"; @@ -251,6 +278,9 @@ /* call status */ "accepted call" = "已接受通话"; +/* No comment provided by engineer. */ +"Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。"; + /* No comment provided by engineer. */ "Add preset servers" = "添加预设服务器"; @@ -269,6 +299,9 @@ /* No comment provided by engineer. */ "Add welcome message" = "添加欢迎信息"; +/* No comment provided by engineer. */ +"Address" = "地址"; + /* member role */ "admin" = "管理员"; @@ -278,9 +311,15 @@ /* No comment provided by engineer. */ "Advanced network settings" = "高级网络设置"; +/* No comment provided by engineer. */ +"All app data is deleted." = "已删除所有应用程序数据。"; + /* No comment provided by engineer. */ "All chats and messages will be deleted - this cannot be undone!" = "所有聊天记录和消息将被删除——这一行为无法撤销!"; +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "所有数据在输入后将被删除。"; + /* No comment provided by engineer. */ "All group members will remain connected." = "所有群组成员将保持连接。"; @@ -290,6 +329,9 @@ /* No comment provided by engineer. */ "All your contacts will remain connected." = "所有联系人会保持连接。"; +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "您的所有联系人将保持连接。个人资料更新将发送给您的联系人。"; + /* No comment provided by engineer. */ "Allow" = "允许"; @@ -302,6 +344,12 @@ /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you." = "仅有您的联系人许可后才允许不可撤回消息移除。"; +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "只有您的联系人允许时才允许消息回应。"; + +/* No comment provided by engineer. */ +"Allow message reactions." = "允许消息回应。"; + /* No comment provided by engineer. */ "Allow sending direct messages to members." = "允许向成员发送私信。"; @@ -320,6 +368,9 @@ /* No comment provided by engineer. */ "Allow voice messages?" = "允许语音消息?"; +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "允许您的联系人添加消息回应。"; + /* No comment provided by engineer. */ "Allow your contacts to call you." = "允许您的联系人给您打电话。"; @@ -341,6 +392,9 @@ /* No comment provided by engineer. */ "Always use relay" = "一直使用中继"; +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。"; + /* No comment provided by engineer. */ "Answer call" = "接听来电"; @@ -353,6 +407,9 @@ /* No comment provided by engineer. */ "App passcode" = "应用程序密码"; +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "应用程序密码被替换为自毁密码。"; + /* No comment provided by engineer. */ "App version" = "应用程序版本"; @@ -392,6 +449,9 @@ /* No comment provided by engineer. */ "Authentication unavailable" = "身份验证不可用"; +/* No comment provided by engineer. */ +"Auto-accept" = "自动接受"; + /* No comment provided by engineer. */ "Auto-accept contact requests" = "自动接受联系人请求"; @@ -413,9 +473,15 @@ /* No comment provided by engineer. */ "Bad message ID" = "错误消息 ID"; +/* No comment provided by engineer. */ +"Better messages" = "更好的消息"; + /* No comment provided by engineer. */ "bold" = "加粗"; +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "您和您的联系人都可以添加消息回应。"; + /* No comment provided by engineer. */ "Both you and your contact can irreversibly delete sent messages." = "您和您的联系人都可以不可逆转地删除已发送的消息。"; @@ -491,6 +557,13 @@ /* No comment provided by engineer. */ "Change role" = "改变角色"; +/* authentication reason */ +"Change self-destruct mode" = "更改自毁模式"; + +/* authentication reason + set passcode view */ +"Change self-destruct passcode" = "更改自毁密码"; + /* chat item text */ "changed address for you" = "为您更改地址"; @@ -627,7 +700,7 @@ "connecting (introduced)" = "连接中(已介绍)"; /* No comment provided by engineer. */ -"connecting (introduction invitation)" = "连接(介绍邀请)"; +"connecting (introduction invitation)" = "连接中(介绍邀请)"; /* call status */ "connecting call" = "连接通话中……"; @@ -698,6 +771,9 @@ /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "联系人可以将信息标记为删除;您将可以查看这些信息。"; +/* No comment provided by engineer. */ +"Continue" = "继续"; + /* chat item action */ "Copy" = "复制"; @@ -707,6 +783,9 @@ /* No comment provided by engineer. */ "Create" = "创建"; +/* No comment provided by engineer. */ +"Create an address to let people connect with you." = "创建一个地址,让人们与您联系。"; + /* server test step */ "Create file" = "创建文件"; @@ -725,6 +804,9 @@ /* No comment provided by engineer. */ "Create secret group" = "创建私密群组"; +/* No comment provided by engineer. */ +"Create SimpleX address" = "创建 SimpleX 地址"; + /* No comment provided by engineer. */ "Create your profile" = "创建您的资料"; @@ -743,6 +825,12 @@ /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "目前支持的最大文件大小为 %@。"; +/* dropdown time picker choice */ +"custom" = "自定义"; + +/* No comment provided by engineer. */ +"Custom time" = "自定义时间"; + /* No comment provided by engineer. */ "Dark" = "深色"; @@ -764,6 +852,9 @@ /* No comment provided by engineer. */ "Database ID" = "数据库 ID"; +/* copied message info */ +"Database ID: %d" = "数据库 ID:%d"; + /* No comment provided by engineer. */ "Database IDs and Transport isolation option." = "数据库 ID 和传输隔离选项。"; @@ -800,6 +891,9 @@ /* No comment provided by engineer. */ "Database will be migrated when the app restarts" = "应用程序重新启动时将迁移数据库"; +/* time unit */ +"days" = "天"; + /* No comment provided by engineer. */ "Decentralized" = "分散式"; @@ -917,6 +1011,12 @@ /* deleted chat item */ "deleted" = "已删除"; +/* No comment provided by engineer. */ +"Deleted at" = "已删除于"; + +/* copied message info */ +"Deleted at: %@" = "已删除于:%@"; + /* rcv group event chat item */ "deleted group" = "已删除群组"; @@ -956,6 +1056,9 @@ /* authentication reason */ "Disable SimpleX Lock" = "禁用 SimpleX 锁定"; +/* No comment provided by engineer. */ +"Disappearing message" = "限时消息"; + /* chat feature */ "Disappearing messages" = "限时消息"; @@ -965,6 +1068,12 @@ /* No comment provided by engineer. */ "Disappearing messages are prohibited in this group." = "该组禁止限时消息。"; +/* No comment provided by engineer. */ +"Disappears at" = "消失于"; + +/* copied message info */ +"Disappears at: %@" = "消失于:%@"; + /* server test step */ "Disconnect" = "断开连接"; @@ -980,6 +1089,9 @@ /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "请勿使用 SimpleX 进行紧急通话。"; +/* No comment provided by engineer. */ +"Don't create address" = "不创建地址"; + /* No comment provided by engineer. */ "Don't show again" = "不再显示"; @@ -995,6 +1107,9 @@ /* integrity error chat item */ "duplicate message" = "重复的消息"; +/* No comment provided by engineer. */ +"Duration" = "时长"; + /* No comment provided by engineer. */ "e2e encrypted" = "端到端加密"; @@ -1022,6 +1137,12 @@ /* No comment provided by engineer. */ "Enable periodic notifications?" = "启用定期通知?"; +/* No comment provided by engineer. */ +"Enable self-destruct" = "启用自毁功能"; + +/* set passcode view */ +"Enable self-destruct passcode" = "启用自毁密码"; + /* authentication reason */ "Enable SimpleX Lock" = "启用 SimpleX 锁定"; @@ -1085,6 +1206,12 @@ /* No comment provided by engineer. */ "Enter server manually" = "手动输入服务器"; +/* placeholder */ +"Enter welcome message…" = "输入欢迎消息……"; + +/* placeholder */ +"Enter welcome message… (optional)" = "输入欢迎消息……(可选)"; + /* No comment provided by engineer. */ "error" = "错误"; @@ -1187,6 +1314,9 @@ /* No comment provided by engineer. */ "Error saving user password" = "保存用户密码时出错"; +/* No comment provided by engineer. */ +"Error sending email" = "发送电邮错误"; + /* No comment provided by engineer. */ "Error sending message" = "发送消息错误"; @@ -1259,6 +1389,9 @@ /* No comment provided by engineer. */ "Files & media" = "文件和媒体"; +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "终于我们有它们了! 🚀"; + /* No comment provided by engineer. */ "For console" = "用于控制台"; @@ -1313,6 +1446,9 @@ /* No comment provided by engineer. */ "Group links" = "群组链接"; +/* No comment provided by engineer. */ +"Group members can add message reactions." = "群组成员可以添加信息回应。"; + /* No comment provided by engineer. */ "Group members can irreversibly delete sent messages." = "群组成员可以不可撤回地删除已发送的消息。"; @@ -1376,6 +1512,12 @@ /* No comment provided by engineer. */ "Hide:" = "隐藏:"; +/* copied message info */ +"History" = "历史记录"; + +/* time unit */ +"hours" = "小时"; + /* No comment provided by engineer. */ "How it works" = "工作原理"; @@ -1394,9 +1536,18 @@ /* No comment provided by engineer. */ "ICE servers (one per line)" = "ICE 服务器(每行一个)"; +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。"; + /* No comment provided by engineer. */ "If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "如果您不能亲自见面,您可以**扫描视频通话中的二维码**,或者您的联系人可以分享邀请链接。"; +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "如果您在打开应用程序时输入自毁密码:"; + /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "如果您现在需要使用聊天,请点击下面的**稍后再做**(当您重新启动应用程序时,系统会提示您迁移数据库)。"; @@ -1472,6 +1623,9 @@ /* connection level description */ "indirect (%d)" = "间接(%d)"; +/* chat item action */ +"Info" = "信息"; + /* No comment provided by engineer. */ "Initial role" = "初始角色"; @@ -1508,6 +1662,9 @@ /* group name */ "invitation to group %@" = "邀请您加入群组 %@"; +/* No comment provided by engineer. */ +"Invite friends" = "邀请朋友"; + /* No comment provided by engineer. */ "Invite members" = "邀请成员"; @@ -1559,6 +1716,9 @@ /* No comment provided by engineer. */ "italic" = "斜体"; +/* No comment provided by engineer. */ +"Japanese interface" = "日语界面"; + /* No comment provided by engineer. */ "Join" = "加入"; @@ -1583,6 +1743,9 @@ /* No comment provided by engineer. */ "Large file!" = "大文件!"; +/* No comment provided by engineer. */ +"Learn more" = "了解更多"; + /* No comment provided by engineer. */ "Leave" = "离开"; @@ -1595,6 +1758,9 @@ /* rcv group event chat item */ "left" = "已离开"; +/* email subject */ +"Let's talk in SimpleX Chat" = "让我们一起在 SimpleX Chat 里聊天"; + /* No comment provided by engineer. */ "Light" = "浅色"; @@ -1679,6 +1845,15 @@ /* No comment provided by engineer. */ "Message draft" = "消息草稿"; +/* chat feature */ +"Message reactions" = "消息回应"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "该聊天禁用了消息回应。"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this group." = "该群组禁用了消息回应。"; + /* notification */ "message received" = "消息已收到"; @@ -1706,6 +1881,9 @@ /* No comment provided by engineer. */ "Migrations: %@" = "迁移:%@"; +/* time unit */ +"minutes" = "分钟"; + /* call status */ "missed call" = "未接来电"; @@ -1715,9 +1893,18 @@ /* moderated chat item */ "moderated" = "已被管理员移除"; +/* No comment provided by engineer. */ +"Moderated at" = "已被管理员移除于"; + +/* copied message info */ +"Moderated at: %@" = "已被管理员移除于:%@"; + /* No comment provided by engineer. */ "moderated by %@" = "由 %@ 审核"; +/* time unit */ +"months" = "月"; + /* No comment provided by engineer. */ "More improvements are coming soon!" = "更多改进即将推出!"; @@ -1757,6 +1944,9 @@ /* No comment provided by engineer. */ "New database archive" = "新数据库存档"; +/* No comment provided by engineer. */ +"New display name" = "新显示名"; + /* No comment provided by engineer. */ "New in %@" = "%@ 的新内容"; @@ -1805,6 +1995,9 @@ /* No comment provided by engineer. */ "No received or sent files" = "未收到或发送文件"; +/* copied message info in history */ +"no text" = "无文本"; + /* No comment provided by engineer. */ "Notifications" = "通知"; @@ -1866,6 +2059,9 @@ /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "只有群主可以启用语音信息。"; +/* No comment provided by engineer. */ +"Only you can add message reactions." = "只有您可以添加消息回应。"; + /* No comment provided by engineer. */ "Only you can irreversibly delete messages (your contact can mark them for deletion)." = "只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。"; @@ -1878,6 +2074,9 @@ /* No comment provided by engineer. */ "Only you can send voice messages." = "只有您可以发送语音消息。"; +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "只有您的联系人可以添加消息回应。"; + /* No comment provided by engineer. */ "Only your contact can irreversibly delete messages (you can mark them for deletion)." = "只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。"; @@ -1905,6 +2104,9 @@ /* No comment provided by engineer. */ "Open-source protocol and code – anybody can run the servers." = "开源协议和代码——任何人都可以运行服务器。"; +/* No comment provided by engineer. */ +"Opening database…" = "打开数据库中……"; + /* No comment provided by engineer. */ "Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." = "在浏览器中打开链接可能会降低连接的隐私和安全性。SimpleX 上不受信任的链接将显示为红色。"; @@ -2013,6 +2215,9 @@ /* No comment provided by engineer. */ "Preset server address" = "预设服务器地址"; +/* No comment provided by engineer. */ +"Preview" = "预览"; + /* No comment provided by engineer. */ "Privacy & security" = "隐私和安全"; @@ -2031,12 +2236,21 @@ /* No comment provided by engineer. */ "Profile password" = "个人资料密码"; +/* No comment provided by engineer. */ +"Profile update will be sent to your contacts." = "个人资料更新将被发送给您的联系人。"; + /* No comment provided by engineer. */ "Prohibit audio/video calls." = "禁止音频/视频通话。"; /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "禁止不可撤回消息删除。"; +/* No comment provided by engineer. */ +"Prohibit message reactions." = "禁止消息回应。"; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "禁止消息回应。"; + /* No comment provided by engineer. */ "Prohibit sending direct messages to members." = "禁止向成员发送私信。"; @@ -2061,9 +2275,21 @@ /* No comment provided by engineer. */ "Rate the app" = "评价此应用程序"; +/* chat item menu */ +"React..." = "回应……"; + /* No comment provided by engineer. */ "Read" = "已读"; +/* No comment provided by engineer. */ +"Read more" = "阅读更多"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。"; + /* No comment provided by engineer. */ "Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "在我们的 [GitHub 仓库](https://github.com/simplex-chat/simplex-chat#readme) 中阅读更多信息。"; @@ -2073,12 +2299,21 @@ /* No comment provided by engineer. */ "received answer…" = "已收到回复……"; +/* No comment provided by engineer. */ +"Received at" = "已收到于"; + +/* copied message info */ +"Received at: %@" = "已收到于:%@"; + /* No comment provided by engineer. */ "received confirmation…" = "已受到确认……"; /* notification */ "Received file event" = "收到文件项目"; +/* message info title */ +"Received message" = "收到的信息"; + /* No comment provided by engineer. */ "Receiving file will be stopped." = "即将停止接收文件。"; @@ -2088,6 +2323,12 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "对方会在您键入时看到更新。"; +/* No comment provided by engineer. */ +"Record updated at" = "记录更新于"; + +/* copied message info */ +"Record updated at: %@" = "记录更新于:%@"; + /* No comment provided by engineer. */ "Reduced battery usage" = "减少电池使用量"; @@ -2202,6 +2443,9 @@ /* No comment provided by engineer. */ "Save archive" = "保存存档"; +/* No comment provided by engineer. */ +"Save auto-accept settings" = "保存自动接受设置"; + /* No comment provided by engineer. */ "Save group profile" = "保存群组资料"; @@ -2223,6 +2467,9 @@ /* No comment provided by engineer. */ "Save servers?" = "保存服务器?"; +/* No comment provided by engineer. */ +"Save settings?" = "保存设置?"; + /* No comment provided by engineer. */ "Save welcome message?" = "保存欢迎信息?"; @@ -2247,6 +2494,9 @@ /* network option */ "sec" = "秒"; +/* time unit */ +"seconds" = "秒"; + /* No comment provided by engineer. */ "secret" = "秘密"; @@ -2259,6 +2509,21 @@ /* No comment provided by engineer. */ "Security code" = "安全码"; +/* No comment provided by engineer. */ +"Select" = "选择"; + +/* No comment provided by engineer. */ +"Self-destruct" = "自毁"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "自毁密码"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "自毁密码已更改!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "自毁密码已启用!"; + /* No comment provided by engineer. */ "Send" = "发送"; @@ -2268,6 +2533,9 @@ /* No comment provided by engineer. */ "Send direct message" = "发送私信"; +/* No comment provided by engineer. */ +"Send disappearing message" = "发送限时消息中"; + /* No comment provided by engineer. */ "Send link previews" = "发送链接预览"; @@ -2298,9 +2566,18 @@ /* No comment provided by engineer. */ "Sending via" = "发送通过"; +/* No comment provided by engineer. */ +"Sent at" = "已发送于"; + +/* copied message info */ +"Sent at: %@" = "已发送于:%@"; + /* notification */ "Sent file event" = "已发送文件项目"; +/* message info title */ +"Sent message" = "已发信息"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "已发送的消息将在设定的时间后被删除。"; @@ -2328,6 +2605,9 @@ /* No comment provided by engineer. */ "Set it instead of system authentication." = "设置它以代替系统身份验证。"; +/* No comment provided by engineer. */ +"Set passcode" = "设置密码"; + /* No comment provided by engineer. */ "Set passphrase to export" = "设置密码来导出"; @@ -2346,12 +2626,21 @@ /* No comment provided by engineer. */ "Share 1-time link" = "分享一次性链接"; +/* No comment provided by engineer. */ +"Share address" = "分享地址"; + +/* No comment provided by engineer. */ +"Share address with contacts?" = "与联系人分享地址?"; + /* No comment provided by engineer. */ "Share link" = "分享链接"; /* No comment provided by engineer. */ "Share one-time invitation link" = "分享一次性邀请链接"; +/* No comment provided by engineer. */ +"Share with contacts" = "与联系人分享"; + /* No comment provided by engineer. */ "Show calls in phone history" = "在电话历史记录中显示通话"; @@ -2364,6 +2653,12 @@ /* No comment provided by engineer. */ "Show:" = "显示:"; +/* No comment provided by engineer. */ +"SimpleX address" = "SimpleX 地址"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "SimpleX 地址"; + /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "SimpleX Chat 的安全性 由 Trail of Bits 审核。"; @@ -2439,6 +2734,12 @@ /* No comment provided by engineer. */ "Stop sending file?" = "停止发送文件?"; +/* No comment provided by engineer. */ +"Stop sharing" = "停止分享"; + +/* No comment provided by engineer. */ +"Stop sharing address?" = "停止分享地址?"; + /* authentication reason */ "Stop SimpleX" = "停止 SimpleX"; @@ -2592,6 +2893,9 @@ /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "要提出任何问题并接收更新,请:"; +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。"; + /* No comment provided by engineer. */ "To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。"; @@ -2655,6 +2959,9 @@ /* No comment provided by engineer. */ "Unhide profile" = "取消隐藏个人资料"; +/* No comment provided by engineer. */ +"Unit" = "单位"; + /* connection info */ "unknown" = "未知"; @@ -2823,6 +3130,9 @@ /* No comment provided by engineer. */ "WebRTC ICE servers" = "WebRTC ICE 服务器"; +/* time unit */ +"weeks" = "周"; + /* No comment provided by engineer. */ "Welcome %@!" = "欢迎%@!"; @@ -2835,6 +3145,9 @@ /* No comment provided by engineer. */ "When available" = "当可用时"; +/* No comment provided by engineer. */ +"When people request to connect, you can accept or reject it." = "当人们请求连接时,您可以接受或拒绝它。"; + /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。"; @@ -2886,6 +3199,9 @@ /* No comment provided by engineer. */ "You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "您也可以通过点击链接进行连接。如果在浏览器中打开,请点击“在移动应用程序中打开”按钮。"; +/* No comment provided by engineer. */ +"You can create it later" = "您可以以后创建它"; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right.\nSimpleX Lock must be enabled." = "您可以隐藏或静音用户个人资料——只需向右滑动。\n必须启用 SimpleX Lock。"; @@ -2898,6 +3214,9 @@ /* No comment provided by engineer. */ "You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。"; +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "您可以与您的联系人分享该地址,让他们与 **%@** 联系。"; + /* No comment provided by engineer. */ "You can share your address as a link or QR code - anybody can connect to you." = "您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。"; @@ -2992,7 +3311,7 @@ "You will stop receiving messages from this group. Chat history will be preserved." = "您将停止接收来自该群组的消息。聊天记录将被保留。"; /* No comment provided by engineer. */ -"You won't lose your contacts if you later delete your address." = "如果您以后删除它,您不会丢失您的联系人。"; +"You won't lose your contacts if you later delete your address." = "如果您以后删除您的地址,您不会丢失您的联系人。"; /* No comment provided by engineer. */ "you: " = "您: "; @@ -3036,6 +3355,12 @@ /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "您的联系人可以允许完全删除消息。"; +/* No comment provided by engineer. */ +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "您的 SimpleX 的联系人会看到它。\n您可以在设置中更改它。"; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "与您的联系人保持连接。"; + /* No comment provided by engineer. */ "Your current chat database will be DELETED and REPLACED with the imported one." = "您当前的聊天数据库将被删除并替换为导入的数据库。"; diff --git a/apps/simplex-bot-advanced/Main.hs b/apps/simplex-bot-advanced/Main.hs index 1a2c06d617..95814a598d 100644 --- a/apps/simplex-bot-advanced/Main.hs +++ b/apps/simplex-bot-advanced/Main.hs @@ -14,6 +14,7 @@ import Simplex.Chat.Bot import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Messages +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Options import Simplex.Chat.Terminal (terminalChatConfig) import Simplex.Chat.Types diff --git a/apps/simplex-broadcast-bot/Main.hs b/apps/simplex-broadcast-bot/Main.hs index d2cd5edd3d..ae8e4f2782 100644 --- a/apps/simplex-broadcast-bot/Main.hs +++ b/apps/simplex-broadcast-bot/Main.hs @@ -16,6 +16,7 @@ import Simplex.Chat.Bot import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Messages +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Terminal (terminalChatConfig) diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md new file mode 100644 index 0000000000..f7bf411735 --- /dev/null +++ b/docs/GLOSSARY.md @@ -0,0 +1,190 @@ +# Glossary + +Choosing a private messenger requires the understanding of many technical terms, that many users, even quite technical, often misunderstand. This list is aiming to fill this knowledge gap. Please suggest any changes or additions. + +While this glossary aims to be factual and objective, it is not completely unbiased. We designed SimpleX to be the most private, secure and resilient communication network, and some definitions reflect this view. + +## Address portability + +Similarly to [phone number portability](https://en.wikipedia.org/wiki/Local_number_portability) (the ability of the customer to transfer the service to another provider without changing the number), the address portability means the ability of a communication service customer to change the service provider without changing the service address. Many [federated networks](#federated-network) support SRV records to provide address portability, but allowing service users to set up their own domains for the addresses is not as commonly supported by the available server and client software as for email. + +## Anonymous credentials + +The credential that allows proving something, e.g. the right to access some resource, without identifying the user. This credential can either be generated by a trusted party or by the user themselves and provided together with the request to create the resource. The first approach creates some centralized dependency in most cases. The second approach does not require any trust - this is used in SimpleX network to authorize access to the messaging queues. + +[Digital credential on Wikipedia](https://en.wikipedia.org/wiki/Digital_credential) + +## Blockchain + +In a wide sense, blockchain means a sequence of blocks of data, where each block contains a cryptographic hash of the previous block, thus providing integrity to the whole chain. Blockchains are used in many communication and information storage systems to provide integrity and immutability of the data. For example, BluRay disks use blockchain. SimpleX messaging queues also use blockchain - each message includes the hash of the previous message, to ensure the integrity – if any message is modified it will be detected by the recipient when the next message is received. Blockchains are a subset of [Merkle directed acyclic graphs](#merkle-directed-acyclic-graph). + +In a more narrow sense, particularly in media, blockchain is used to refer specifically to distributed ledger, where each record also includes the hash of the previous record, but the blocks have to be agreed by the participating peers using some [consensus protocol](https://en.wikipedia.org/wiki/Consensus_(computer_science)). + +[Wikipedia](https://en.wikipedia.org/wiki/Blockchain) + +## Break-in recovery + +[Post-compromise security](#post-compromise-security). + +## Centralized network + +Centralized networks are provided or controlled by a single entity. The examples are Threema, Signal, WhatsApp and Telegram. The advantage of that design is that the provider can innovate faster, and has a centralized approach to security. But the disadvantage is that the provider can change or discontinue the service, and leak, sell or disclose in some other way all users' data, including who they are connected with. + +## Content padding + +[Message padding](#message-padding). + +## Decentralized network + +Decentralized network is often used to mean "the network based on decentralized blockchain". In its original meaning, decentralized network means that there is no central authority or any other point of centralization in the network, other than network protocols specification. The advantage of decentralized networks is that they are resilient to censorship and to the provider going out of business. The disadvantage is that they are often slower to innovate, and the security may be worse than with the centralized network. + +The examples of decentralized networks are email, web, DNS, XMPP, Matrix, BitTorrent, etc. All these examples have a shared global application-level address space. Cryptocurrency blockchains not only have a shared address space, but also a shared state, so they are more centralized than email. Tor network also has a shared global address space, but also a central authority. SimpleX network does not have a shared application-level address space (it relies on the shared transport-level addresses - SMP relay hostnames or IP addresses), and it does not have any central authority or any shared state. + +## Defense in depth + +Originally, it is a military strategy that seeks to delay rather than prevent the advance of an attacker, buying time and causing additional casualties by yielding space. + +In information security, defense in depth represents the use of multiple computer security techniques to help mitigate the risk of one component of the defense being compromised or circumvented. An example could be anti-virus software installed on individual workstations when there is already virus protection on the firewalls and servers within the same environment. + +SimpleX network applies defense in depth approach to security by having multiple layers for the communication security and privacy: +- double ratchet algorithm for [end-to-end encryption](#end-to-end-encryption) with [perfect forward secrecy](#forward-secrecy) and [post-compromise security](#post-compromise-security), +- additional layer of end-to-end encryption for each messaging queue and another encryption layer of encryption from the server to the recipient inside TLS to prevent correlation by ciphertext, +- TLS with only strong ciphers allowed, +- mitigation of [man-in-the-middle attack](#man-in-the-middle-attack) on client-server connection via server offline certificate verification, +- mitigation of replay attacks via signing over transport channel binding, +- multiple layers of [message padding](#message-padding) to reduce efficiency of traffic analysis, +- mitigation of [man-in-the-middle attack](#man-in-the-middle-attack) on client-client out-of-band channel when sending the invitation, +- rotation of delivery queues to reduce efficiency of traffic analysis, +- etc. + +[Wikipedia](https://en.wikipedia.org/wiki/Defence_in_depth) + +## Double ratchet algorithm + +It is used by two parties to exchange [end-to-end encrypted](#end-to-end-encryption) messages. The parties will use some [key agreement protocol](#key-agreement-protocol) to agree on the initial shared secret key. + +Double Ratchet algorithm provides [perfect forward secrecy](#forward-secrecy) and [post-compromise security](#post-compromise-security). It is [designed by Signal](https://signal.org/docs/specifications/doubleratchet), and used in SimpleX Chat and many other secure messengers. Most experts consider it the state-of-the-art encryption protocol in message encryption. + +## End-to-end encryption + +A communication system where only the communicating parties can read the messages. It is designed to protect message content from any potential eavesdroppers – telecom and Internet providers, malicious actors, and also the provider of the communication service. + +End-to-end encryption requires agreeing cryptographic keys between the sender and the recipient in a way that no eavesdroppers can access the agreed keys. See [key agreement protocol](#key-agreement-protocol). This key exchange can be compromised via [man-in-the-middle attack](#man-in-the-middle-attack), particularly if key exchange happens via the same communication provider and no out-of-band channel is used to verify key exchange. + +[Wikipedia](https://en.wikipedia.org/wiki/End-to-end_encryption) + +## Federated network + +Federated network is provided by several entities that agree upon the standards and operate the network collectively. This allows the users to choose their provider, that will hold their account, their messaging history and contacts, and communicate with other providers' servers on behalf of the user. The examples are email, XMPP, Matrix and Mastodon. + +The advantage of that design is that there is no single organization that all users depend on, and the standards are more difficult to change, unless it benefits all users. There are several disadvantages: 1) the innovation is slower, 2) each user account still depends on a single organization, and in most cases can't move to another provider without changing their network address – there is no [address portability](#address-portability), 3) the security and privacy are inevitably worse than with the centralized networks. + +[Federation on Wikipedia](https://en.wikipedia.org/wiki/Federation_(information_technology)) + +## Forward secrecy + +Also known as perfect forward secrecy, it is a feature of a [key agreement protocol](#key-agreement-protocol) that ensures that session keys will not be compromised even if long-term secrets used in the session key exchange are compromised. Forward secrecy protects past sessions against future compromises of session or long-term keys. + +[Wikipedia](https://en.wikipedia.org/wiki/Forward_secrecy) + +## Key agreement protocol + +Also known as key exchange, it is a process of agreeing cryptographic keys between the sender and the recipient(s) of the message. It is required for [end-to-end encryption](#end-to-end-encryption) to work. + +[Wikipedia](https://en.wikipedia.org/wiki/Key-agreement_protocol) + +## Key exchange + +[Key agreement protocol](#key-agreement-protocol). + +## Man-in-the-middle attack + +The attack when the attacker secretly relays and possibly alters the communications between two parties who believe that they are directly communicating with each other. + +This attack can be used to compromise [end-to-end encryption](#end-to-end-encryption) by intercepting public keys during [key exchange](#key-agreement-protocol), substituting them with the attacker's keys, and then intercepting and re-encrypting all messages, without altering their content. With this attack, while the attacker does not change message content, but she can read the messages, while the communicating parties believe the messages are end-to-end encrypted. + +Such attack is possible with any system that uses the same channel for key exchange as used to send messages - it includes almost all communication systems except SimpleX, where the initial public key is always passed out-of-band. Even with SimpleX, the attacker may intercept and substitute the key sent via another channel, gaining access to communication. This risk is substantially lower, as attacker does not know in advance which channel will be used to pass the key. + +To mitigate such attack the communicating parties must verify the integrity of key exchange - SimpleX and many other messaging apps, e.g. Signal and WhatsApp, have the feature that allows it. + +[Wikipedia](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). + +## Merkle directed acyclic graph + +Also known as Merkle DAG, a data structure based on a general graph structure where node contains the cryptographic hashes of the previous nodes that point to it. Merkle trees are a subset of Merkle DAGs - in this case each leaf contains a cryptographic hash of the parent. + +This structure by design allows to verify the integrity of the whole structure by computing its hashes and comparing with the hashes included in the nodes, in the same way as with [blockchain](#blockchain). + +The motivation to use DAG in distributed environments instead of a simpler linear blockchain is to allow concurrent additions, when there is no requirement for a single order of added items. Merkle DAG is used, for example, in [IPFS](https://en.wikipedia.org/wiki/InterPlanetary_File_System) and will be used in decentralized SimpleX groups. + +[Wikipedia](https://en.wikipedia.org/wiki/Merkle_tree). + +## Message padding + +Also known as content padding, it is the process of adding data to the beginning or the end of a message prior to encryption. Padding conceals the actual message size from any eavesdroppers. SimpleX has several encryption layers, and prior to each encryption the content is padded to a fixed size. + +[Wikipedia](https://en.wikipedia.org/wiki/Padding_(cryptography)). + +## Onion routing + +A technique for anonymous communication over a computer network that uses multiple layers of message encryption, analogous to the layers of an onion. The encrypted data is transmitted through a series of network nodes called "onion routers," each of which "peels" away a single layer, revealing the data's next destination. The sender remains anonymous because each intermediary knows only the location of the immediately preceding and following nodes. + +The most widely used onion network is [Tor](https://en.wikipedia.org/wiki/Tor_(network)). + +Some elements of SimpleX network use similar ideas in their design - different addresses for the same resource used by different parties, and additional encryption layers. Currently though, SimpleX messaging protocol does not protect sender network address, as the relay server is chosen by the recipient. The delivery relays chosen by sender that are planned for the future would make SimpleX design closer to onion routing. + +[Wikipedia](https://en.wikipedia.org/wiki/Onion_routing) + +## Overlay network + +Nodes in the overlay network can be thought of as being connected by virtual or logical links, each of which corresponds to a path, perhaps through many physical links, in the underlying network. Tor, for example, is an overlay network on top of IP network, which in its turn is also an overlay network over some underlying physical network. + +SimpleX Clients also form a network using SMP relays and IP or some other overlay network (e.g., Tor), to communicate with each other. SMP relays, on another hand, do not form a network. + +[Wikipedia](https://en.wikipedia.org/wiki/Overlay_network) + +## Pairwise pseudonymous identifier + +Generalizing [the definition](https://csrc.nist.gov/glossary/term/pairwise_pseudonymous_identifier) from NIST Digital Identity Guidelines, it is an opaque unguessable identifier generated by a service used to access a resource by only one party. + +In the context of SimpleX network, these are the identifiers generated by SMP relays to access anonymous messaging queues, with a separate identifier (and access credential) for each accessing party: recipient, sender and and optional notifications subscriber. The same approach is used by XFTP relays to access file chunks, with separate identifiers (and access credentials) for sender and each recipient. + +## Peer-to-peer + +Peer-to-peer (P2P) is the network architecture when participants have equal rights and communicate directly via a general purpose transport or overlay network. Unlike client-server architecture, all peers in a P2P network both provide and consume the resources. In the context of messaging, P2P architecture usually means that the messages are sent between peers, without user accounts or messages being stored on any servers. Examples are Tox, Briar, Cwtch and many others. + +The advantage is that the participants do not depend on any servers. There are [multiple downsides](./SIMPLEX.md#comparison-with-p2p9-messaging-protocols) to that architecture, such as no asynchronous message delivery, the need for network-wide peer addresses, possibility of network-wide attacks, that are usually mitigated only by using a centralized authority. These disadvantages are avoided with [proxied P2P](#proxied-peer-to-peer) architecture. + +[Wikipedia](https://en.wikipedia.org/wiki/Peer-to-peer). + +## Perfect forward secrecy + +[Forward secrecy](#forward-secrecy). + +## Post-compromise security + +Also known as break-in recovery, it is the quality of the end-to-end encryption scheme allowing to recover security against a passive attacker who observes encrypted messages after compromising one (or both) of the parties. Also known as recovery from compromise or break-in recovery. [Double-ratchet algorithm](#double-ratchet-algorithm) has this quality. + +## Post-quantum cryptography + +Any of the proposed cryptographic systems or algorithms that are thought to be secure against an attack by a quantum computer. It appears that as of 2023 there is no system or algorithm that is proven to be secure against such attacks, or even to be secure against attacks by massively parallel conventional computers, so a general recommendation is to use post-quantum cryptographic systems in combination with the traditional cryptographic systems. + +[Wikipedia](https://en.wikipedia.org/wiki/Post-quantum_cryptography) + +## Privacy + +Someone's right to keep (or the state when they keep) their personal matters and relationships secret (e.g., [Cambridge dictionary](https://dictionary.cambridge.org/dictionary/english/privacy)). Privacy of communication systems should include the privacy of connections and meta-data, not only the privacy of the content of messages. [End-to-end encryption](#end-to-end-encryption) on its own does not provide privacy, as it only protects message content and not connections or meta-data. + +[Wikipedia](https://en.wikipedia.org/wiki/Privacy) + +## Proxied peer-to-peer + +Network topology of the communication system when peers communicate via proxies that do not form the network themselves. Such design is used in Pond, that has a fixed home server for each user, and in SimpleX, that uses multiple relays providing temporary connections. + +## Recovery from compromise + +[Post-compromise security](#post-compromise-security). + +## User identity + +In a communication system it refers to anything that uniquely identifies the users to the network. Depending on the communication network, it can be a phone number, email address, username, public key or a random opaque identifier. Most messaging networks rely on some form of user identity. SimpleX appears to be the only messaging network that does not rely on any kind of user identity - see [this comparison](https://en.wikipedia.org/wiki/Comparison_of_instant_messaging_protocols). diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 9b92f32d94..2266d35589 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -33,6 +33,7 @@ library Simplex.Chat.Help Simplex.Chat.Markdown Simplex.Chat.Messages + Simplex.Chat.Messages.ChatItemContent Simplex.Chat.Migrations.M20220101_initial Simplex.Chat.Migrations.M20220122_v1_1 Simplex.Chat.Migrations.M20220205_chat_item_status diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 54d7bddbfa..4b6b5d259e 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -54,6 +54,7 @@ import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Markdown import Simplex.Chat.Messages +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Options import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index ab1340c818..2c837907eb 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -16,6 +16,7 @@ import qualified Data.Text as T import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Messages +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Store import Simplex.Chat.Types (Contact (..), IsContact (..), User (..)) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 618e059d71..0b7ad9db73 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -42,6 +42,7 @@ import qualified Paths_simplex_chat as SC import Simplex.Chat.Call import Simplex.Chat.Markdown (MarkdownList) import Simplex.Chat.Messages +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Protocol import Simplex.Chat.Store (AutoAccept, StoreError, UserContactLink) import Simplex.Chat.Types diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 1e9ec03a59..cb3dc505e9 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -21,7 +21,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy.Char8 as LB import Data.Int (Int64) -import Data.Maybe (isNothing, isJust) +import Data.Maybe (isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) @@ -29,21 +29,18 @@ import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay) import Data.Time.LocalTime (TimeZone, ZonedTime, utcToZonedTime) import Data.Type.Equality import Data.Typeable (Typeable) -import Data.Word (Word32) -import Database.SQLite.Simple (ResultError (..), SQLData (..)) -import Database.SQLite.Simple.FromField (Field, FromField (..), returnError) -import Database.SQLite.Simple.Internal (Field (..)) -import Database.SQLite.Simple.Ok +import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Generics (Generic) import Simplex.Chat.Markdown +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Protocol import Simplex.Chat.Types -import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgErrorType (..), MsgMeta (..), SwitchPhase (..)) +import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..)) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON, sumTypeJSON) +import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) -import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, tshow, (<$?>)) +import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection deriving (Eq, Show, Ord, Generic) @@ -212,6 +209,10 @@ chatItemMember GroupInfo {membership} ChatItem {chatDir} = case chatDir of CIGroupSnd -> membership CIGroupRcv m -> m +ciReactionAllowed :: ChatItem c d -> Bool +ciReactionAllowed ChatItem {meta = CIMeta {itemDeleted = Just _}} = False +ciReactionAllowed ChatItem {content} = isJust $ ciMsgContent content + data CIDeletedState = CIDeletedState { markedDeleted :: Bool, deletedByMember :: Maybe GroupMember @@ -633,11 +634,6 @@ data CIStatus (d :: MsgDirection) where deriving instance Show (CIStatus d) -ciStatusNew :: forall d. MsgDirectionI d => CIStatus d -ciStatusNew = case msgDirection @d of - SMDSnd -> CISSndNew - SMDRcv -> CISRcvNew - instance ToJSON (CIStatus d) where toJSON = J.toJSON . jsonCIStatus toEncoding = J.toEncoding . jsonCIStatus @@ -694,6 +690,16 @@ jsonCIStatus = \case CISRcvNew -> JCISRcvNew CISRcvRead -> JCISRcvRead +ciStatusNew :: forall d. MsgDirectionI d => CIStatus d +ciStatusNew = case msgDirection @d of + SMDSnd -> CISSndNew + SMDRcv -> CISRcvNew + +ciCreateStatus :: forall d. MsgDirectionI d => CIContent d -> CIStatus d +ciCreateStatus content = case msgDirection @d of + SMDSnd -> ciStatusNew + SMDRcv -> if ciRequiresAttention content then ciStatusNew else CISRcvRead + type ChatItemId = Int64 type ChatItemTs = UTCTime @@ -704,573 +710,6 @@ data ChatPagination | CPBefore ChatItemId Int deriving (Show) -data CIDeleteMode = CIDMBroadcast | CIDMInternal - deriving (Show, Generic) - -instance ToJSON CIDeleteMode where - toJSON = J.genericToJSON . enumJSON $ dropPrefix "CIDM" - toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CIDM" - -instance FromJSON CIDeleteMode where - parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CIDM" - -ciDeleteModeToText :: CIDeleteMode -> Text -ciDeleteModeToText = \case - CIDMBroadcast -> "this item is deleted (broadcast)" - CIDMInternal -> "this item is deleted (internal)" - -ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text -ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role = - "invitation to join group " <> displayName <> optionalFullName displayName fullName <> " as " <> (decodeLatin1 . strEncode $ role) - -rcvGroupEventToText :: RcvGroupEvent -> Text -rcvGroupEventToText = \case - RGEMemberAdded _ p -> "added " <> profileToText p - RGEMemberConnected -> "connected" - RGEMemberLeft -> "left" - RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) - RGEUserRole r -> "changed your role to " <> safeDecodeUtf8 (strEncode r) - RGEMemberDeleted _ p -> "removed " <> profileToText p - RGEUserDeleted -> "removed you" - RGEGroupDeleted -> "deleted group" - RGEGroupUpdated _ -> "group profile updated" - RGEInvitedViaGroupLink -> "invited via your group link" - -sndGroupEventToText :: SndGroupEvent -> Text -sndGroupEventToText = \case - SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) - SGEUserRole r -> "changed role for yourself to " <> safeDecodeUtf8 (strEncode r) - SGEMemberDeleted _ p -> "removed " <> profileToText p - SGEUserLeft -> "left" - SGEGroupUpdated _ -> "group profile updated" - -rcvConnEventToText :: RcvConnEvent -> Text -rcvConnEventToText = \case - RCESwitchQueue phase -> case phase of - SPCompleted -> "changed address for you" - _ -> decodeLatin1 (strEncode phase) <> " changing address for you..." - -sndConnEventToText :: SndConnEvent -> Text -sndConnEventToText = \case - SCESwitchQueue phase m -> case phase of - SPCompleted -> "you changed address" <> forMember m - _ -> decodeLatin1 (strEncode phase) <> " changing address" <> forMember m <> "..." - where - forMember member_ = - maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_ - -profileToText :: Profile -> Text -profileToText Profile {displayName, fullName} = displayName <> optionalFullName displayName fullName - --- This type is used both in API and in DB, so we use different JSON encodings for the database and for the API --- ! Nested sum types also have to use different encodings for database and API --- ! to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent -data CIContent (d :: MsgDirection) where - CISndMsgContent :: MsgContent -> CIContent 'MDSnd - CIRcvMsgContent :: MsgContent -> CIContent 'MDRcv - CISndDeleted :: CIDeleteMode -> CIContent 'MDSnd -- legacy - since v4.3.0 item_deleted field is used - CIRcvDeleted :: CIDeleteMode -> CIContent 'MDRcv -- legacy - since v4.3.0 item_deleted field is used - CISndCall :: CICallStatus -> Int -> CIContent 'MDSnd - CIRcvCall :: CICallStatus -> Int -> CIContent 'MDRcv - CIRcvIntegrityError :: MsgErrorType -> CIContent 'MDRcv - CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv - CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv - CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd - CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv - CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd - CIRcvConnEvent :: RcvConnEvent -> CIContent 'MDRcv - CISndConnEvent :: SndConnEvent -> CIContent 'MDSnd - CIRcvChatFeature :: ChatFeature -> PrefEnabled -> Maybe Int -> CIContent 'MDRcv - CISndChatFeature :: ChatFeature -> PrefEnabled -> Maybe Int -> CIContent 'MDSnd - CIRcvChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDRcv - CISndChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDSnd - CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDRcv - CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDSnd - CIRcvChatFeatureRejected :: ChatFeature -> CIContent 'MDRcv - CIRcvGroupFeatureRejected :: GroupFeature -> CIContent 'MDRcv - CISndModerated :: CIContent 'MDSnd - CIRcvModerated :: CIContent 'MDRcv - CIInvalidJSON :: Text -> CIContent d --- ^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API --- ! ^ Nested sum types also have to use different encodings for database and API --- ! ^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent - -deriving instance Show (CIContent d) - -ciMsgContent :: CIContent d -> Maybe MsgContent -ciMsgContent = \case - CISndMsgContent mc -> Just mc - CIRcvMsgContent mc -> Just mc - _ -> Nothing - -data MsgDecryptError = MDERatchetHeader | MDETooManySkipped - deriving (Eq, Show, Generic) - -instance ToJSON MsgDecryptError where - toJSON = J.genericToJSON . enumJSON $ dropPrefix "MDE" - toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "MDE" - -instance FromJSON MsgDecryptError where - parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "MDE" - -ciReactionAllowed :: ChatItem c d -> Bool -ciReactionAllowed ChatItem {meta = CIMeta {itemDeleted = Just _}} = False -ciReactionAllowed ChatItem {content} = isJust $ ciMsgContent content - -ciRequiresAttention :: forall d. MsgDirectionI d => CIContent d -> Bool -ciRequiresAttention content = case msgDirection @d of - SMDSnd -> True - SMDRcv -> case content of - CIRcvMsgContent _ -> True - CIRcvDeleted _ -> True - CIRcvCall {} -> True - CIRcvIntegrityError _ -> True - CIRcvDecryptionError {} -> True - CIRcvGroupInvitation {} -> True - CIRcvGroupEvent rge -> case rge of - RGEMemberAdded {} -> False - RGEMemberConnected -> False - RGEMemberLeft -> False - RGEMemberRole {} -> False - RGEUserRole _ -> True - RGEMemberDeleted {} -> False - RGEUserDeleted -> True - RGEGroupDeleted -> True - RGEGroupUpdated _ -> False - RGEInvitedViaGroupLink -> False - CIRcvConnEvent _ -> True - CIRcvChatFeature {} -> False - CIRcvChatPreference {} -> False - CIRcvGroupFeature {} -> False - CIRcvChatFeatureRejected _ -> True - CIRcvGroupFeatureRejected _ -> True - CIRcvModerated -> True - CIInvalidJSON _ -> False - -ciCreateStatus :: forall d. MsgDirectionI d => CIContent d -> CIStatus d -ciCreateStatus content = case msgDirection @d of - SMDSnd -> ciStatusNew - SMDRcv -> if ciRequiresAttention content then ciStatusNew else CISRcvRead - -data RcvGroupEvent - = RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting - | RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember - | RGEMemberLeft -- CRLeftMember - | RGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} - | RGEUserRole {role :: GroupMemberRole} - | RGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRDeletedMember - | RGEUserDeleted -- CRDeletedMemberUser - | RGEGroupDeleted -- CRGroupDeleted - | RGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated - -- RGEInvitedViaGroupLink chat items are not received - they're created when sending group invitations, - -- but being RcvGroupEvent allows them to be assigned to the respective member (and so enable "send direct message") - -- and be created as unread without adding / working around new status for sent items - | RGEInvitedViaGroupLink -- CRSentGroupInvitationViaLink - deriving (Show, Generic) - -instance FromJSON RcvGroupEvent where - parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RGE" - -instance ToJSON RcvGroupEvent where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RGE" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RGE" - -newtype DBRcvGroupEvent = RGE RcvGroupEvent - -instance FromJSON DBRcvGroupEvent where - parseJSON v = RGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RGE") v - -instance ToJSON DBRcvGroupEvent where - toJSON (RGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RGE") v - toEncoding (RGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RGE") v - -data SndGroupEvent - = SGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} - | SGEUserRole {role :: GroupMemberRole} - | SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMember - | SGEUserLeft -- CRLeftMemberUser - | SGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated - deriving (Show, Generic) - -instance FromJSON SndGroupEvent where - parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SGE" - -instance ToJSON SndGroupEvent where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SGE" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SGE" - -newtype DBSndGroupEvent = SGE SndGroupEvent - -instance FromJSON DBSndGroupEvent where - parseJSON v = SGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "SGE") v - -instance ToJSON DBSndGroupEvent where - toJSON (SGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SGE") v - toEncoding (SGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SGE") v - -data RcvConnEvent = RCESwitchQueue {phase :: SwitchPhase} - deriving (Show, Generic) - -data SndConnEvent = SCESwitchQueue {phase :: SwitchPhase, member :: Maybe GroupMemberRef} - deriving (Show, Generic) - -instance FromJSON RcvConnEvent where - parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RCE" - -instance ToJSON RcvConnEvent where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RCE" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RCE" - -newtype DBRcvConnEvent = RCE RcvConnEvent - -instance FromJSON DBRcvConnEvent where - parseJSON v = RCE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RCE") v - -instance ToJSON DBRcvConnEvent where - toJSON (RCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RCE") v - toEncoding (RCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RCE") v - -instance FromJSON SndConnEvent where - parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SCE" - -instance ToJSON SndConnEvent where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SCE" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SCE" - -newtype DBSndConnEvent = SCE SndConnEvent - -instance FromJSON DBSndConnEvent where - parseJSON v = SCE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "SCE") v - -instance ToJSON DBSndConnEvent where - toJSON (SCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SCE") v - toEncoding (SCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SCE") v - -newtype DBMsgErrorType = DBME MsgErrorType - -instance FromJSON DBMsgErrorType where - parseJSON v = DBME <$> J.genericParseJSON (singleFieldJSON fstToLower) v - -instance ToJSON DBMsgErrorType where - toJSON (DBME v) = J.genericToJSON (singleFieldJSON fstToLower) v - toEncoding (DBME v) = J.genericToEncoding (singleFieldJSON fstToLower) v - -data CIGroupInvitation = CIGroupInvitation - { groupId :: GroupId, - groupMemberId :: GroupMemberId, - localDisplayName :: GroupName, - groupProfile :: GroupProfile, - status :: CIGroupInvitationStatus - } - deriving (Eq, Show, Generic, FromJSON) - -instance ToJSON CIGroupInvitation where - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} - -data CIGroupInvitationStatus - = CIGISPending - | CIGISAccepted - | CIGISRejected - | CIGISExpired - deriving (Eq, Show, Generic) - -instance FromJSON CIGroupInvitationStatus where - parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CIGIS" - -instance ToJSON CIGroupInvitationStatus where - toJSON = J.genericToJSON . enumJSON $ dropPrefix "CIGIS" - toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CIGIS" - -ciContentToText :: CIContent d -> Text -ciContentToText = \case - CISndMsgContent mc -> msgContentText mc - CIRcvMsgContent mc -> msgContentText mc - CISndDeleted cidm -> ciDeleteModeToText cidm - CIRcvDeleted cidm -> ciDeleteModeToText cidm - CISndCall status duration -> "outgoing call: " <> ciCallInfoText status duration - CIRcvCall status duration -> "incoming call: " <> ciCallInfoText status duration - CIRcvIntegrityError err -> msgIntegrityError err - CIRcvDecryptionError err n -> msgDecryptErrorText err n - CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole - CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole - CIRcvGroupEvent event -> rcvGroupEventToText event - CISndGroupEvent event -> sndGroupEventToText event - CIRcvConnEvent event -> rcvConnEventToText event - CISndConnEvent event -> sndConnEventToText event - CIRcvChatFeature feature enabled param -> featureStateText feature enabled param - CISndChatFeature feature enabled param -> featureStateText feature enabled param - CIRcvChatPreference feature allowed param -> prefStateText feature allowed param - CISndChatPreference feature allowed param -> "you " <> prefStateText feature allowed param - CIRcvGroupFeature feature pref param -> groupPrefStateText feature pref param - CISndGroupFeature feature pref param -> groupPrefStateText feature pref param - CIRcvChatFeatureRejected feature -> chatFeatureNameText feature <> ": received, prohibited" - CIRcvGroupFeatureRejected feature -> groupFeatureNameText feature <> ": received, prohibited" - CISndModerated -> ciModeratedText - CIRcvModerated -> ciModeratedText - CIInvalidJSON _ -> "invalid content JSON" - -msgIntegrityError :: MsgErrorType -> Text -msgIntegrityError = \case - MsgSkipped fromId toId -> - "skipped message ID " <> tshow fromId - <> if fromId == toId then "" else ".." <> tshow toId - MsgBadId msgId -> "unexpected message ID " <> tshow msgId - MsgBadHash -> "incorrect message hash" - MsgDuplicate -> "duplicate message ID" - -msgDecryptErrorText :: MsgDecryptError -> Word32 -> Text -msgDecryptErrorText err n = - "decryption error, possibly due to the device change (" <> errName <> if n == 1 then ")" else ", " <> tshow n <> " messages)" - where - errName = case err of - MDERatchetHeader -> "header" - MDETooManySkipped -> "too many skipped messages" - -msgDirToModeratedContent_ :: SMsgDirection d -> CIContent d -msgDirToModeratedContent_ = \case - SMDRcv -> CIRcvModerated - SMDSnd -> CISndModerated - -ciModeratedText :: Text -ciModeratedText = "moderated" - --- platform independent -instance MsgDirectionI d => ToField (CIContent d) where - toField = toField . encodeJSON . dbJsonCIContent - --- platform specific -instance MsgDirectionI d => ToJSON (CIContent d) where - toJSON = J.toJSON . jsonCIContent - toEncoding = J.toEncoding . jsonCIContent - -data ACIContent = forall d. MsgDirectionI d => ACIContent (SMsgDirection d) (CIContent d) - -deriving instance Show ACIContent - --- platform independent -dbParseACIContent :: Text -> Either String ACIContent -dbParseACIContent = fmap aciContentDBJSON . J.eitherDecodeStrict' . encodeUtf8 - --- platform specific -instance FromJSON ACIContent where - parseJSON = fmap aciContentJSON . J.parseJSON - --- platform specific -data JSONCIContent - = JCISndMsgContent {msgContent :: MsgContent} - | JCIRcvMsgContent {msgContent :: MsgContent} - | JCISndDeleted {deleteMode :: CIDeleteMode} - | JCIRcvDeleted {deleteMode :: CIDeleteMode} - | JCISndCall {status :: CICallStatus, duration :: Int} -- duration in seconds - | JCIRcvCall {status :: CICallStatus, duration :: Int} - | JCIRcvIntegrityError {msgError :: MsgErrorType} - | JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} - | JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} - | JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} - | JCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent} - | JCISndGroupEvent {sndGroupEvent :: SndGroupEvent} - | JCIRcvConnEvent {rcvConnEvent :: RcvConnEvent} - | JCISndConnEvent {sndConnEvent :: SndConnEvent} - | JCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} - | JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} - | JCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | JCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | JCIRcvChatFeatureRejected {feature :: ChatFeature} - | JCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} - | JCISndModerated - | JCIRcvModerated - | JCIInvalidJSON {direction :: MsgDirection, json :: Text} - deriving (Generic) - -instance FromJSON JSONCIContent where - parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCI" - -instance ToJSON JSONCIContent where - toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCI" - toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCI" - -jsonCIContent :: forall d. MsgDirectionI d => CIContent d -> JSONCIContent -jsonCIContent = \case - CISndMsgContent mc -> JCISndMsgContent mc - CIRcvMsgContent mc -> JCIRcvMsgContent mc - CISndDeleted cidm -> JCISndDeleted cidm - CIRcvDeleted cidm -> JCIRcvDeleted cidm - CISndCall status duration -> JCISndCall {status, duration} - CIRcvCall status duration -> JCIRcvCall {status, duration} - CIRcvIntegrityError err -> JCIRcvIntegrityError err - CIRcvDecryptionError err n -> JCIRcvDecryptionError err n - CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole} - CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole} - CIRcvGroupEvent rcvGroupEvent -> JCIRcvGroupEvent {rcvGroupEvent} - CISndGroupEvent sndGroupEvent -> JCISndGroupEvent {sndGroupEvent} - CIRcvConnEvent rcvConnEvent -> JCIRcvConnEvent {rcvConnEvent} - CISndConnEvent sndConnEvent -> JCISndConnEvent {sndConnEvent} - CIRcvChatFeature feature enabled param -> JCIRcvChatFeature {feature, enabled, param} - CISndChatFeature feature enabled param -> JCISndChatFeature {feature, enabled, param} - CIRcvChatPreference feature allowed param -> JCIRcvChatPreference {feature, allowed, param} - CISndChatPreference feature allowed param -> JCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> JCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> JCISndGroupFeature {groupFeature, preference, param} - CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} - CIRcvGroupFeatureRejected groupFeature -> JCIRcvGroupFeatureRejected {groupFeature} - CISndModerated -> JCISndModerated - CIRcvModerated -> JCISndModerated - CIInvalidJSON json -> JCIInvalidJSON (toMsgDirection $ msgDirection @d) json - -aciContentJSON :: JSONCIContent -> ACIContent -aciContentJSON = \case - JCISndMsgContent mc -> ACIContent SMDSnd $ CISndMsgContent mc - JCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc - JCISndDeleted cidm -> ACIContent SMDSnd $ CISndDeleted cidm - JCIRcvDeleted cidm -> ACIContent SMDRcv $ CIRcvDeleted cidm - JCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration - JCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration - JCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err - JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n - JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole - JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole - JCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent - JCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent - JCIRcvConnEvent {rcvConnEvent} -> ACIContent SMDRcv $ CIRcvConnEvent rcvConnEvent - JCISndConnEvent {sndConnEvent} -> ACIContent SMDSnd $ CISndConnEvent sndConnEvent - JCIRcvChatFeature {feature, enabled, param} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled param - JCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param - JCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param - JCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - JCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - JCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param - JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature - JCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature - JCISndModerated -> ACIContent SMDSnd CISndModerated - JCIRcvModerated -> ACIContent SMDRcv CIRcvModerated - JCIInvalidJSON dir json -> case fromMsgDirection dir of - AMsgDirection d -> ACIContent d $ CIInvalidJSON json - --- platform independent -data DBJSONCIContent - = DBJCISndMsgContent {msgContent :: MsgContent} - | DBJCIRcvMsgContent {msgContent :: MsgContent} - | DBJCISndDeleted {deleteMode :: CIDeleteMode} - | DBJCIRcvDeleted {deleteMode :: CIDeleteMode} - | DBJCISndCall {status :: CICallStatus, duration :: Int} - | DBJCIRcvCall {status :: CICallStatus, duration :: Int} - | DBJCIRcvIntegrityError {msgError :: DBMsgErrorType} - | DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} - | DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} - | DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} - | DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent} - | DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent} - | DBJCIRcvConnEvent {rcvConnEvent :: DBRcvConnEvent} - | DBJCISndConnEvent {sndConnEvent :: DBSndConnEvent} - | DBJCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} - | DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} - | DBJCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | DBJCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} - | DBJCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} - | DBJCISndModerated - | DBJCIRcvModerated - | DBJCIInvalidJSON {direction :: MsgDirection, json :: Text} - deriving (Generic) - -instance FromJSON DBJSONCIContent where - parseJSON = J.genericParseJSON . singleFieldJSON $ dropPrefix "DBJCI" - -instance ToJSON DBJSONCIContent where - toJSON = J.genericToJSON . singleFieldJSON $ dropPrefix "DBJCI" - toEncoding = J.genericToEncoding . singleFieldJSON $ dropPrefix "DBJCI" - -dbJsonCIContent :: forall d. MsgDirectionI d => CIContent d -> DBJSONCIContent -dbJsonCIContent = \case - CISndMsgContent mc -> DBJCISndMsgContent mc - CIRcvMsgContent mc -> DBJCIRcvMsgContent mc - CISndDeleted cidm -> DBJCISndDeleted cidm - CIRcvDeleted cidm -> DBJCIRcvDeleted cidm - CISndCall status duration -> DBJCISndCall {status, duration} - CIRcvCall status duration -> DBJCIRcvCall {status, duration} - CIRcvIntegrityError err -> DBJCIRcvIntegrityError $ DBME err - CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n - CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole} - CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole} - CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge - CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge - CIRcvConnEvent rce -> DBJCIRcvConnEvent $ RCE rce - CISndConnEvent sce -> DBJCISndConnEvent $ SCE sce - CIRcvChatFeature feature enabled param -> DBJCIRcvChatFeature {feature, enabled, param} - CISndChatFeature feature enabled param -> DBJCISndChatFeature {feature, enabled, param} - CIRcvChatPreference feature allowed param -> DBJCIRcvChatPreference {feature, allowed, param} - CISndChatPreference feature allowed param -> DBJCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> DBJCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> DBJCISndGroupFeature {groupFeature, preference, param} - CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} - CIRcvGroupFeatureRejected groupFeature -> DBJCIRcvGroupFeatureRejected {groupFeature} - CISndModerated -> DBJCISndModerated - CIRcvModerated -> DBJCIRcvModerated - CIInvalidJSON json -> DBJCIInvalidJSON (toMsgDirection $ msgDirection @d) json - -aciContentDBJSON :: DBJSONCIContent -> ACIContent -aciContentDBJSON = \case - DBJCISndMsgContent mc -> ACIContent SMDSnd $ CISndMsgContent mc - DBJCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc - DBJCISndDeleted cidm -> ACIContent SMDSnd $ CISndDeleted cidm - DBJCIRcvDeleted cidm -> ACIContent SMDRcv $ CIRcvDeleted cidm - DBJCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration - DBJCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration - DBJCIRcvIntegrityError (DBME err) -> ACIContent SMDRcv $ CIRcvIntegrityError err - DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n - DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole - DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole - DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge - DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge - DBJCIRcvConnEvent (RCE rce) -> ACIContent SMDRcv $ CIRcvConnEvent rce - DBJCISndConnEvent (SCE sce) -> ACIContent SMDSnd $ CISndConnEvent sce - DBJCIRcvChatFeature {feature, enabled, param} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled param - DBJCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param - DBJCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param - DBJCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - DBJCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - DBJCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param - DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature - DBJCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature - DBJCISndModerated -> ACIContent SMDSnd CISndModerated - DBJCIRcvModerated -> ACIContent SMDRcv CIRcvModerated - DBJCIInvalidJSON dir json -> case fromMsgDirection dir of - AMsgDirection d -> ACIContent d $ CIInvalidJSON json - -data CICallStatus - = CISCallPending - | CISCallMissed - | CISCallRejected -- only possible for received calls, not on type level - | CISCallAccepted - | CISCallNegotiated - | CISCallProgress - | CISCallEnded - | CISCallError - deriving (Show, Generic) - -instance FromJSON CICallStatus where - parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CISCall" - -instance ToJSON CICallStatus where - toJSON = J.genericToJSON . enumJSON $ dropPrefix "CISCall" - toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CISCall" - -ciCallInfoText :: CICallStatus -> Int -> Text -ciCallInfoText status duration = case status of - CISCallPending -> "calling..." - CISCallMissed -> "missed" - CISCallRejected -> "rejected" - CISCallAccepted -> "accepted" - CISCallNegotiated -> "connecting..." - CISCallProgress -> "in progress " <> durationText duration - CISCallEnded -> "ended " <> durationText duration - CISCallError -> "error" - data SChatType (c :: ChatType) where SCTDirect :: SChatType 'CTDirect SCTGroup :: SChatType 'CTGroup @@ -1323,73 +762,6 @@ type MessageId = Int64 data ConnOrGroupId = ConnectionId Int64 | GroupId Int64 -data MsgDirection = MDRcv | MDSnd - deriving (Eq, Show, Generic) - -instance FromJSON MsgDirection where - parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "MD" - -instance ToJSON MsgDirection where - toJSON = J.genericToJSON . enumJSON $ dropPrefix "MD" - toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "MD" - -instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgDirection . msgDirectionIntP - -instance ToField MsgDirection where toField = toField . msgDirectionInt - -fromIntField_ :: (Typeable a) => (Int64 -> Maybe a) -> Field -> Ok a -fromIntField_ fromInt = \case - f@(Field (SQLInteger i) _) -> - case fromInt i of - Just x -> Ok x - _ -> returnError ConversionFailed f ("invalid integer: " <> show i) - f -> returnError ConversionFailed f "expecting SQLInteger column type" - -data SMsgDirection (d :: MsgDirection) where - SMDRcv :: SMsgDirection 'MDRcv - SMDSnd :: SMsgDirection 'MDSnd - -deriving instance Show (SMsgDirection d) - -instance TestEquality SMsgDirection where - testEquality SMDRcv SMDRcv = Just Refl - testEquality SMDSnd SMDSnd = Just Refl - testEquality _ _ = Nothing - -instance ToField (SMsgDirection d) where toField = toField . msgDirectionInt . toMsgDirection - -data AMsgDirection = forall d. MsgDirectionI d => AMsgDirection (SMsgDirection d) - -deriving instance Show AMsgDirection - -toMsgDirection :: SMsgDirection d -> MsgDirection -toMsgDirection = \case - SMDRcv -> MDRcv - SMDSnd -> MDSnd - -fromMsgDirection :: MsgDirection -> AMsgDirection -fromMsgDirection = \case - MDRcv -> AMsgDirection SMDRcv - MDSnd -> AMsgDirection SMDSnd - -class MsgDirectionI (d :: MsgDirection) where - msgDirection :: SMsgDirection d - -instance MsgDirectionI 'MDRcv where msgDirection = SMDRcv - -instance MsgDirectionI 'MDSnd where msgDirection = SMDSnd - -msgDirectionInt :: MsgDirection -> Int -msgDirectionInt = \case - MDRcv -> 0 - MDSnd -> 1 - -msgDirectionIntP :: Int64 -> Maybe MsgDirection -msgDirectionIntP = \case - 0 -> Just MDRcv - 1 -> Just MDSnd - _ -> Nothing - data SndMsgDelivery = SndMsgDelivery { connId :: Int64, agentMsgId :: AgentMsgId diff --git a/src/Simplex/Chat/Messages/ChatItemContent.hs b/src/Simplex/Chat/Messages/ChatItemContent.hs new file mode 100644 index 0000000000..f69bd59b55 --- /dev/null +++ b/src/Simplex/Chat/Messages/ChatItemContent.hs @@ -0,0 +1,660 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} + +module Simplex.Chat.Messages.ChatItemContent where + +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Aeson as J +import Data.Int (Int64) +import Data.Text (Text) +import Data.Text.Encoding (decodeLatin1, encodeUtf8) +import Data.Type.Equality +import Data.Typeable (Typeable) +import Data.Word (Word32) +import Database.SQLite.Simple (ResultError (..), SQLData (..)) +import Database.SQLite.Simple.FromField (Field, FromField (..), returnError) +import Database.SQLite.Simple.Internal (Field (..)) +import Database.SQLite.Simple.Ok +import Database.SQLite.Simple.ToField (ToField (..)) +import GHC.Generics (Generic) +import Simplex.Chat.Protocol +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), SwitchPhase (..)) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fstToLower, singleFieldJSON, sumTypeJSON) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow) + +data MsgDirection = MDRcv | MDSnd + deriving (Eq, Show, Generic) + +instance FromJSON MsgDirection where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "MD" + +instance ToJSON MsgDirection where + toJSON = J.genericToJSON . enumJSON $ dropPrefix "MD" + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "MD" + +instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgDirection . msgDirectionIntP + +instance ToField MsgDirection where toField = toField . msgDirectionInt + +fromIntField_ :: (Typeable a) => (Int64 -> Maybe a) -> Field -> Ok a +fromIntField_ fromInt = \case + f@(Field (SQLInteger i) _) -> + case fromInt i of + Just x -> Ok x + _ -> returnError ConversionFailed f ("invalid integer: " <> show i) + f -> returnError ConversionFailed f "expecting SQLInteger column type" + +data SMsgDirection (d :: MsgDirection) where + SMDRcv :: SMsgDirection 'MDRcv + SMDSnd :: SMsgDirection 'MDSnd + +deriving instance Show (SMsgDirection d) + +instance TestEquality SMsgDirection where + testEquality SMDRcv SMDRcv = Just Refl + testEquality SMDSnd SMDSnd = Just Refl + testEquality _ _ = Nothing + +instance ToField (SMsgDirection d) where toField = toField . msgDirectionInt . toMsgDirection + +data AMsgDirection = forall d. MsgDirectionI d => AMsgDirection (SMsgDirection d) + +deriving instance Show AMsgDirection + +toMsgDirection :: SMsgDirection d -> MsgDirection +toMsgDirection = \case + SMDRcv -> MDRcv + SMDSnd -> MDSnd + +fromMsgDirection :: MsgDirection -> AMsgDirection +fromMsgDirection = \case + MDRcv -> AMsgDirection SMDRcv + MDSnd -> AMsgDirection SMDSnd + +class MsgDirectionI (d :: MsgDirection) where + msgDirection :: SMsgDirection d + +instance MsgDirectionI 'MDRcv where msgDirection = SMDRcv + +instance MsgDirectionI 'MDSnd where msgDirection = SMDSnd + +msgDirectionInt :: MsgDirection -> Int +msgDirectionInt = \case + MDRcv -> 0 + MDSnd -> 1 + +msgDirectionIntP :: Int64 -> Maybe MsgDirection +msgDirectionIntP = \case + 0 -> Just MDRcv + 1 -> Just MDSnd + _ -> Nothing + +data CIDeleteMode = CIDMBroadcast | CIDMInternal + deriving (Show, Generic) + +instance ToJSON CIDeleteMode where + toJSON = J.genericToJSON . enumJSON $ dropPrefix "CIDM" + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CIDM" + +instance FromJSON CIDeleteMode where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CIDM" + +ciDeleteModeToText :: CIDeleteMode -> Text +ciDeleteModeToText = \case + CIDMBroadcast -> "this item is deleted (broadcast)" + CIDMInternal -> "this item is deleted (internal)" + +-- This type is used both in API and in DB, so we use different JSON encodings for the database and for the API +-- ! Nested sum types also have to use different encodings for database and API +-- ! to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent +data CIContent (d :: MsgDirection) where + CISndMsgContent :: MsgContent -> CIContent 'MDSnd + CIRcvMsgContent :: MsgContent -> CIContent 'MDRcv + CISndDeleted :: CIDeleteMode -> CIContent 'MDSnd -- legacy - since v4.3.0 item_deleted field is used + CIRcvDeleted :: CIDeleteMode -> CIContent 'MDRcv -- legacy - since v4.3.0 item_deleted field is used + CISndCall :: CICallStatus -> Int -> CIContent 'MDSnd + CIRcvCall :: CICallStatus -> Int -> CIContent 'MDRcv + CIRcvIntegrityError :: MsgErrorType -> CIContent 'MDRcv + CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv + CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv + CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd + CIRcvGroupEvent :: RcvGroupEvent -> CIContent 'MDRcv + CISndGroupEvent :: SndGroupEvent -> CIContent 'MDSnd + CIRcvConnEvent :: RcvConnEvent -> CIContent 'MDRcv + CISndConnEvent :: SndConnEvent -> CIContent 'MDSnd + CIRcvChatFeature :: ChatFeature -> PrefEnabled -> Maybe Int -> CIContent 'MDRcv + CISndChatFeature :: ChatFeature -> PrefEnabled -> Maybe Int -> CIContent 'MDSnd + CIRcvChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDRcv + CISndChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDSnd + CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDRcv + CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDSnd + CIRcvChatFeatureRejected :: ChatFeature -> CIContent 'MDRcv + CIRcvGroupFeatureRejected :: GroupFeature -> CIContent 'MDRcv + CISndModerated :: CIContent 'MDSnd + CIRcvModerated :: CIContent 'MDRcv + CIInvalidJSON :: Text -> CIContent d +-- ^ This type is used both in API and in DB, so we use different JSON encodings for the database and for the API +-- ! ^ Nested sum types also have to use different encodings for database and API +-- ! ^ to avoid breaking cross-platform compatibility, see RcvGroupEvent and SndGroupEvent + +deriving instance Show (CIContent d) + +ciMsgContent :: CIContent d -> Maybe MsgContent +ciMsgContent = \case + CISndMsgContent mc -> Just mc + CIRcvMsgContent mc -> Just mc + _ -> Nothing + +data MsgDecryptError = MDERatchetHeader | MDETooManySkipped + deriving (Eq, Show, Generic) + +instance ToJSON MsgDecryptError where + toJSON = J.genericToJSON . enumJSON $ dropPrefix "MDE" + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "MDE" + +instance FromJSON MsgDecryptError where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "MDE" + +ciRequiresAttention :: forall d. MsgDirectionI d => CIContent d -> Bool +ciRequiresAttention content = case msgDirection @d of + SMDSnd -> True + SMDRcv -> case content of + CIRcvMsgContent _ -> True + CIRcvDeleted _ -> True + CIRcvCall {} -> True + CIRcvIntegrityError _ -> True + CIRcvDecryptionError {} -> True + CIRcvGroupInvitation {} -> True + CIRcvGroupEvent rge -> case rge of + RGEMemberAdded {} -> False + RGEMemberConnected -> False + RGEMemberLeft -> False + RGEMemberRole {} -> False + RGEUserRole _ -> True + RGEMemberDeleted {} -> False + RGEUserDeleted -> True + RGEGroupDeleted -> True + RGEGroupUpdated _ -> False + RGEInvitedViaGroupLink -> False + CIRcvConnEvent _ -> True + CIRcvChatFeature {} -> False + CIRcvChatPreference {} -> False + CIRcvGroupFeature {} -> False + CIRcvChatFeatureRejected _ -> True + CIRcvGroupFeatureRejected _ -> True + CIRcvModerated -> True + CIInvalidJSON _ -> False + +data RcvGroupEvent + = RGEMemberAdded {groupMemberId :: GroupMemberId, profile :: Profile} -- CRJoinedGroupMemberConnecting + | RGEMemberConnected -- CRUserJoinedGroup, CRJoinedGroupMember, CRConnectedToGroupMember + | RGEMemberLeft -- CRLeftMember + | RGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} + | RGEUserRole {role :: GroupMemberRole} + | RGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRDeletedMember + | RGEUserDeleted -- CRDeletedMemberUser + | RGEGroupDeleted -- CRGroupDeleted + | RGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated + -- RGEInvitedViaGroupLink chat items are not received - they're created when sending group invitations, + -- but being RcvGroupEvent allows them to be assigned to the respective member (and so enable "send direct message") + -- and be created as unread without adding / working around new status for sent items + | RGEInvitedViaGroupLink -- CRSentGroupInvitationViaLink + deriving (Show, Generic) + +instance FromJSON RcvGroupEvent where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RGE" + +instance ToJSON RcvGroupEvent where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RGE" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RGE" + +newtype DBRcvGroupEvent = RGE RcvGroupEvent + +instance FromJSON DBRcvGroupEvent where + parseJSON v = RGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RGE") v + +instance ToJSON DBRcvGroupEvent where + toJSON (RGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RGE") v + toEncoding (RGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RGE") v + +data SndGroupEvent + = SGEMemberRole {groupMemberId :: GroupMemberId, profile :: Profile, role :: GroupMemberRole} + | SGEUserRole {role :: GroupMemberRole} + | SGEMemberDeleted {groupMemberId :: GroupMemberId, profile :: Profile} -- CRUserDeletedMember + | SGEUserLeft -- CRLeftMemberUser + | SGEGroupUpdated {groupProfile :: GroupProfile} -- CRGroupUpdated + deriving (Show, Generic) + +instance FromJSON SndGroupEvent where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SGE" + +instance ToJSON SndGroupEvent where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SGE" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SGE" + +newtype DBSndGroupEvent = SGE SndGroupEvent + +instance FromJSON DBSndGroupEvent where + parseJSON v = SGE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "SGE") v + +instance ToJSON DBSndGroupEvent where + toJSON (SGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SGE") v + toEncoding (SGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SGE") v + +data RcvConnEvent = RCESwitchQueue {phase :: SwitchPhase} + deriving (Show, Generic) + +data SndConnEvent = SCESwitchQueue {phase :: SwitchPhase, member :: Maybe GroupMemberRef} + deriving (Show, Generic) + +instance FromJSON RcvConnEvent where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RCE" + +instance ToJSON RcvConnEvent where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "RCE" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RCE" + +newtype DBRcvConnEvent = RCE RcvConnEvent + +instance FromJSON DBRcvConnEvent where + parseJSON v = RCE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "RCE") v + +instance ToJSON DBRcvConnEvent where + toJSON (RCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "RCE") v + toEncoding (RCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "RCE") v + +instance FromJSON SndConnEvent where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "SCE" + +instance ToJSON SndConnEvent where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "SCE" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "SCE" + +newtype DBSndConnEvent = SCE SndConnEvent + +instance FromJSON DBSndConnEvent where + parseJSON v = SCE <$> J.genericParseJSON (singleFieldJSON $ dropPrefix "SCE") v + +instance ToJSON DBSndConnEvent where + toJSON (SCE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SCE") v + toEncoding (SCE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SCE") v + +newtype DBMsgErrorType = DBME MsgErrorType + +instance FromJSON DBMsgErrorType where + parseJSON v = DBME <$> J.genericParseJSON (singleFieldJSON fstToLower) v + +instance ToJSON DBMsgErrorType where + toJSON (DBME v) = J.genericToJSON (singleFieldJSON fstToLower) v + toEncoding (DBME v) = J.genericToEncoding (singleFieldJSON fstToLower) v + +data CIGroupInvitation = CIGroupInvitation + { groupId :: GroupId, + groupMemberId :: GroupMemberId, + localDisplayName :: GroupName, + groupProfile :: GroupProfile, + status :: CIGroupInvitationStatus + } + deriving (Eq, Show, Generic, FromJSON) + +instance ToJSON CIGroupInvitation where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + +data CIGroupInvitationStatus + = CIGISPending + | CIGISAccepted + | CIGISRejected + | CIGISExpired + deriving (Eq, Show, Generic) + +instance FromJSON CIGroupInvitationStatus where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CIGIS" + +instance ToJSON CIGroupInvitationStatus where + toJSON = J.genericToJSON . enumJSON $ dropPrefix "CIGIS" + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CIGIS" + +ciContentToText :: CIContent d -> Text +ciContentToText = \case + CISndMsgContent mc -> msgContentText mc + CIRcvMsgContent mc -> msgContentText mc + CISndDeleted cidm -> ciDeleteModeToText cidm + CIRcvDeleted cidm -> ciDeleteModeToText cidm + CISndCall status duration -> "outgoing call: " <> ciCallInfoText status duration + CIRcvCall status duration -> "incoming call: " <> ciCallInfoText status duration + CIRcvIntegrityError err -> msgIntegrityError err + CIRcvDecryptionError err n -> msgDecryptErrorText err n + CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole + CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole + CIRcvGroupEvent event -> rcvGroupEventToText event + CISndGroupEvent event -> sndGroupEventToText event + CIRcvConnEvent event -> rcvConnEventToText event + CISndConnEvent event -> sndConnEventToText event + CIRcvChatFeature feature enabled param -> featureStateText feature enabled param + CISndChatFeature feature enabled param -> featureStateText feature enabled param + CIRcvChatPreference feature allowed param -> prefStateText feature allowed param + CISndChatPreference feature allowed param -> "you " <> prefStateText feature allowed param + CIRcvGroupFeature feature pref param -> groupPrefStateText feature pref param + CISndGroupFeature feature pref param -> groupPrefStateText feature pref param + CIRcvChatFeatureRejected feature -> chatFeatureNameText feature <> ": received, prohibited" + CIRcvGroupFeatureRejected feature -> groupFeatureNameText feature <> ": received, prohibited" + CISndModerated -> ciModeratedText + CIRcvModerated -> ciModeratedText + CIInvalidJSON _ -> "invalid content JSON" + +ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text +ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role = + "invitation to join group " <> displayName <> optionalFullName displayName fullName <> " as " <> (decodeLatin1 . strEncode $ role) + +rcvGroupEventToText :: RcvGroupEvent -> Text +rcvGroupEventToText = \case + RGEMemberAdded _ p -> "added " <> profileToText p + RGEMemberConnected -> "connected" + RGEMemberLeft -> "left" + RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + RGEUserRole r -> "changed your role to " <> safeDecodeUtf8 (strEncode r) + RGEMemberDeleted _ p -> "removed " <> profileToText p + RGEUserDeleted -> "removed you" + RGEGroupDeleted -> "deleted group" + RGEGroupUpdated _ -> "group profile updated" + RGEInvitedViaGroupLink -> "invited via your group link" + +sndGroupEventToText :: SndGroupEvent -> Text +sndGroupEventToText = \case + SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r) + SGEUserRole r -> "changed role for yourself to " <> safeDecodeUtf8 (strEncode r) + SGEMemberDeleted _ p -> "removed " <> profileToText p + SGEUserLeft -> "left" + SGEGroupUpdated _ -> "group profile updated" + +rcvConnEventToText :: RcvConnEvent -> Text +rcvConnEventToText = \case + RCESwitchQueue phase -> case phase of + SPCompleted -> "changed address for you" + _ -> decodeLatin1 (strEncode phase) <> " changing address for you..." + +sndConnEventToText :: SndConnEvent -> Text +sndConnEventToText = \case + SCESwitchQueue phase m -> case phase of + SPCompleted -> "you changed address" <> forMember m + _ -> decodeLatin1 (strEncode phase) <> " changing address" <> forMember m <> "..." + where + forMember member_ = + maybe "" (\GroupMemberRef {profile = Profile {displayName}} -> " for " <> displayName) member_ + +profileToText :: Profile -> Text +profileToText Profile {displayName, fullName} = displayName <> optionalFullName displayName fullName + +msgIntegrityError :: MsgErrorType -> Text +msgIntegrityError = \case + MsgSkipped fromId toId -> + "skipped message ID " <> tshow fromId + <> if fromId == toId then "" else ".." <> tshow toId + MsgBadId msgId -> "unexpected message ID " <> tshow msgId + MsgBadHash -> "incorrect message hash" + MsgDuplicate -> "duplicate message ID" + +msgDecryptErrorText :: MsgDecryptError -> Word32 -> Text +msgDecryptErrorText err n = + "decryption error, possibly due to the device change (" <> errName <> if n == 1 then ")" else ", " <> tshow n <> " messages)" + where + errName = case err of + MDERatchetHeader -> "header" + MDETooManySkipped -> "too many skipped messages" + +msgDirToModeratedContent_ :: SMsgDirection d -> CIContent d +msgDirToModeratedContent_ = \case + SMDRcv -> CIRcvModerated + SMDSnd -> CISndModerated + +ciModeratedText :: Text +ciModeratedText = "moderated" + +-- platform independent +instance MsgDirectionI d => ToField (CIContent d) where + toField = toField . encodeJSON . dbJsonCIContent + +-- platform specific +instance MsgDirectionI d => ToJSON (CIContent d) where + toJSON = J.toJSON . jsonCIContent + toEncoding = J.toEncoding . jsonCIContent + +data ACIContent = forall d. MsgDirectionI d => ACIContent (SMsgDirection d) (CIContent d) + +deriving instance Show ACIContent + +-- platform independent +dbParseACIContent :: Text -> Either String ACIContent +dbParseACIContent = fmap aciContentDBJSON . J.eitherDecodeStrict' . encodeUtf8 + +-- platform specific +instance FromJSON ACIContent where + parseJSON = fmap aciContentJSON . J.parseJSON + +-- platform specific +data JSONCIContent + = JCISndMsgContent {msgContent :: MsgContent} + | JCIRcvMsgContent {msgContent :: MsgContent} + | JCISndDeleted {deleteMode :: CIDeleteMode} + | JCIRcvDeleted {deleteMode :: CIDeleteMode} + | JCISndCall {status :: CICallStatus, duration :: Int} -- duration in seconds + | JCIRcvCall {status :: CICallStatus, duration :: Int} + | JCIRcvIntegrityError {msgError :: MsgErrorType} + | JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} + | JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} + | JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} + | JCIRcvGroupEvent {rcvGroupEvent :: RcvGroupEvent} + | JCISndGroupEvent {sndGroupEvent :: SndGroupEvent} + | JCIRcvConnEvent {rcvConnEvent :: RcvConnEvent} + | JCISndConnEvent {sndConnEvent :: SndConnEvent} + | JCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} + | JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} + | JCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} + | JCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} + | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | JCIRcvChatFeatureRejected {feature :: ChatFeature} + | JCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} + | JCISndModerated + | JCIRcvModerated + | JCIInvalidJSON {direction :: MsgDirection, json :: Text} + deriving (Generic) + +instance FromJSON JSONCIContent where + parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "JCI" + +instance ToJSON JSONCIContent where + toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "JCI" + toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "JCI" + +jsonCIContent :: forall d. MsgDirectionI d => CIContent d -> JSONCIContent +jsonCIContent = \case + CISndMsgContent mc -> JCISndMsgContent mc + CIRcvMsgContent mc -> JCIRcvMsgContent mc + CISndDeleted cidm -> JCISndDeleted cidm + CIRcvDeleted cidm -> JCIRcvDeleted cidm + CISndCall status duration -> JCISndCall {status, duration} + CIRcvCall status duration -> JCIRcvCall {status, duration} + CIRcvIntegrityError err -> JCIRcvIntegrityError err + CIRcvDecryptionError err n -> JCIRcvDecryptionError err n + CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole} + CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole} + CIRcvGroupEvent rcvGroupEvent -> JCIRcvGroupEvent {rcvGroupEvent} + CISndGroupEvent sndGroupEvent -> JCISndGroupEvent {sndGroupEvent} + CIRcvConnEvent rcvConnEvent -> JCIRcvConnEvent {rcvConnEvent} + CISndConnEvent sndConnEvent -> JCISndConnEvent {sndConnEvent} + CIRcvChatFeature feature enabled param -> JCIRcvChatFeature {feature, enabled, param} + CISndChatFeature feature enabled param -> JCISndChatFeature {feature, enabled, param} + CIRcvChatPreference feature allowed param -> JCIRcvChatPreference {feature, allowed, param} + CISndChatPreference feature allowed param -> JCISndChatPreference {feature, allowed, param} + CIRcvGroupFeature groupFeature preference param -> JCIRcvGroupFeature {groupFeature, preference, param} + CISndGroupFeature groupFeature preference param -> JCISndGroupFeature {groupFeature, preference, param} + CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} + CIRcvGroupFeatureRejected groupFeature -> JCIRcvGroupFeatureRejected {groupFeature} + CISndModerated -> JCISndModerated + CIRcvModerated -> JCISndModerated + CIInvalidJSON json -> JCIInvalidJSON (toMsgDirection $ msgDirection @d) json + +aciContentJSON :: JSONCIContent -> ACIContent +aciContentJSON = \case + JCISndMsgContent mc -> ACIContent SMDSnd $ CISndMsgContent mc + JCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc + JCISndDeleted cidm -> ACIContent SMDSnd $ CISndDeleted cidm + JCIRcvDeleted cidm -> ACIContent SMDRcv $ CIRcvDeleted cidm + JCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration + JCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration + JCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err + JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n + JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole + JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole + JCIRcvGroupEvent {rcvGroupEvent} -> ACIContent SMDRcv $ CIRcvGroupEvent rcvGroupEvent + JCISndGroupEvent {sndGroupEvent} -> ACIContent SMDSnd $ CISndGroupEvent sndGroupEvent + JCIRcvConnEvent {rcvConnEvent} -> ACIContent SMDRcv $ CIRcvConnEvent rcvConnEvent + JCISndConnEvent {sndConnEvent} -> ACIContent SMDSnd $ CISndConnEvent sndConnEvent + JCIRcvChatFeature {feature, enabled, param} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled param + JCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param + JCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param + JCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param + JCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param + JCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature + JCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature + JCISndModerated -> ACIContent SMDSnd CISndModerated + JCIRcvModerated -> ACIContent SMDRcv CIRcvModerated + JCIInvalidJSON dir json -> case fromMsgDirection dir of + AMsgDirection d -> ACIContent d $ CIInvalidJSON json + +-- platform independent +data DBJSONCIContent + = DBJCISndMsgContent {msgContent :: MsgContent} + | DBJCIRcvMsgContent {msgContent :: MsgContent} + | DBJCISndDeleted {deleteMode :: CIDeleteMode} + | DBJCIRcvDeleted {deleteMode :: CIDeleteMode} + | DBJCISndCall {status :: CICallStatus, duration :: Int} + | DBJCIRcvCall {status :: CICallStatus, duration :: Int} + | DBJCIRcvIntegrityError {msgError :: DBMsgErrorType} + | DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} + | DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} + | DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} + | DBJCIRcvGroupEvent {rcvGroupEvent :: DBRcvGroupEvent} + | DBJCISndGroupEvent {sndGroupEvent :: DBSndGroupEvent} + | DBJCIRcvConnEvent {rcvConnEvent :: DBRcvConnEvent} + | DBJCISndConnEvent {sndConnEvent :: DBSndConnEvent} + | DBJCIRcvChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} + | DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} + | DBJCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} + | DBJCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} + | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} + | DBJCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} + | DBJCISndModerated + | DBJCIRcvModerated + | DBJCIInvalidJSON {direction :: MsgDirection, json :: Text} + deriving (Generic) + +instance FromJSON DBJSONCIContent where + parseJSON = J.genericParseJSON . singleFieldJSON $ dropPrefix "DBJCI" + +instance ToJSON DBJSONCIContent where + toJSON = J.genericToJSON . singleFieldJSON $ dropPrefix "DBJCI" + toEncoding = J.genericToEncoding . singleFieldJSON $ dropPrefix "DBJCI" + +dbJsonCIContent :: forall d. MsgDirectionI d => CIContent d -> DBJSONCIContent +dbJsonCIContent = \case + CISndMsgContent mc -> DBJCISndMsgContent mc + CIRcvMsgContent mc -> DBJCIRcvMsgContent mc + CISndDeleted cidm -> DBJCISndDeleted cidm + CIRcvDeleted cidm -> DBJCIRcvDeleted cidm + CISndCall status duration -> DBJCISndCall {status, duration} + CIRcvCall status duration -> DBJCIRcvCall {status, duration} + CIRcvIntegrityError err -> DBJCIRcvIntegrityError $ DBME err + CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n + CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole} + CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole} + CIRcvGroupEvent rge -> DBJCIRcvGroupEvent $ RGE rge + CISndGroupEvent sge -> DBJCISndGroupEvent $ SGE sge + CIRcvConnEvent rce -> DBJCIRcvConnEvent $ RCE rce + CISndConnEvent sce -> DBJCISndConnEvent $ SCE sce + CIRcvChatFeature feature enabled param -> DBJCIRcvChatFeature {feature, enabled, param} + CISndChatFeature feature enabled param -> DBJCISndChatFeature {feature, enabled, param} + CIRcvChatPreference feature allowed param -> DBJCIRcvChatPreference {feature, allowed, param} + CISndChatPreference feature allowed param -> DBJCISndChatPreference {feature, allowed, param} + CIRcvGroupFeature groupFeature preference param -> DBJCIRcvGroupFeature {groupFeature, preference, param} + CISndGroupFeature groupFeature preference param -> DBJCISndGroupFeature {groupFeature, preference, param} + CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} + CIRcvGroupFeatureRejected groupFeature -> DBJCIRcvGroupFeatureRejected {groupFeature} + CISndModerated -> DBJCISndModerated + CIRcvModerated -> DBJCIRcvModerated + CIInvalidJSON json -> DBJCIInvalidJSON (toMsgDirection $ msgDirection @d) json + +aciContentDBJSON :: DBJSONCIContent -> ACIContent +aciContentDBJSON = \case + DBJCISndMsgContent mc -> ACIContent SMDSnd $ CISndMsgContent mc + DBJCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc + DBJCISndDeleted cidm -> ACIContent SMDSnd $ CISndDeleted cidm + DBJCIRcvDeleted cidm -> ACIContent SMDRcv $ CIRcvDeleted cidm + DBJCISndCall {status, duration} -> ACIContent SMDSnd $ CISndCall status duration + DBJCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration + DBJCIRcvIntegrityError (DBME err) -> ACIContent SMDRcv $ CIRcvIntegrityError err + DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n + DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole + DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole + DBJCIRcvGroupEvent (RGE rge) -> ACIContent SMDRcv $ CIRcvGroupEvent rge + DBJCISndGroupEvent (SGE sge) -> ACIContent SMDSnd $ CISndGroupEvent sge + DBJCIRcvConnEvent (RCE rce) -> ACIContent SMDRcv $ CIRcvConnEvent rce + DBJCISndConnEvent (SCE sce) -> ACIContent SMDSnd $ CISndConnEvent sce + DBJCIRcvChatFeature {feature, enabled, param} -> ACIContent SMDRcv $ CIRcvChatFeature feature enabled param + DBJCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param + DBJCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param + DBJCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param + DBJCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param + DBJCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature + DBJCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature + DBJCISndModerated -> ACIContent SMDSnd CISndModerated + DBJCIRcvModerated -> ACIContent SMDRcv CIRcvModerated + DBJCIInvalidJSON dir json -> case fromMsgDirection dir of + AMsgDirection d -> ACIContent d $ CIInvalidJSON json + +data CICallStatus + = CISCallPending + | CISCallMissed + | CISCallRejected -- only possible for received calls, not on type level + | CISCallAccepted + | CISCallNegotiated + | CISCallProgress + | CISCallEnded + | CISCallError + deriving (Show, Generic) + +instance FromJSON CICallStatus where + parseJSON = J.genericParseJSON . enumJSON $ dropPrefix "CISCall" + +instance ToJSON CICallStatus where + toJSON = J.genericToJSON . enumJSON $ dropPrefix "CISCall" + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "CISCall" + +ciCallInfoText :: CICallStatus -> Int -> Text +ciCallInfoText status duration = case status of + CISCallPending -> "calling..." + CISCallMissed -> "missed" + CISCallRejected -> "rejected" + CISCallAccepted -> "accepted" + CISCallNegotiated -> "connecting..." + CISCallProgress -> "in progress " <> durationText duration + CISCallEnded -> "ended " <> durationText duration + CISCallError -> "error" diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 4640a8bcdb..12ac37032d 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -329,6 +329,7 @@ import GHC.Generics (Generic) import Simplex.Chat.Call import Simplex.Chat.Markdown import Simplex.Chat.Messages +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Migrations.M20220101_initial import Simplex.Chat.Migrations.M20220122_v1_1 import Simplex.Chat.Migrations.M20220205_chat_item_status @@ -4866,8 +4867,8 @@ getGroupChatReactions_ db g c@Chat {chatItems} = do getDirectCIReactions :: DB.Connection -> Contact -> SharedMsgId -> IO [CIReactionCount] getDirectCIReactions db Contact {contactId} itemSharedMsgId = - map toCIReaction <$> - DB.query + map toCIReaction + <$> DB.query db [sql| SELECT reaction, MAX(reaction_sent), COUNT(chat_item_reaction_id) @@ -4879,8 +4880,8 @@ getDirectCIReactions db Contact {contactId} itemSharedMsgId = getGroupCIReactions :: DB.Connection -> GroupInfo -> MemberId -> SharedMsgId -> IO [CIReactionCount] getGroupCIReactions db GroupInfo {groupId} itemMemberId itemSharedMsgId = - map toCIReaction <$> - DB.query + map toCIReaction + <$> DB.query db [sql| SELECT reaction, MAX(reaction_sent), COUNT(chat_item_reaction_id) @@ -4905,14 +4906,15 @@ getACIReactions db aci@(AChatItem _ md chat ci@ChatItem {meta = CIMeta {itemShar deleteDirectCIReactions_ :: DB.Connection -> ContactId -> ChatItem 'CTDirect d -> IO () deleteDirectCIReactions_ db contactId ChatItem {meta = CIMeta {itemSharedMsgId}} = - forM_ itemSharedMsgId $ \itemSharedMId -> + forM_ itemSharedMsgId $ \itemSharedMId -> DB.execute db "DELETE FROM chat_item_reactions WHERE contact_id = ? AND shared_msg_id = ?" (contactId, itemSharedMId) deleteGroupCIReactions_ :: DB.Connection -> GroupInfo -> ChatItem 'CTGroup d -> IO () deleteGroupCIReactions_ db g@GroupInfo {groupId} ci@ChatItem {meta = CIMeta {itemSharedMsgId}} = forM_ itemSharedMsgId $ \itemSharedMId -> do let GroupMember {memberId} = chatItemMember g ci - DB.execute db + DB.execute + db "DELETE FROM chat_item_reactions WHERE group_id = ? AND shared_msg_id = ? AND item_member_id = ?" (groupId, itemSharedMId, memberId) @@ -4921,8 +4923,8 @@ toCIReaction (reaction, userReacted, totalReacted) = CIReactionCount {reaction, getDirectReactions :: DB.Connection -> Contact -> SharedMsgId -> Bool -> IO [MsgReaction] getDirectReactions db ct itemSharedMId sent = - map fromOnly <$> - DB.query + map fromOnly + <$> DB.query db [sql| SELECT reaction @@ -4953,8 +4955,8 @@ setDirectReaction db ct itemSharedMId sent reaction add msgId reactionTs getGroupReactions :: DB.Connection -> GroupInfo -> GroupMember -> MemberId -> SharedMsgId -> Bool -> IO [MsgReaction] getGroupReactions db GroupInfo {groupId} m itemMemberId itemSharedMId sent = - map fromOnly <$> - DB.query + map fromOnly + <$> DB.query db [sql| SELECT reaction diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 719b77b73c..37644ad5e9 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -31,6 +31,7 @@ import GHC.Weak (deRefWeak) import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Messages +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Styled import Simplex.Chat.Terminal.Output import Simplex.Chat.Types (User (..)) @@ -322,7 +323,7 @@ updateTermState user_ st ac live tw (key, ms) ts@TerminalState {inputString = s, go _ _ = "" charsWithContact cs | live = cs - | null s && cs /= "@" && cs /= "#" && cs /= "/" && cs /= ">" && cs /= "\\" && cs /= "!" && cs /= "+" && cs /= "-" = + | null s && cs /= "@" && cs /= "#" && cs /= "/" && cs /= ">" && cs /= "\\" && cs /= "!" && cs /= "+" && cs /= "-" = contactPrefix <> cs | (s == ">" || s == "\\" || s == "!") && cs == " " = cs <> contactPrefix diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index a49b0d460f..c13318250e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -37,6 +37,7 @@ import Simplex.Chat.Controller import Simplex.Chat.Help import Simplex.Chat.Markdown import Simplex.Chat.Messages hiding (NewChatItem (..)) +import Simplex.Chat.Messages.ChatItemContent import Simplex.Chat.Protocol import Simplex.Chat.Store (AutoAccept (..), StoreError (..), UserContactLink (..)) import Simplex.Chat.Styled diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 8359e72e33..b813569089 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -95,7 +95,8 @@ data TestCC = TestCC virtualTerminal :: VirtualTerminal, chatAsync :: Async (), termAsync :: Async (), - termQ :: TQueue String + termQ :: TQueue String, + printOutput :: Bool } aCfg :: AgentConfig @@ -149,7 +150,7 @@ startTestChat_ db cfg opts user = do atomically . unless (maintenance opts) $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO termAsync <- async $ readTerminalOutput t termQ - pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ} + pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ, printOutput = False} stopTestChat :: TestCC -> IO () stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do @@ -192,6 +193,9 @@ withTestChatOpts tmp = withTestChatCfgOpts tmp testCfg withTestChatCfgOpts :: HasCallStack => FilePath -> ChatConfig -> ChatOpts -> String -> (HasCallStack => TestCC -> IO a) -> IO a withTestChatCfgOpts tmp cfg opts dbPrefix = bracket (startTestChat tmp cfg opts dbPrefix) (\cc -> cc > stopTestChat cc) +withTestOutput :: HasCallStack => TestCC -> (HasCallStack => TestCC -> IO a) -> IO a +withTestOutput cc runTest = runTest cc {printOutput = True} + readTerminalOutput :: VirtualTerminal -> TQueue String -> IO () readTerminalOutput t termQ = do let w = virtualWindow t @@ -239,14 +243,15 @@ getTermLine :: HasCallStack => TestCC -> IO String getTermLine cc = 5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case Just s -> do - -- uncomment 2 lines below to echo virtual terminal - -- name <- userName cc - -- putStrLn $ name <> ": " <> s + -- remove condition to always echo virtual terminal + when (printOutput cc) $ do + name <- userName cc + putStrLn $ name <> ": " <> s pure s _ -> error "no output for 5 seconds" userName :: TestCC -> IO [Char] -userName (TestCC ChatController {currentUser} _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser +userName (TestCC ChatController {currentUser} _ _ _ _ _) = T.unpack . localDisplayName . fromJust <$> readTVarIO currentUser testChat2 :: HasCallStack => Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () testChat2 = testChatCfgOpts2 testCfg testOpts diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 17a3c23bcf..48b3fd9537 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -895,8 +895,8 @@ testMaintenanceModeWithFiles tmp = do testDatabaseEncryption :: HasCallStack => FilePath -> IO () testDatabaseEncryption tmp = do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \alice -> do + withNewTestChat tmp "bob" bobProfile $ \b -> withTestOutput b $ \bob -> do + withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do alice ##> "/_start" alice <## "chat started" connectUsers alice bob @@ -914,7 +914,7 @@ testDatabaseEncryption tmp = do alice <## "ok" alice ##> "/_start" alice <## "error: chat store changed, please restart chat" - withTestChatOpts tmp (getTestOpts True "mykey") "alice" $ \alice -> do + withTestChatOpts tmp (getTestOpts True "mykey") "alice" $ \a -> withTestOutput a $ \alice -> do alice ##> "/_start" alice <## "chat started" testChatWorking alice bob @@ -926,7 +926,7 @@ testDatabaseEncryption tmp = do alice <## "ok" alice ##> "/_db encryption {\"currentKey\":\"nextkey\",\"newKey\":\"anotherkey\"}" alice <## "ok" - withTestChatOpts tmp (getTestOpts True "anotherkey") "alice" $ \alice -> do + withTestChatOpts tmp (getTestOpts True "anotherkey") "alice" $ \a -> withTestOutput a $ \alice -> do alice ##> "/_start" alice <## "chat started" testChatWorking alice bob @@ -934,7 +934,8 @@ testDatabaseEncryption tmp = do alice <## "chat stopped" alice ##> "/db decrypt anotherkey" alice <## "ok" - withTestChat tmp "alice" $ \alice -> testChatWorking alice bob + withTestChat tmp "alice" $ \a -> withTestOutput a $ \alice -> do + testChatWorking alice bob testMuteContact :: HasCallStack => FilePath -> IO () testMuteContact = @@ -1315,13 +1316,13 @@ testUsersRestartCIExpiration tmp = do withNewTestChat tmp "bob" bobProfile $ \bob -> do withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do -- set ttl for first user - alice #$> ("/_ttl 1 1", id, "ok") + alice #$> ("/_ttl 1 2", id, "ok") connectUsers alice bob -- create second user and set ttl alice ##> "/create user alisa" showActiveUser alice "alisa" - alice #$> ("/_ttl 2 3", id, "ok") + alice #$> ("/_ttl 2 5", id, "ok") connectUsers alice bob -- first user messages @@ -1353,7 +1354,7 @@ testUsersRestartCIExpiration tmp = do -- first user messages alice ##> "/user alice" showActiveUser alice "alice (Alice)" - alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)") + alice #$> ("/ttl", id, "old messages are set to be deleted after: 2 second(s)") alice #> "@bob alice 3" bob <# "alice> alice 3" @@ -1365,7 +1366,7 @@ testUsersRestartCIExpiration tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/ttl", id, "old messages are set to be deleted after: 3 second(s)") + alice #$> ("/ttl", id, "old messages are set to be deleted after: 5 second(s)") alice #> "@bob alisa 3" bob <# "alisa> alisa 3" @@ -1374,7 +1375,7 @@ testUsersRestartCIExpiration tmp = do alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2000000 + threadDelay 3000000 -- messages both before and after restart are deleted -- first user messages @@ -1387,7 +1388,7 @@ testUsersRestartCIExpiration tmp = do showActiveUser alice "alisa" alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2000000 + threadDelay 3000000 alice #$> ("/_get chat @4 count=100", chat, []) where diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 862dcc5489..9ecf95a729 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -50,7 +50,7 @@ chatFileTests = do describe "async sending and receiving files" $ do -- fails on CI xit'' "send and receive file, sender restarts" testAsyncFileTransferSenderRestarts - it "send and receive file, receiver restarts" testAsyncFileTransferReceiverRestarts + xit'' "send and receive file, receiver restarts" testAsyncFileTransferReceiverRestarts xdescribe "send and receive file, fully asynchronous" $ do it "v2" testAsyncFileTransfer it "v1" testAsyncFileTransferV1 @@ -65,7 +65,7 @@ chatFileTests = do it "with changed XFTP config: send and receive file" testXFTPWithChangedConfig it "with relative paths: send and receive file" testXFTPWithRelativePaths xit' "continue receiving file after restart" testXFTPContinueRcv - it "receive file marked to receive on chat start" testXFTPMarkToReceive + xit' "receive file marked to receive on chat start" testXFTPMarkToReceive it "error receiving file" testXFTPRcvError it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat @@ -986,13 +986,17 @@ testXFTPFileTransfer = alice #> "/f @bob ./tests/fixtures/test.pdf" alice <## "use /fc 1 to cancel sending" + -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [/ | ] to receive it" bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? - alice <## "completed uploading file 1 (test.pdf) for bob" - bob <## "started receiving file 1 (test.pdf) from alice" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for bob", + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] bob <## "completed receiving file 1 (test.pdf) from alice" alice ##> "/fs 1" @@ -1022,8 +1026,10 @@ testXFTPAcceptAfterUpload = threadDelay 100000 bob ##> "/fr 1 ./tests/tmp" - bob <## "started receiving file 1 (test.pdf) from alice" - bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] bob <## "completed receiving file 1 (test.pdf) from alice" src <- B.readFile "./tests/fixtures/test.pdf" @@ -1166,13 +1172,17 @@ testXFTPWithChangedConfig = alice #> "/f @bob ./tests/fixtures/test.pdf" alice <## "use /fc 1 to cancel sending" + -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [/ | ] to receive it" bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? - alice <## "completed uploading file 1 (test.pdf) for bob" - bob <## "started receiving file 1 (test.pdf) from alice" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for bob", + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] bob <## "completed receiving file 1 (test.pdf) from alice" src <- B.readFile "./tests/fixtures/test.pdf" @@ -1205,13 +1215,17 @@ testXFTPWithRelativePaths = alice #> "/f @bob test.pdf" alice <## "use /fc 1 to cancel sending" + -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" bob <## "use /fr 1 [/ | ] to receive it" bob ##> "/fr 1" - bob <## "saving file 1 from alice to test.pdf" - -- alice <## "started sending file 1 (test.pdf) to bob" -- TODO "started uploading" ? - alice <## "completed uploading file 1 (test.pdf) for bob" - bob <## "started receiving file 1 (test.pdf) from alice" + concurrentlyN_ + [ alice <## "completed uploading file 1 (test.pdf) for bob", + bob + <### [ "saving file 1 from alice to test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] + ] bob <## "completed receiving file 1 (test.pdf) from alice" src <- B.readFile "./tests/fixtures/test.pdf" @@ -1238,8 +1252,10 @@ testXFTPContinueRcv tmp = do withTestChatCfg tmp cfg "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/fr 1 ./tests/tmp" - bob <## "started receiving file 1 (test.pdf) from alice" - bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] bob ##> "/fs 1" bob <## "receiving file 1 (test.pdf) progress 0% of 266.0 KiB" @@ -1310,8 +1326,10 @@ testXFTPRcvError tmp = do withTestChatCfg tmp cfg "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/fr 1 ./tests/tmp" - bob <## "started receiving file 1 (test.pdf) from alice" - bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" + bob + <### [ "saving file 1 from alice to ./tests/tmp/test.pdf", + "started receiving file 1 (test.pdf) from alice" + ] bob <## "error receiving file 1 (test.pdf) from alice" bob ##> "/fs 1" @@ -1329,13 +1347,17 @@ testXFTPCancelRcvRepeat = alice #> "/f @bob ./tests/tmp/testfile" alice <## "use /fc 1 to cancel sending" + -- alice <## "started sending file 1 (testfile) to bob" -- TODO "started uploading" ? bob <# "alice> sends file testfile (17.0 MiB / 17825792 bytes)" bob <## "use /fr 1 [/ | ] to receive it" bob ##> "/fr 1 ./tests/tmp" - bob <## "saving file 1 from alice to ./tests/tmp/testfile_1" - -- alice <## "started sending file 1 (testfile) to bob" -- TODO "started uploading" ? - alice <## "completed uploading file 1 (testfile) for bob" - bob <## "started receiving file 1 (testfile) from alice" + concurrentlyN_ + [ alice <## "completed uploading file 1 (testfile) for bob", + bob + <### [ "saving file 1 from alice to ./tests/tmp/testfile_1", + "started receiving file 1 (testfile) from alice" + ] + ] threadDelay 100000 diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index a6223cfafc..db8d634571 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -7,6 +7,7 @@ module ChatTests.Utils where import ChatClient +import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) import Control.Concurrent.STM import Control.Monad (unless, when) @@ -199,18 +200,20 @@ groupFeatures = map (\(a, _, _) -> a) groupFeatures'' groupFeatures'' :: [((Int, String), Maybe (Int, String), Maybe String)] groupFeatures'' = - [ ((0, "Disappearing messages: off"), Nothing, Nothing), - ((0, "Direct messages: on"), Nothing, Nothing), - ((0, "Full deletion: off"), Nothing, Nothing), - ((0, "Message reactions: on"), Nothing, Nothing), - ((0, "Voice messages: on"), Nothing, Nothing) - ] + [ ((0, "Disappearing messages: off"), Nothing, Nothing), + ((0, "Direct messages: on"), Nothing, Nothing), + ((0, "Full deletion: off"), Nothing, Nothing), + ((0, "Message reactions: on"), Nothing, Nothing), + ((0, "Voice messages: on"), Nothing, Nothing) + ] itemId :: Int -> String itemId i = show $ length chatFeatures + i (@@@) :: HasCallStack => TestCC -> [(String, String)] -> Expectation -(@@@) = getChats mapChats +(@@@) cc res = do + threadDelay 10000 + getChats mapChats cc res mapChats :: [(String, String, Maybe ConnStatus)] -> [(String, String)] mapChats = map $ \(ldn, msg, _) -> (ldn, msg) @@ -407,7 +410,7 @@ connectUsers cc1 cc2 = do (cc1 <## (name2 <> ": contact is connected")) showName :: TestCC -> IO String -showName (TestCC ChatController {currentUser} _ _ _ _) = do +showName (TestCC ChatController {currentUser} _ _ _ _ _) = do Just User {localDisplayName, profile = LocalProfile {fullName}} <- readTVarIO currentUser pure . T.unpack $ localDisplayName <> optionalFullName localDisplayName fullName