mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Merge branch 'master' into master-ios
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)|[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|
||||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|
||||
|🇯🇵 ja|Japanese ||[](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|||
|
||||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|
||||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|
||||
|🇧🇷 pt-BR|Português||[](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|
||||
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|
||||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[](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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<ArchiveError> {
|
||||
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<ArchiveError>): 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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_hour">%d hodinu</string>
|
||||
<string name="feature_offered_item_with_param">offered %s: %2s</string>
|
||||
<string name="feature_offered_item_with_param">nabízeno %s: %2s</string>
|
||||
<string name="v4_2_group_links">Odkazy na skupiny</string>
|
||||
<string name="v4_3_voice_messages">Hlasové zprávy</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Vaše kontakty mohou povolit úplné mazání zpráv.</string>
|
||||
@@ -809,7 +809,7 @@
|
||||
<string name="you_joined_this_group">Připojili jste se k této skupině</string>
|
||||
<string name="you_rejected_group_invitation">Odmítli jste pozvánku do skupiny</string>
|
||||
<string name="group_invitation_expired">Platnost pozvánky do skupiny vypršela</string>
|
||||
<string name="rcv_group_event_member_added">pozvaný <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_added">pozval <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_connected">připojen</string>
|
||||
<string name="rcv_group_event_changed_member_role">změnil roli %s na %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">změnil svou roli na %s</string>
|
||||
@@ -1168,11 +1168,11 @@
|
||||
<string name="share_address">Sdílet adresu</string>
|
||||
<string name="info_row_moderated_at">Upraveno v</string>
|
||||
<string name="info_row_received_at">Přijato v</string>
|
||||
<string name="share_text_received_at">Přijato v: %s</string>
|
||||
<string name="share_text_updated_at">Záznam aktualizován v: %s</string>
|
||||
<string name="share_text_sent_at">Posláno v: %s</string>
|
||||
<string name="share_text_disappears_at">Zmizí v: %s</string>
|
||||
<string name="share_text_moderated_at">Upraveno v: %s</string>
|
||||
<string name="share_text_received_at">Přijato: %s</string>
|
||||
<string name="share_text_updated_at">Záznam aktualizován: %s</string>
|
||||
<string name="share_text_sent_at">Posláno: %s</string>
|
||||
<string name="share_text_disappears_at">Zmizí: %s</string>
|
||||
<string name="share_text_moderated_at">Upraveno: %s</string>
|
||||
<string name="current_version_timestamp">%s (aktuální)</string>
|
||||
<string name="dark_theme">Tmavý motiv</string>
|
||||
<string name="import_theme">Import motivu</string>
|
||||
@@ -1245,7 +1245,7 @@
|
||||
<string name="item_info_current">(aktuální)</string>
|
||||
<string name="share_text_database_id">ID databáze: %d</string>
|
||||
<string name="info_row_deleted_at">Smazáno v</string>
|
||||
<string name="share_text_deleted_at">Smazáno v: %s</string>
|
||||
<string name="share_text_deleted_at">Smazáno: %s</string>
|
||||
<string name="info_row_disappears_at">Zmizí v</string>
|
||||
<string name="enter_welcome_message">Zadat uvítací zprávu…</string>
|
||||
<string name="import_theme_error">Chyba importu motivu</string>
|
||||
@@ -1261,4 +1261,5 @@
|
||||
<string name="you_can_share_your_address">Svou adresu můžete sdílet jako odkaz nebo QR kód - kdokoli se k vám může připojit.</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Pokud později adresu odstraníte, o kontakty nepřijdete.</string>
|
||||
<string name="app_passcode_replaced_with_self_destruct">Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem.</string>
|
||||
<string name="item_info_no_text">žádný text</string>
|
||||
</resources>
|
||||
@@ -1341,4 +1341,5 @@
|
||||
<string name="share_text_disappears_at">Verschwindet um: %s</string>
|
||||
<string name="edit_history">Vergangenheit</string>
|
||||
<string name="message_reactions_prohibited_in_this_chat">In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt.</string>
|
||||
<string name="item_info_no_text">Kein Text</string>
|
||||
</resources>
|
||||
@@ -27,7 +27,7 @@
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Se puede desactivar en Configuración</b> – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.</string>
|
||||
<string name="notifications_mode_service">Siempre activo</string>
|
||||
<string name="allow_verb">Permitir</string>
|
||||
<string name="above_then_preposition_continuation">arriba y luego:</string>
|
||||
<string name="above_then_preposition_continuation">y después:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Añadir nuevo contacto</b>: para crear tu código QR de un solo uso para tu contacto.</string>
|
||||
<string name="accept_connection_request__question">¿Aceptar solicitud de conexión\?</string>
|
||||
<string name="accept_contact_incognito_button">Aceptar incógnito</string>
|
||||
@@ -37,9 +37,9 @@
|
||||
<string name="all_your_contacts_will_remain_connected">Todos tus contactos permanecerán conectados.</string>
|
||||
<string name="appearance_settings">Apariencia</string>
|
||||
<string name="app_version_title">Versión</string>
|
||||
<string name="network_session_mode_user_description">Se usará una conexión TCP independiente (y credencial SOCKS) <b>por cada perfil de chat que tengas en la aplicación</b>.</string>
|
||||
<string name="network_session_mode_entity_description">Se usará una conexión TCP independiente (y credencial SOCKS) <b>por cada contacto y miembro del grupo</b>.
|
||||
\n<b>Atención</b>: si tienes muchas conexiones, tu consumo de batería y tráfico pueden ser sustancialmente mayor y algunas conexiones pueden fallar.</string>
|
||||
<string name="network_session_mode_user_description">Se usará una conexión TCP (y credenciales SOCKS) independiente <b>por cada perfil que tengas en la aplicación</b>.</string>
|
||||
<string name="network_session_mode_entity_description">Se usará una conexión TCP (y credenciales SOCKS) independiente <b>por cada contacto y miembro del grupo</b>.
|
||||
\n<b>Atención</b>: si tienes muchas conexiones, el consumo de batería y tráfico pueden ser sustancialmente mayores y algunas conexiones pueden fallar.</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex">Acerca de SimpleX</string>
|
||||
<string name="bold">negrita</string>
|
||||
@@ -83,10 +83,10 @@
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Tanto tú como tu contacto podéis enviar mensajes temporales.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Escanear código QR</b>: para conectar con tu contacto mediante su código QR.</string>
|
||||
<string name="create_profile_button">Crear</string>
|
||||
<string name="create_one_time_link">Crear enlace de invitación de un solo uso.</string>
|
||||
<string name="create_group">Crear grupo secreto</string>
|
||||
<string name="create_one_time_link">Crea enlace de invitación de un uso</string>
|
||||
<string name="create_group">Crea grupo secreto</string>
|
||||
<string name="database_passphrase_will_be_updated">La contraseña de cifrado de la base de datos será actualizada.</string>
|
||||
<string name="info_row_database_id">ID de base de datos</string>
|
||||
<string name="info_row_database_id">ID base de datos</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Los mensajes directos entre miembros del grupo no están permitidos.</string>
|
||||
<string name="passphrase_is_different">La contraseña es distinta a la almacenada en Keystore</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">La base de datos será cifrada y la contraseña se guardará en Keystore.</string>
|
||||
@@ -95,7 +95,7 @@
|
||||
<string name="delete_chat_profile_question">¿Eliminar el perfil de chat\?</string>
|
||||
<string name="rcv_group_event_group_deleted">grupo eliminado</string>
|
||||
<string name="delete_group_question">¿Eliminar grupo\?</string>
|
||||
<string name="delete_messages_after">Eliminar después de</string>
|
||||
<string name="delete_messages_after">Eliminar en</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo.</string>
|
||||
<string name="no_call_on_lock_screen">Desactivar</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Los mensajes temporales no están permitidos en este chat.</string>
|
||||
@@ -142,12 +142,12 @@
|
||||
<string name="group_member_status_accepted">conectando (aceptado)</string>
|
||||
<string name="group_member_status_announced">conectando (anunciado)</string>
|
||||
<string name="connection_local_display_name">conexión <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="connect_via_link_or_qr">Conectar mediante enlace / Código QR</string>
|
||||
<string name="connect_via_link_or_qr">Conecta vía enlace / Código QR</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">El contacto y todos los mensajes serán eliminados. ¡No se puede deshacer!</string>
|
||||
<string name="icon_descr_contact_checked">Contacto verificado</string>
|
||||
<string name="status_contact_has_e2e_encryption">el contacto dispone de cifrado de extremo a extremo</string>
|
||||
<string name="smp_server_test_disconnect">Desconectar</string>
|
||||
<string name="notification_preview_mode_contact">Nombre del contacto</string>
|
||||
<string name="notification_preview_mode_contact">Contacto</string>
|
||||
<string name="copy_verb">Copiar</string>
|
||||
<string name="create_your_profile">Crear tu perfil</string>
|
||||
<string name="always_use_relay">Siempre usar relay</string>
|
||||
@@ -156,7 +156,7 @@
|
||||
<string name="enable_automatic_deletion_question">¿Activar eliminación automática de mensajes\?</string>
|
||||
<string name="contact_preferences">Preferencias de contacto</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="delete_after">Eliminar después de</string>
|
||||
<string name="delete_after">Eliminar en</string>
|
||||
<string name="ttl_sec">%d seg</string>
|
||||
<string name="contact_already_exists">El contácto ya existe</string>
|
||||
<string name="connection_error_auth">Error de conexión (Autenticación)</string>
|
||||
@@ -164,15 +164,15 @@
|
||||
<string name="icon_descr_server_status_disconnected">Desconectado</string>
|
||||
<string name="icon_descr_server_status_connected">Conectado</string>
|
||||
<string name="copied">Copiado en portapapeles</string>
|
||||
<string name="share_one_time_link">Crear enlace de invitación de un solo uso.</string>
|
||||
<string name="share_one_time_link">Crea enlace de invitación de un uso.</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 PC: escanéa el código QR desde la aplicación mediante <b>Escanear código QR </b></string>
|
||||
<string name="delete_contact_menu_action">Eliminar</string>
|
||||
<string name="delete_group_menu_action">Eliminar</string>
|
||||
<string name="alert_title_contact_connection_pending">¡El contacto aun no se ha conectado!</string>
|
||||
<string name="delete_pending_connection__question">¿Eliminar conexion pendiente\?</string>
|
||||
<string name="delete_pending_connection__question">¿Eliminar la conexion pendiente\?</string>
|
||||
<string name="icon_descr_close_button">Botón cerrar</string>
|
||||
<string name="connection_request_sent">¡Solicitud de conexión enviada!</string>
|
||||
<string name="network_session_mode_entity">Conexión</string>
|
||||
<string name="network_session_mode_entity">por conexión</string>
|
||||
<string name="callstatus_connecting">conectando llamada…</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Activar llamadas desde la pantalla de bloqueo en Configuración</string>
|
||||
<string name="database_encryption_will_be_updated">La contraseña de cifrado de la base de datos será actualizada y almacenada en Keystore.</string>
|
||||
@@ -276,7 +276,7 @@
|
||||
<string name="cannot_receive_file">No se puede recibir el archivo</string>
|
||||
<string name="database_initialization_error_title">No se puede iniciar la base de datos</string>
|
||||
<string name="clear_chat_question">¿Vaciar chat\?</string>
|
||||
<string name="network_session_mode_user">Perfil de Chat</string>
|
||||
<string name="network_session_mode_user">por perfil</string>
|
||||
<string name="chat_is_stopped_indication">Chat está detenido</string>
|
||||
<string name="rcv_group_event_changed_member_role">rol de %s cambiado a %s</string>
|
||||
<string name="change_role">Cambiar rol</string>
|
||||
@@ -382,7 +382,7 @@
|
||||
<string name="error_deleting_group">Error al eliminar grupo</string>
|
||||
<string name="error_deleting_pending_contact_connection">Error al eliminar conexión de contacto pendiente</string>
|
||||
<string name="hide_notification">Ocultar</string>
|
||||
<string name="notification_preview_mode_hidden">Oculta</string>
|
||||
<string name="notification_preview_mode_hidden">Sin vista previa</string>
|
||||
<string name="notification_display_mode_hidden_desc">Se ocultan tanto el contacto como el mensaje</string>
|
||||
<string name="hide_verb">Ocultar</string>
|
||||
<string name="for_everybody">Para todos</string>
|
||||
@@ -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.</string>
|
||||
<string name="notification_preview_mode_message">Texto del mensaje</string>
|
||||
<string name="notification_preview_mode_message">Contacto y texto</string>
|
||||
<string name="member_info_section_title_member">MIEMBRO</string>
|
||||
<string name="chat_item_ttl_none">nunca</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Se requieren hosts .onion para la conexión</string>
|
||||
@@ -481,7 +481,7 @@
|
||||
<string name="group_member_status_left">ha salido</string>
|
||||
<string name="v4_4_live_messages">Mensajes en vivo</string>
|
||||
<string name="service_notifications">Notificación instantánea</string>
|
||||
<string name="settings_notifications_mode_title">Servicio de notificaciones</string>
|
||||
<string name="settings_notifications_mode_title">Servicio</string>
|
||||
<string name="delete_message_mark_deleted_warning">El mensaje se marcará para eliminar. El destinatario o destinatarios podrán revelar este mensaje.</string>
|
||||
<string name="live_message">¡Mensaje en vivo!</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 móvil: pulse <b>Abrir en aplicación móvil</b>, después pulse <b>Conectar</b> en la aplicación.</string>
|
||||
@@ -600,7 +600,7 @@
|
||||
<string name="only_you_can_send_disappearing">Sólo tú puedes enviar mensajes temporales.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Sólo tu contacto puede enviar mensajes temporales.</string>
|
||||
<string name="prohibit_sending_voice">No se permiten mensajes de voz.</string>
|
||||
<string name="notifications_mode_off">Se ejecuta sólo cuando la aplicación está abierta</string>
|
||||
<string name="notifications_mode_off">Sólo con aplicación abierta</string>
|
||||
<string name="auth_open_chat_console">Abrir consola de chat</string>
|
||||
<string name="save_verb">Guardar</string>
|
||||
<string name="restore_database_alert_confirm">Restaurar</string>
|
||||
@@ -780,7 +780,7 @@
|
||||
<string name="error_smp_test_server_auth">El servidor requiere autorización para crear colas, comprueba la contraseña</string>
|
||||
<string name="enter_passphrase_notification_desc">Para recibir notificaciones, introduce la contraseña de la base de datos</string>
|
||||
<string name="ntf_channel_calls">Llamadas de chat SimpleX</string>
|
||||
<string name="notifications_mode_periodic">Se ejecuta periódicamente</string>
|
||||
<string name="notifications_mode_periodic">Cíclico</string>
|
||||
<string name="notification_preview_mode_message_desc">Se muestran el nombre del contacto y el mensaje</string>
|
||||
<string name="notification_preview_mode_contact_desc">Se muestra sólo el nombre del contacto</string>
|
||||
<string name="auth_simplex_lock_turned_on">Bloqueo SimpleX activado</string>
|
||||
@@ -884,7 +884,7 @@
|
||||
<string name="you_can_also_connect_by_clicking_the_link">También puedes conectarte pulsando el enlace. Si se abre en el navegador, pulsa en <b>Abrir en aplicación móvil</b>.</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Puedes <font color="#0088ff">ponerte en contacto con los desarrolladores de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para consultas y para recibir actualizaciones</font>.</string>
|
||||
<string name="observer_cant_send_message_title">¡No puedes enviar mensajes!</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Puedes usar la sintaxis markdown para dar formato a los mensajes:</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Puedes usar la sintaxis markdown para dar formato a tus mensajes:</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">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.</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">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).</string>
|
||||
@@ -903,7 +903,7 @@
|
||||
<string name="waiting_for_file">Esperando archivo</string>
|
||||
<string name="icon_descr_waiting_for_image">Esperando imagen</string>
|
||||
<string name="voice_message_with_duration">Mensaje de voz (<xliff:g id="duration">%1$s</xliff:g> )</string>
|
||||
<string name="network_use_onion_hosts_prefer">Si disponible</string>
|
||||
<string name="network_use_onion_hosts_prefer">Si disponibles</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">No almacenamos ninguno de tus contactos o mensajes (una vez entregados) en los servidores.</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Has sido invitado al grupo. Únete para conectarte con los miembros del grupo.</string>
|
||||
<string name="snd_group_event_member_deleted">has expulsado a <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
@@ -1003,15 +1003,15 @@
|
||||
<string name="v4_6_reduced_battery_usage">Reducción consumo de batería</string>
|
||||
<string name="v4_6_group_welcome_message">Mensaje de bienvenida en grupos</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">¡Más mejoras en camino!</string>
|
||||
<string name="hidden_profile_password">Contraseña de perfil oculto</string>
|
||||
<string name="hidden_profile_password">Contraseña del perfil oculto</string>
|
||||
<string name="save_welcome_message_question">¿Guardar mensaje de bienvenida\?</string>
|
||||
<string name="user_unmute">Activar audio</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Puedes ocultar o silenciar un perfil. Mantenlo pulsado para abrir el menú.</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos.</string>
|
||||
<string name="v4_6_group_moderation_descr">Ahora los administradores pueden:
|
||||
\n- borrar mensajes de los miembros.
|
||||
\n- desactivar el rol a miembros (pasando a rol \"observador\")</string>
|
||||
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Tus perfiles Chat.</string>
|
||||
\n- desactivar el rol miembro (a rol \"observador\")</string>
|
||||
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Mis perfiles.</string>
|
||||
<string name="database_upgrade">Actualización de la base de datos</string>
|
||||
<string name="database_downgrade">Volviendo a versión anterior de la base de datos</string>
|
||||
<string name="invalid_migration_confirmation">Confirmación de migración no válida</string>
|
||||
@@ -1057,7 +1057,7 @@
|
||||
<string name="your_XFTP_servers">Mis servidores XFTP</string>
|
||||
<string name="port_verb">Puerto</string>
|
||||
<string name="network_proxy_port">puerto %d</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Establece <i>Usar hosts .onion</i> en No si el proxy SOCKS no los admite.</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Configura <i>Usar hosts .onion</i> a No si el proxy SOCKS no los admite.</string>
|
||||
<string name="smp_server_test_download_file">Descargar archivo</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Usar proxy SOCKS</string>
|
||||
<string name="host_verb">Host</string>
|
||||
@@ -1083,8 +1083,8 @@
|
||||
<string name="you_can_turn_on_lock">Puedes activar el Bloqueo SimpleX a través de Configuración.</string>
|
||||
<string name="confirm_passcode">Confirma código</string>
|
||||
<string name="enable_lock">Activar bloqueo</string>
|
||||
<string name="lock_after">Bloquear después de</string>
|
||||
<string name="lock_mode">Modo de bloqueo</string>
|
||||
<string name="lock_after">Bloquear en</string>
|
||||
<string name="lock_mode">Modo bloqueo</string>
|
||||
<string name="submit_passcode">Enviar</string>
|
||||
<string name="incorrect_passcode">Código de acceso incorrecto</string>
|
||||
<string name="new_passcode">Código nuevo</string>
|
||||
@@ -1186,7 +1186,7 @@
|
||||
<string name="your_contacts_will_see_it">Tus contactos en SimpleX lo verán.
|
||||
\nPuedes cambiarlo en Configuración.</string>
|
||||
<string name="your_contacts_will_remain_connected">Tus contactos permanecerán conectados.</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Si más tarde decides borrar tu dirección los contactos no se perderán.</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">Si más tarde decides eliminar tu dirección los contactos no se perderán.</string>
|
||||
<string name="all_app_data_will_be_cleared">Todos los datos de la aplicación se eliminarán.</string>
|
||||
<string name="empty_chat_profile_is_created">Se creará un perfil vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre.</string>
|
||||
<string name="self_destruct">Autodestrucción</string>
|
||||
@@ -1213,19 +1213,19 @@
|
||||
<string name="send_disappearing_message">Enviar mensaje temporal</string>
|
||||
<string name="send_disappearing_message_custom_time">Tiempo personalizado</string>
|
||||
<string name="send_disappearing_message_send">Enviar</string>
|
||||
<string name="info_row_moderated_at">Moderado el</string>
|
||||
<string name="info_row_received_at">Recibido el</string>
|
||||
<string name="info_row_sent_at">Enviado el</string>
|
||||
<string name="info_row_moderated_at">Moderado</string>
|
||||
<string name="info_row_received_at">Recibido</string>
|
||||
<string name="info_row_sent_at">Enviado</string>
|
||||
<string name="item_info_current">(actual)</string>
|
||||
<string name="share_text_database_id">ID de base de datos: %d</string>
|
||||
<string name="share_text_deleted_at">Borrado el: %s</string>
|
||||
<string name="share_text_disappears_at">Desaparecerá el: %s</string>
|
||||
<string name="share_text_moderated_at">Moderado el: %s</string>
|
||||
<string name="share_text_received_at">Recibido el: %s</string>
|
||||
<string name="share_text_updated_at">Registro actualizado: %s</string>
|
||||
<string name="share_text_database_id">ID base de datos: %d</string>
|
||||
<string name="share_text_deleted_at">Eliminado: %s</string>
|
||||
<string name="share_text_disappears_at">Desaparecerá: %s</string>
|
||||
<string name="share_text_moderated_at">Moderado: %s</string>
|
||||
<string name="share_text_received_at">Recibido: %s</string>
|
||||
<string name="share_text_updated_at">Registro actualiz: %s</string>
|
||||
<string name="current_version_timestamp">%s (actual)</string>
|
||||
<string name="share_text_sent_at">Enviado el: %s</string>
|
||||
<string name="info_row_disappears_at">Desaparecerá el</string>
|
||||
<string name="share_text_sent_at">Enviado: %s</string>
|
||||
<string name="info_row_disappears_at">Desaparecerá</string>
|
||||
<string name="message_reactions">Reacciones a los mensajes</string>
|
||||
<string name="prohibit_message_reactions">No se permiten reacciones a los mensajes.</string>
|
||||
<string name="allow_message_reactions">Permitir reacciones a los mensajes.</string>
|
||||
@@ -1243,11 +1243,11 @@
|
||||
<string name="message_reactions_are_prohibited">Las reacciones a los mensajes no están permitidas en este grupo.</string>
|
||||
<string name="only_your_contact_can_add_message_reactions">Sólo tu contacto puede añadir reacciones a los mensajes.</string>
|
||||
<string name="send_disappearing_message_1_minute">1 minuto</string>
|
||||
<string name="info_row_updated_at">Registro actualizado</string>
|
||||
<string name="info_row_updated_at">Registro actualiz</string>
|
||||
<string name="send_disappearing_message_30_seconds">30 segundos</string>
|
||||
<string name="send_disappearing_message_5_minutes">5 minutos</string>
|
||||
<string name="allow_your_contacts_adding_message_reactions">Permitir que tus contactos añadan reacciones a los mensajes.</string>
|
||||
<string name="info_row_deleted_at">Eliminado el</string>
|
||||
<string name="info_row_deleted_at">Eliminado</string>
|
||||
<string name="allow_message_reactions_only_if">Se permiten las reacciones a los mensajes pero sólo si tu contacto también las permite.</string>
|
||||
<string name="v5_1_custom_themes">Temas personalizados</string>
|
||||
<string name="v5_1_self_destruct_passcode">Código de autodestrucción</string>
|
||||
@@ -1256,10 +1256,11 @@
|
||||
\n- historial de edición.</string>
|
||||
<string name="whats_new_thanks_to_users_contribute_weblate">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string>
|
||||
<string name="v5_1_better_messages">Mensajes mejorados</string>
|
||||
<string name="v5_1_self_destruct_passcode_descr">Al introducirlo todos los datos son borrados.</string>
|
||||
<string name="v5_1_self_destruct_passcode_descr">Al introducirlo todos los datos son eliminados.</string>
|
||||
<string name="v5_1_custom_themes_descr">Personalizar y compartir temas de color.</string>
|
||||
<string name="v5_1_message_reactions_descr">¡Por fin los tenemos! 🚀</string>
|
||||
<string name="v5_1_message_reactions">Reacciones a los mensajes</string>
|
||||
<string name="whats_new_read_more">Saber más</string>
|
||||
<string name="v5_1_japanese_portuguese_interface">Interfaz en japonés y portugués</string>
|
||||
<string name="item_info_no_text">sin texto</string>
|
||||
</resources>
|
||||
@@ -1247,8 +1247,8 @@
|
||||
<string name="info_row_sent_at">Envoyé le</string>
|
||||
<string name="share_text_database_id">ID de base de données : %d</string>
|
||||
<string name="share_text_deleted_at">Supprimé à : %s</string>
|
||||
<string name="share_text_disappears_at">Disparaît à : %s</string>
|
||||
<string name="info_row_disappears_at">Disparaît à</string>
|
||||
<string name="share_text_disappears_at">Disparaîtra le : %s</string>
|
||||
<string name="info_row_disappears_at">Disparaîtra le</string>
|
||||
<string name="v5_1_japanese_portuguese_interface">Interface en japonais et en portugais</string>
|
||||
<string name="whats_new_thanks_to_users_contribute_weblate">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
<string name="v5_1_message_reactions_descr">Enfin, les voilà ! 🚀</string>
|
||||
@@ -1261,4 +1261,5 @@
|
||||
<string name="v5_1_self_destruct_passcode">Code d\'autodestruction</string>
|
||||
<string name="share_text_sent_at">Envoyé le : %s</string>
|
||||
<string name="sent_message">Message envoyé</string>
|
||||
<string name="item_info_no_text">aucun texte</string>
|
||||
</resources>
|
||||
@@ -910,7 +910,7 @@
|
||||
<string name="this_string_is_not_a_connection_link">Deze string is geen verbinding link!</string>
|
||||
<string name="enable_automatic_deletion_message">Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren.</string>
|
||||
<string name="switch_receiving_address_desc">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.</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Om je privacy te behouden, heeft de app in plaats van push meldingen een <b><xliff:g id="appName">SimpleX</xliff:g> achtergrondservice</b> – deze gebruikt een paar procent van de batterij per dag.</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Om je privacy te behouden, heeft de app in plaats van push meldingen een <b><xliff:g id="appName">SimpleX</xliff:g> achtergrond service</b> - deze gebruikt een paar procent van de batterij per dag.</string>
|
||||
<string name="chat_preferences_you_allow">Jij staat toe</string>
|
||||
<string name="you_are_invited_to_group">Je bent uitgenodigd voor de groep</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">U kunt <font color="#0088ff">verbinding maken met <xliff:g id="appNameFull">SimpleX Chat</xliff:g> ontwikkelaars om vragen te stellen en updates te ontvangen</font>.</string>
|
||||
@@ -1260,4 +1260,5 @@
|
||||
<string name="v5_1_better_messages_descr">- spraakberichten tot 5 minuten.
|
||||
\n- aangepaste tijd om te verdwijnen.
|
||||
\n- bewerkingsgeschiedenis.</string>
|
||||
<string name="item_info_no_text">geen tekst</string>
|
||||
</resources>
|
||||
@@ -446,7 +446,7 @@
|
||||
<string name="use_chat">Użyj czatu</string>
|
||||
<string name="onboarding_notifications_mode_off">Gdy aplikacja jest uruchomiona</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Kontrolujesz przez który serwer(y) <b>odbierać</b> wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Zużywa więcej baterii</b>! Usługa w tle jest zawsze uruchomiona - powiadomienia będą wyświetlane, od razu gdy wiadomości będą dostępne.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Zużywa więcej baterii</b>! Usługa zawsze działa w tle - powiadomienia są wyświetlane, gdy tylko wiadomości są dostępne.</string>
|
||||
<string name="incoming_audio_call">Przychodzące połączenie audio</string>
|
||||
<string name="incoming_video_call">Przychodzące połączenie wideo</string>
|
||||
<string name="paste_the_link_you_received">Wklej otrzymany link</string>
|
||||
@@ -925,8 +925,8 @@
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Dodaj nowy kontakt</b>: aby stworzyć swój jednorazowy kod QR dla kontaktu.</string>
|
||||
<string name="turning_off_service_and_periodic">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ń.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Można je wyłączyć poprzez ustawienia</b> - powiadomienia nadal będą pokazywane podczas działania aplikacji.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Najlepsze dla baterii</b>. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona, usługi w tle NIE będą używane.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Dobre dla baterii</b>. Usługa w tle sprawdza nowe wiadomości co 10 minut. Możesz przegapić połączenia i pilne wiadomości.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Najlepsze dla baterii</b>. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona (NIE w tle).</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Dobry dla baterii</b>. Usługa w tle sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe.</string>
|
||||
<string name="v4_5_transport_isolation_descr">Według profilu czatu (domyślnie) lub połączenia (BETA).</string>
|
||||
<string name="alert_title_cant_invite_contacts">Nie można zaprosić kontaktów!</string>
|
||||
@@ -1261,4 +1261,5 @@
|
||||
<string name="v5_1_better_messages_descr">- wiadomości głosowe do 5 minut.
|
||||
\n- niestandardowy czas zniknięcia.
|
||||
\n- historia edycji.</string>
|
||||
<string name="item_info_no_text">brak tekstu</string>
|
||||
</resources>
|
||||
@@ -733,7 +733,7 @@
|
||||
<string name="above_then_preposition_continuation">acima, então:</string>
|
||||
<string name="to_connect_via_link_title">Para conectar via link</string>
|
||||
<string name="image_descr_qr_code">QR Code</string>
|
||||
<string name="mark_code_verified">Marcado como verificado</string>
|
||||
<string name="mark_code_verified">Marcar como verificado</string>
|
||||
<string name="to_verify_compare">Para verificar a criptografia de ponta-a-ponta com seu contato, compare (ou escaneie) o código em seus dispositivos.</string>
|
||||
<string name="smp_save_servers_question">Salvar servidores\?</string>
|
||||
<string name="smp_servers_per_user">Os servidores para novas conexões do seu perfil de chat atual</string>
|
||||
@@ -807,7 +807,7 @@
|
||||
<string name="no_details">sem detalhes</string>
|
||||
<string name="add_contact">Link de convite de uso único</string>
|
||||
<string name="smp_servers_your_server_address">Seu endereço de servidor</string>
|
||||
<string name="your_settings">Suas configurações</string>
|
||||
<string name="your_settings">Configurações</string>
|
||||
<string name="is_verified">%s é verificado</string>
|
||||
<string name="is_not_verified">%s não é verificado</string>
|
||||
<string name="add_contact_or_create_group">Começar novo chat</string>
|
||||
@@ -1258,4 +1258,5 @@
|
||||
<string name="info_row_sent_at">Enviado em</string>
|
||||
<string name="change_self_destruct_passcode">Alterar senha de auto-destruição</string>
|
||||
<string name="if_you_enter_self_destruct_code">Se você digitar sua senha de auto-destruição ao abrir o aplicativo:</string>
|
||||
<string name="item_info_no_text">Sem texto</string>
|
||||
</resources>
|
||||
@@ -159,8 +159,8 @@
|
||||
<string name="change_member_role_question">更改群组角色?</string>
|
||||
<string name="icon_descr_cancel_link_preview">取消链接预览</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">正在为 %s 更改地址……</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">更改地址……</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">正在更改地址……</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">更改地址中……</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">更改地址中……</string>
|
||||
<string name="create_your_profile">创建您的资料</string>
|
||||
<string name="chat_database_deleted">聊天数据库已删除</string>
|
||||
<string name="chat_database_imported">聊天数据库已导入</string>
|
||||
@@ -325,7 +325,7 @@
|
||||
<string name="callstatus_connecting">连接通话中……</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">联系人和所有的消息都将被删除——这是不可逆回的!</string>
|
||||
<string name="notification_preview_mode_contact">联系人姓名</string>
|
||||
<string name="group_member_status_intro_invitation">连接(介绍邀请)</string>
|
||||
<string name="group_member_status_intro_invitation">连接中(介绍邀请)</string>
|
||||
<string name="callstate_connecting">连接中……</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">联系人可以将信息标记为删除;您将可以查看这些信息。</string>
|
||||
<string name="contribute">贡献</string>
|
||||
@@ -847,7 +847,7 @@
|
||||
<string name="use_chat">使用聊天</string>
|
||||
<string name="read_more_in_github_with_link">在我们的 <font color="#0088ff">GitHub 存储库</font> 中阅读更多内容。</string>
|
||||
<string name="auth_open_chat_console">打开聊天控制台</string>
|
||||
<string name="auth_stop_chat">Stop chat</string>
|
||||
<string name="auth_stop_chat">停止聊天程序</string>
|
||||
<string name="toast_permission_denied">权限被拒绝!</string>
|
||||
<string name="chat_help_tap_button">点击按钮</string>
|
||||
<string name="thank_you_for_installing_simplex">感谢您安装 <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
@@ -1056,8 +1056,8 @@
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">确保 XFTP 服务器地址格式正确、行分隔且不重复。</string>
|
||||
<string name="smp_server_test_create_file">创建文件</string>
|
||||
<string name="smp_server_test_download_file">下载文件</string>
|
||||
<string name="error_loading_smp_servers">加载 SMP 服务器时出错</string>
|
||||
<string name="error_loading_xftp_servers">加载 XFTP 服务器时出错</string>
|
||||
<string name="error_loading_smp_servers">加载 SMP 服务器错误</string>
|
||||
<string name="error_loading_xftp_servers">加载 XFTP 服务器错误</string>
|
||||
<string name="error_saving_xftp_servers">保存 SMP 服务器时出错</string>
|
||||
<string name="network_proxy_port">端口 %d</string>
|
||||
<string name="network_socks_proxy_settings">SOCKS 代理设置</string>
|
||||
@@ -1108,7 +1108,7 @@
|
||||
<string name="stop_snd_file__message">即将停止发送文件。</string>
|
||||
<string name="stop_rcv_file__title">停止接收文件?</string>
|
||||
<string name="stop_rcv_file__message">即将停止接收文件。</string>
|
||||
<string name="stop_file__confirm">Stop</string>
|
||||
<string name="stop_file__confirm">停止</string>
|
||||
<string name="revoke_file__action">撤销文件</string>
|
||||
<string name="revoke_file__title">撤销文件?</string>
|
||||
<string name="revoke_file__message">文件将从服务器中删除。</string>
|
||||
@@ -1159,7 +1159,7 @@
|
||||
<string name="enabled_self_destruct_passcode">启用自毁密码</string>
|
||||
<string name="self_destruct">自毁</string>
|
||||
<string name="share_text_database_id">数据库 ID:%d</string>
|
||||
<string name="info_row_disappears_at">消失在</string>
|
||||
<string name="info_row_disappears_at">消失于</string>
|
||||
<string name="error_setting_address">设置地址错误</string>
|
||||
<string name="customize_theme_title">自定义主题</string>
|
||||
<string name="create_address_and_let_people_connect">创建一个地址,让人们与您联系。</string>
|
||||
@@ -1176,7 +1176,90 @@
|
||||
<string name="custom_time_unit_days">天</string>
|
||||
<string name="custom_time_picker_custom">自定义</string>
|
||||
<string name="item_info_current">(当前)</string>
|
||||
<string name="info_row_deleted_at">已删除在</string>
|
||||
<string name="share_text_deleted_at">已删除在:%s</string>
|
||||
<string name="share_text_disappears_at">消失在:%s</string>
|
||||
<string name="info_row_deleted_at">已删除于</string>
|
||||
<string name="share_text_deleted_at">已删除于:%s</string>
|
||||
<string name="share_text_disappears_at">消失于:%s</string>
|
||||
<string name="prohibit_message_reactions">禁止消息回应。</string>
|
||||
<string name="only_you_can_add_message_reactions">只有您可以添加消息回应。</string>
|
||||
<string name="group_members_can_add_message_reactions">群组成员可以添加信息回应。</string>
|
||||
<string name="message_reactions_are_prohibited">该群组禁用了消息回应。</string>
|
||||
<string name="self_destruct_passcode_changed">自毁密码已更改!</string>
|
||||
<string name="self_destruct_passcode_enabled">自毁密码已启用!</string>
|
||||
<string name="set_passcode">设置密码</string>
|
||||
<string name="info_menu">信息</string>
|
||||
<string name="sent_message">已发信息</string>
|
||||
<string name="edit_history">历史记录</string>
|
||||
<string name="send_disappearing_message_send">发送</string>
|
||||
<string name="if_you_enter_passcode_data_removed">如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!</string>
|
||||
<string name="self_destruct_new_display_name">新的显示名:</string>
|
||||
<string name="info_row_moderated_at">已被管理员移除于</string>
|
||||
<string name="info_row_sent_at">已发送于</string>
|
||||
<string name="share_text_received_at">已收到于: %s</string>
|
||||
<string name="share_text_updated_at">记录更新于: %s</string>
|
||||
<string name="share_text_moderated_at">已被管理员移除于: %s</string>
|
||||
<string name="current_version_timestamp">%s (当前)</string>
|
||||
<string name="share_text_sent_at">发送于 %s</string>
|
||||
<string name="color_received_message">收到的信息</string>
|
||||
<string name="only_your_contact_can_add_message_reactions">只有您的联系人可以添加消息回应。</string>
|
||||
<string name="opening_database">打开数据库中……</string>
|
||||
<string name="auth_open_chat_profiles">打开聊天资料</string>
|
||||
<string name="scan_qr_to_connect_to_contact">您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。</string>
|
||||
<string name="you_can_share_your_address">您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。</string>
|
||||
<string name="if_you_cant_meet_in_person">如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。</string>
|
||||
<string name="learn_more">了解更多</string>
|
||||
<string name="you_can_accept_or_reject_connection">当人们请求连接时,您可以接受或拒绝它。</string>
|
||||
<string name="you_wont_lose_your_contacts_if_delete_address">如果您以后删除您的地址,您不会丢失您的联系人。</string>
|
||||
<string name="read_more_in_user_guide_with_link">在<font color="#0088ff">用户指南</font>中阅读更多。</string>
|
||||
<string name="theme_colors_section_title">主题颜色</string>
|
||||
<string name="your_contacts_will_remain_connected">与您的联系人保持连接。</string>
|
||||
<string name="share_with_contacts">与联系人分享</string>
|
||||
<string name="invite_friends">邀请朋友</string>
|
||||
<string name="save_auto_accept_settings">保存自动接受设置</string>
|
||||
<string name="save_settings_question">保存设置?</string>
|
||||
<string name="stop_sharing">停止分享</string>
|
||||
<string name="email_invite_body">你好!
|
||||
\n用 SimpleX Chat 与我联系:%s</string>
|
||||
<string name="email_invite_subject">让我们一起在 SimpleX Chat 里聊天</string>
|
||||
<string name="your_contacts_will_see_it">您的 SimpleX 的联系人会看到它。
|
||||
\n您可以在设置中更改它。</string>
|
||||
<string name="you_can_create_it_later">您可以以后创建它</string>
|
||||
<string name="share_address">分享地址</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">您可以与您的联系人分享该地址,让他们与 %s 联系。</string>
|
||||
<string name="group_welcome_preview">预览</string>
|
||||
<string name="import_theme">导入主题</string>
|
||||
<string name="theme_simplex">SimpleX</string>
|
||||
<string name="import_theme_error_desc">请确保该文件具有正确的YAML语法。导出主题,以便有一个主题文件结构的例子。</string>
|
||||
<string name="color_secondary">第二选项</string>
|
||||
<string name="color_title">标题</string>
|
||||
<string name="whats_new_read_more">阅读更多</string>
|
||||
<string name="v5_1_message_reactions_descr">终于我们有它们了! 🚀</string>
|
||||
<string name="v5_1_message_reactions">消息回应</string>
|
||||
<string name="v5_1_self_destruct_passcode">自毁密码</string>
|
||||
<string name="custom_time_unit_hours">小时</string>
|
||||
<string name="whats_new_thanks_to_users_contribute_weblate">感谢用户——通过 Weblate 做出贡献!</string>
|
||||
<string name="v5_1_japanese_portuguese_interface">日语和葡萄牙语用户界面</string>
|
||||
<string name="custom_time_unit_minutes">分钟</string>
|
||||
<string name="custom_time_unit_seconds">秒</string>
|
||||
<string name="custom_time_unit_months">月</string>
|
||||
<string name="custom_time_picker_select">选择</string>
|
||||
<string name="custom_time_unit_weeks">周</string>
|
||||
<string name="item_info_no_text">无文本</string>
|
||||
<string name="import_theme_error">导入主题错误</string>
|
||||
<string name="color_surface">菜单和警报</string>
|
||||
<string name="color_sent_message">已发信息</string>
|
||||
<string name="message_reactions">消息回应</string>
|
||||
<string name="message_reactions_prohibited_in_this_chat">该聊天禁用了消息回应。</string>
|
||||
<string name="if_you_enter_self_destruct_code">如果您在打开应用程序时输入自毁密码:</string>
|
||||
<string name="profile_update_will_be_sent_to_contacts">个人资料更新将被发送给您的联系人。</string>
|
||||
<string name="info_row_updated_at">记录更新于</string>
|
||||
<string name="prohibit_message_reactions_group">禁止消息回应。</string>
|
||||
<string name="info_row_received_at">已收到于</string>
|
||||
<string name="received_message">收到的信息</string>
|
||||
<string name="send_disappearing_message">发送限时消息中</string>
|
||||
<string name="share_address_with_contacts_question">与联系人分享地址?</string>
|
||||
<string name="stop_sharing_address">停止分享地址?</string>
|
||||
<string name="simplex_address">SimpleX 地址</string>
|
||||
<string name="v5_1_better_messages_descr">- 语音消息最长5分钟。
|
||||
\n- 自定义限时消息。
|
||||
\n- 编辑消息历史。</string>
|
||||
</resources>
|
||||
@@ -892,6 +892,7 @@
|
||||
<string name="error_importing_database">Error importing chat database</string>
|
||||
<string name="chat_database_imported">Chat database imported</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Restart the app to use imported chat database.</string>
|
||||
<string name="non_fatal_errors_occured_during_import">Some non-fatal errors occurred during import - you may see Chat console for more details.</string>
|
||||
<string name="delete_chat_profile_question">Delete chat profile?</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</string>
|
||||
<string name="chat_database_deleted">Chat database deleted</string>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5498,6 +5498,7 @@ Servery SimpleX nevidí váš profil.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="no text" xml:space="preserve">
|
||||
<source>no text</source>
|
||||
<target>žádný text</target>
|
||||
<note>copied message info in history</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="observer" xml:space="preserve">
|
||||
|
||||
@@ -5498,6 +5498,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="no text" xml:space="preserve">
|
||||
<source>no text</source>
|
||||
<target>Kein Text</target>
|
||||
<note>copied message info in history</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="observer" xml:space="preserve">
|
||||
|
||||
@@ -503,7 +503,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="All data is erased when it is entered." xml:space="preserve">
|
||||
<source>All data is erased when it is entered.</source>
|
||||
<target>Al introducirlo todos los datos son borrados.</target>
|
||||
<target>Al introducirlo todos los datos son eliminados.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All group members will remain connected." xml:space="preserve">
|
||||
@@ -1019,7 +1019,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via link / QR code" xml:space="preserve">
|
||||
<source>Connect via link / QR code</source>
|
||||
<target>Conectar mediante enlace / Código QR</target>
|
||||
<target>Conecta vía enlace / Código QR</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via one-time link?" xml:space="preserve">
|
||||
@@ -1099,7 +1099,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact name" xml:space="preserve">
|
||||
<source>Contact name</source>
|
||||
<target>Nombre del contacto</target>
|
||||
<target>Contacto</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact preferences" xml:space="preserve">
|
||||
@@ -1159,7 +1159,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Crear enlace de invitación de un solo uso</target>
|
||||
<target>Crea enlace de invitación de un uso</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create queue" xml:space="preserve">
|
||||
@@ -1169,7 +1169,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Create secret group" xml:space="preserve">
|
||||
<source>Create secret group</source>
|
||||
<target>Crear grupo secreto</target>
|
||||
<target>Crea grupo secreto</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create your profile" xml:space="preserve">
|
||||
@@ -1209,12 +1209,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Database ID" xml:space="preserve">
|
||||
<source>Database ID</source>
|
||||
<target>ID de base de datos</target>
|
||||
<target>ID base de datos</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database ID: %d" xml:space="preserve">
|
||||
<source>Database ID: %d</source>
|
||||
<target>ID de base de datos: %d</target>
|
||||
<target>ID base de datos: %d</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database IDs and Transport isolation option." xml:space="preserve">
|
||||
@@ -1337,7 +1337,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete after" xml:space="preserve">
|
||||
<source>Delete after</source>
|
||||
<target>Eliminar después de</target>
|
||||
<target>Eliminar en</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete all files" xml:space="preserve">
|
||||
@@ -1452,7 +1452,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete messages after" xml:space="preserve">
|
||||
<source>Delete messages after</source>
|
||||
<target>Eliminar después de</target>
|
||||
<target>Eliminar en</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete old database" xml:space="preserve">
|
||||
@@ -1472,7 +1472,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete pending connection?" xml:space="preserve">
|
||||
<source>Delete pending connection?</source>
|
||||
<target>¿Eliminar conexion pendiente?</target>
|
||||
<target>¿Eliminar la conexion pendiente?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete profile" xml:space="preserve">
|
||||
@@ -1492,12 +1492,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Deleted at" xml:space="preserve">
|
||||
<source>Deleted at</source>
|
||||
<target>Eliminado el</target>
|
||||
<target>Eliminado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Deleted at: %@" xml:space="preserve">
|
||||
<source>Deleted at: %@</source>
|
||||
<target>Eliminado el: %@</target>
|
||||
<target>Eliminado: %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Description" xml:space="preserve">
|
||||
@@ -1572,12 +1572,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at" xml:space="preserve">
|
||||
<source>Disappears at</source>
|
||||
<target>Desaparecerá el</target>
|
||||
<target>Desaparecerá</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at: %@" xml:space="preserve">
|
||||
<source>Disappears at: %@</source>
|
||||
<target>Desaparecerá el: %@</target>
|
||||
<target>Desaparecerá: %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -2237,12 +2237,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Hidden profile password" xml:space="preserve">
|
||||
<source>Hidden profile password</source>
|
||||
<target>Contraseña de perfil oculto</target>
|
||||
<target>Contraseña del perfil oculto</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hide" xml:space="preserve">
|
||||
<source>Hide</source>
|
||||
<target>Ocultar</target>
|
||||
<target>Sin vista previa</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hide app screen in the recent apps." xml:space="preserve">
|
||||
@@ -2630,12 +2630,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Lock after" xml:space="preserve">
|
||||
<source>Lock after</source>
|
||||
<target>Bloquear después de</target>
|
||||
<target>Bloquear en</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Lock mode" xml:space="preserve">
|
||||
<source>Lock mode</source>
|
||||
<target>Modo de bloqueo</target>
|
||||
<target>Modo bloqueo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Make a private connection" xml:space="preserve">
|
||||
@@ -2735,7 +2735,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Message text" xml:space="preserve">
|
||||
<source>Message text</source>
|
||||
<target>Texto del mensaje</target>
|
||||
<target>Contacto y texto</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Messages" xml:space="preserve">
|
||||
@@ -2780,12 +2780,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderated at" xml:space="preserve">
|
||||
<source>Moderated at</source>
|
||||
<target>Moderado el</target>
|
||||
<target>Moderado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderated at: %@" xml:space="preserve">
|
||||
<source>Moderated at: %@</source>
|
||||
<target>Moderado el: %@</target>
|
||||
<target>Moderado: %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="More improvements are coming soon!" xml:space="preserve">
|
||||
@@ -2933,8 +2933,8 @@
|
||||
- delete members' messages.
|
||||
- disable members ("observer" role)</source>
|
||||
<target>Ahora los administradores pueden:
|
||||
- borrar mensajes de los miembros.
|
||||
- desactivar el rol a miembros (pasando a rol "observador")</target>
|
||||
- eliminar mensajes de los miembros.
|
||||
- desactivar el rol miembro (a rol "observador")</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Off" xml:space="preserve">
|
||||
@@ -3379,12 +3379,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Received at" xml:space="preserve">
|
||||
<source>Received at</source>
|
||||
<target>Recibido el</target>
|
||||
<target>Recibido</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Received at: %@" xml:space="preserve">
|
||||
<source>Received at: %@</source>
|
||||
<target>Recibido el: %@</target>
|
||||
<target>Recibido: %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Received file event" xml:space="preserve">
|
||||
@@ -3414,12 +3414,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Record updated at" xml:space="preserve">
|
||||
<source>Record updated at</source>
|
||||
<target>Registro actualizado</target>
|
||||
<target>Registro actualiz.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Record updated at: %@" xml:space="preserve">
|
||||
<source>Record updated at: %@</source>
|
||||
<target>Registro actualizado: %@</target>
|
||||
<target>Registro actualiz: %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reduced battery usage" xml:space="preserve">
|
||||
@@ -3789,12 +3789,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent at" xml:space="preserve">
|
||||
<source>Sent at</source>
|
||||
<target>Enviado el</target>
|
||||
<target>Enviado</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent at: %@" xml:space="preserve">
|
||||
<source>Sent at: %@</source>
|
||||
<target>Enviado el: %@</target>
|
||||
<target>Enviado: %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent file event" xml:space="preserve">
|
||||
@@ -4353,7 +4353,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t
|
||||
</trans-unit>
|
||||
<trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve">
|
||||
<source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source>
|
||||
<target>Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Tus perfiles Chat**.</target>
|
||||
<target>Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Mis perfiles**.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve">
|
||||
@@ -4676,7 +4676,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb
|
||||
</trans-unit>
|
||||
<trans-unit id="When available" xml:space="preserve">
|
||||
<source>When available</source>
|
||||
<target>Si disponible</target>
|
||||
<target>Si disponibles</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve">
|
||||
@@ -4803,7 +4803,7 @@ Bloqueo SimpleX debe estar activado.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can use markdown to format messages:" xml:space="preserve">
|
||||
<source>You can use markdown to format messages:</source>
|
||||
<target>Puedes usar la sintaxis markdown para dar formato a los mensajes:</target>
|
||||
<target>Puedes usar la sintaxis markdown para dar formato a tus mensajes:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can't send messages!" xml:space="preserve">
|
||||
@@ -5089,7 +5089,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="above, then choose:" xml:space="preserve">
|
||||
<source>above, then choose:</source>
|
||||
<target>arriba y luego elige:</target>
|
||||
<target>y después elige:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="accepted call" xml:space="preserve">
|
||||
@@ -5499,6 +5499,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="no text" xml:space="preserve">
|
||||
<source>no text</source>
|
||||
<target>sin texto</target>
|
||||
<note>copied message info in history</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="observer" xml:space="preserve">
|
||||
|
||||
@@ -1572,12 +1572,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at" xml:space="preserve">
|
||||
<source>Disappears at</source>
|
||||
<target>Disparaît à</target>
|
||||
<target>Disparaîtra le</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at: %@" xml:space="preserve">
|
||||
<source>Disappears at: %@</source>
|
||||
<target>Disparaît à : %@</target>
|
||||
<target>Disparaîtra le : %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -5498,6 +5498,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="no text" xml:space="preserve">
|
||||
<source>no text</source>
|
||||
<target>aucun texte</target>
|
||||
<note>copied message info in history</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="observer" xml:space="preserve">
|
||||
|
||||
@@ -5497,6 +5497,7 @@ SimpleX servers kunnen uw profiel niet zien.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="no text" xml:space="preserve">
|
||||
<source>no text</source>
|
||||
<target>geen tekst</target>
|
||||
<note>copied message info in history</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="observer" xml:space="preserve">
|
||||
|
||||
@@ -5498,6 +5498,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="no text" xml:space="preserve">
|
||||
<source>no text</source>
|
||||
<target>brak tekstu</target>
|
||||
<note>copied message info in history</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="observer" xml:space="preserve">
|
||||
|
||||
@@ -59,10 +59,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ (current)" xml:space="preserve">
|
||||
<source>%@ (current)</source>
|
||||
<target>%@(当前)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ (current):" xml:space="preserve">
|
||||
<source>%@ (current):</source>
|
||||
<target>%@(当前):</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%@ / %@" xml:space="preserve">
|
||||
@@ -97,6 +99,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%@:" xml:space="preserve">
|
||||
<source>%@:</source>
|
||||
<target>%@:</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%d days" xml:space="preserve">
|
||||
@@ -298,6 +301,9 @@
|
||||
<source>- voice messages up to 5 minutes.
|
||||
- custom time to disappear.
|
||||
- editing history.</source>
|
||||
<target>- 语音消息最长5分钟。
|
||||
- 自定义限时消息。
|
||||
- 编辑消息历史。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="." xml:space="preserve">
|
||||
@@ -307,6 +313,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="0s" xml:space="preserve">
|
||||
<source>0s</source>
|
||||
<target>0秒</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 day" xml:space="preserve">
|
||||
@@ -341,6 +348,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="5 minutes" xml:space="preserve">
|
||||
<source>5 minutes</source>
|
||||
<target>5分钟</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="6" xml:space="preserve">
|
||||
@@ -350,6 +358,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="30 seconds" xml:space="preserve">
|
||||
<source>30 seconds</source>
|
||||
<target>30秒</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id=": " xml:space="preserve">
|
||||
@@ -360,6 +369,8 @@
|
||||
<trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve">
|
||||
<source><p>Hi!</p>
|
||||
<p><a href="%@">Connect to me via SimpleX Chat</a></p></source>
|
||||
<target><p>你好!</p>
|
||||
<p><a href="%@">通过 SimpleX Chat </a></p>与我联系</target>
|
||||
<note>email text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="A new contact" xml:space="preserve">
|
||||
@@ -401,6 +412,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="About SimpleX address" xml:space="preserve">
|
||||
<source>About SimpleX address</source>
|
||||
<target>关于 SimpleX 地址</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accent color" xml:space="preserve">
|
||||
@@ -431,6 +443,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve">
|
||||
<source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source>
|
||||
<target>将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add preset servers" xml:space="preserve">
|
||||
@@ -465,6 +478,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Address" xml:space="preserve">
|
||||
<source>Address</source>
|
||||
<target>地址</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Admins can create the links to join groups." xml:space="preserve">
|
||||
@@ -479,6 +493,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="All app data is deleted." xml:space="preserve">
|
||||
<source>All app data is deleted.</source>
|
||||
<target>已删除所有应用程序数据。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve">
|
||||
@@ -488,6 +503,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="All data is erased when it is entered." xml:space="preserve">
|
||||
<source>All data is erased when it is entered.</source>
|
||||
<target>所有数据在输入后将被删除。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All group members will remain connected." xml:space="preserve">
|
||||
@@ -507,6 +523,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve">
|
||||
<source>All your contacts will remain connected. Profile update will be sent to your contacts.</source>
|
||||
<target>您的所有联系人将保持连接。个人资料更新将发送给您的联系人。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow" xml:space="preserve">
|
||||
@@ -531,10 +548,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve">
|
||||
<source>Allow message reactions only if your contact allows them.</source>
|
||||
<target>只有您的联系人允许时才允许消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow message reactions." xml:space="preserve">
|
||||
<source>Allow message reactions.</source>
|
||||
<target>允许消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow sending direct messages to members." xml:space="preserve">
|
||||
@@ -569,6 +588,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts adding message reactions." xml:space="preserve">
|
||||
<source>Allow your contacts adding message reactions.</source>
|
||||
<target>允许您的联系人添加消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Allow your contacts to call you." xml:space="preserve">
|
||||
@@ -603,6 +623,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve">
|
||||
<source>An empty chat profile with the provided name is created, and the app opens as usual.</source>
|
||||
<target>已创建一个包含所提供名字的空白聊天资料,应用程序照常打开。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Answer call" xml:space="preserve">
|
||||
@@ -627,6 +648,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve">
|
||||
<source>App passcode is replaced with self-destruct passcode.</source>
|
||||
<target>应用程序密码被替换为自毁密码。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="App version" xml:space="preserve">
|
||||
@@ -691,6 +713,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Auto-accept" xml:space="preserve">
|
||||
<source>Auto-accept</source>
|
||||
<target>自动接受</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Auto-accept contact requests" xml:space="preserve">
|
||||
@@ -720,10 +743,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Better messages" xml:space="preserve">
|
||||
<source>Better messages</source>
|
||||
<target>更好的消息</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can add message reactions." xml:space="preserve">
|
||||
<source>Both you and your contact can add message reactions.</source>
|
||||
<target>您和您的联系人都可以添加消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve">
|
||||
@@ -833,10 +858,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Change self-destruct mode" xml:space="preserve">
|
||||
<source>Change self-destruct mode</source>
|
||||
<target>更改自毁模式</target>
|
||||
<note>authentication reason</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change self-destruct passcode" xml:space="preserve">
|
||||
<source>Change self-destruct passcode</source>
|
||||
<target>更改自毁密码</target>
|
||||
<note>authentication reason
|
||||
set passcode view</note>
|
||||
</trans-unit>
|
||||
@@ -1087,6 +1114,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Continue" xml:space="preserve">
|
||||
<source>Continue</source>
|
||||
<target>继续</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
@@ -1106,10 +1134,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Create SimpleX address" xml:space="preserve">
|
||||
<source>Create SimpleX address</source>
|
||||
<target>创建 SimpleX 地址</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create an address to let people connect with you." xml:space="preserve">
|
||||
<source>Create an address to let people connect with you.</source>
|
||||
<target>创建一个地址,让人们与您联系。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create file" xml:space="preserve">
|
||||
@@ -1169,6 +1199,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Custom time" xml:space="preserve">
|
||||
<source>Custom time</source>
|
||||
<target>自定义时间</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Dark" xml:space="preserve">
|
||||
@@ -1183,6 +1214,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Database ID: %d" xml:space="preserve">
|
||||
<source>Database ID: %d</source>
|
||||
<target>数据库 ID:%d</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Database IDs and Transport isolation option." xml:space="preserve">
|
||||
@@ -1460,10 +1492,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Deleted at" xml:space="preserve">
|
||||
<source>Deleted at</source>
|
||||
<target>已删除于</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Deleted at: %@" xml:space="preserve">
|
||||
<source>Deleted at: %@</source>
|
||||
<target>已删除于:%@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Description" xml:space="preserve">
|
||||
@@ -1518,6 +1552,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing message" xml:space="preserve">
|
||||
<source>Disappearing message</source>
|
||||
<target>限时消息</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappearing messages" xml:space="preserve">
|
||||
@@ -1537,10 +1572,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at" xml:space="preserve">
|
||||
<source>Disappears at</source>
|
||||
<target>消失于</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at: %@" xml:space="preserve">
|
||||
<source>Disappears at: %@</source>
|
||||
<target>消失于:%@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -1570,6 +1607,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Don't create address" xml:space="preserve">
|
||||
<source>Don't create address</source>
|
||||
<target>不创建地址</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Don't show again" xml:space="preserve">
|
||||
@@ -1594,6 +1632,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Duration" xml:space="preserve">
|
||||
<source>Duration</source>
|
||||
<target>时长</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit" xml:space="preserve">
|
||||
@@ -1648,10 +1687,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable self-destruct" xml:space="preserve">
|
||||
<source>Enable self-destruct</source>
|
||||
<target>启用自毁功能</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable self-destruct passcode" xml:space="preserve">
|
||||
<source>Enable self-destruct passcode</source>
|
||||
<target>启用自毁密码</target>
|
||||
<note>set passcode view</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypt" xml:space="preserve">
|
||||
@@ -1726,10 +1767,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter welcome message…" xml:space="preserve">
|
||||
<source>Enter welcome message…</source>
|
||||
<target>输入欢迎消息……</target>
|
||||
<note>placeholder</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enter welcome message… (optional)" xml:space="preserve">
|
||||
<source>Enter welcome message… (optional)</source>
|
||||
<target>输入欢迎消息……(可选)</target>
|
||||
<note>placeholder</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error" xml:space="preserve">
|
||||
@@ -1899,6 +1942,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending email" xml:space="preserve">
|
||||
<source>Error sending email</source>
|
||||
<target>发送电邮错误</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error sending message" xml:space="preserve">
|
||||
@@ -2023,6 +2067,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Finally, we have them! 🚀" xml:space="preserve">
|
||||
<source>Finally, we have them! 🚀</source>
|
||||
<target>终于我们有它们了! 🚀</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="For console" xml:space="preserve">
|
||||
@@ -2112,6 +2157,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can add message reactions." xml:space="preserve">
|
||||
<source>Group members can add message reactions.</source>
|
||||
<target>群组成员可以添加信息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
|
||||
@@ -2216,6 +2262,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="History" xml:space="preserve">
|
||||
<source>History</source>
|
||||
<target>历史记录</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="How SimpleX works" xml:space="preserve">
|
||||
@@ -2250,6 +2297,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve">
|
||||
<source>If you can't meet in person, show QR code in a video call, or share the link.</source>
|
||||
<target>如果您不能亲自见面,可以在视频通话中展示二维码,或分享链接。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve">
|
||||
@@ -2259,10 +2307,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve">
|
||||
<source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source>
|
||||
<target>如果您在打开应用时输入该密码,所有应用程序数据将被不可撤回地删除!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve">
|
||||
<source>If you enter your self-destruct passcode while opening the app:</source>
|
||||
<target>如果您在打开应用程序时输入自毁密码:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="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)." xml:space="preserve">
|
||||
@@ -2372,6 +2422,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Info" xml:space="preserve">
|
||||
<source>Info</source>
|
||||
<target>信息</target>
|
||||
<note>chat item action</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Initial role" xml:space="preserve">
|
||||
@@ -2418,6 +2469,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Invite friends" xml:space="preserve">
|
||||
<source>Invite friends</source>
|
||||
<target>邀请朋友</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invite members" xml:space="preserve">
|
||||
@@ -2478,6 +2530,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Japanese interface" xml:space="preserve">
|
||||
<source>Japanese interface</source>
|
||||
<target>日语界面</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Join" xml:space="preserve">
|
||||
@@ -2522,6 +2575,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Learn more" xml:space="preserve">
|
||||
<source>Learn more</source>
|
||||
<target>了解更多</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Leave" xml:space="preserve">
|
||||
@@ -2541,6 +2595,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve">
|
||||
<source>Let's talk in SimpleX Chat</source>
|
||||
<target>让我们一起在 SimpleX Chat 里聊天</target>
|
||||
<note>email subject</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Light" xml:space="preserve">
|
||||
@@ -2665,14 +2720,17 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reactions" xml:space="preserve">
|
||||
<source>Message reactions</source>
|
||||
<target>消息回应</target>
|
||||
<note>chat feature</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve">
|
||||
<source>Message reactions are prohibited in this chat.</source>
|
||||
<target>该聊天禁用了消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message reactions are prohibited in this group." xml:space="preserve">
|
||||
<source>Message reactions are prohibited in this group.</source>
|
||||
<target>该群组禁用了消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message text" xml:space="preserve">
|
||||
@@ -2722,10 +2780,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderated at" xml:space="preserve">
|
||||
<source>Moderated at</source>
|
||||
<target>已被管理员移除于</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderated at: %@" xml:space="preserve">
|
||||
<source>Moderated at: %@</source>
|
||||
<target>已被管理员移除于:%@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="More improvements are coming soon!" xml:space="preserve">
|
||||
@@ -2795,6 +2855,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="New display name" xml:space="preserve">
|
||||
<source>New display name</source>
|
||||
<target>新显示名</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New in %@" xml:space="preserve">
|
||||
@@ -2938,6 +2999,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Only you can add message reactions." xml:space="preserve">
|
||||
<source>Only you can add message reactions.</source>
|
||||
<target>只有您可以添加消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve">
|
||||
@@ -2962,6 +3024,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Only your contact can add message reactions." xml:space="preserve">
|
||||
<source>Only your contact can add message reactions.</source>
|
||||
<target>只有您的联系人可以添加消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
|
||||
@@ -3011,6 +3074,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening database…" xml:space="preserve">
|
||||
<source>Opening database…</source>
|
||||
<target>打开数据库中……</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||
@@ -3180,6 +3244,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Preview" xml:space="preserve">
|
||||
<source>Preview</source>
|
||||
<target>预览</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Privacy & security" xml:space="preserve">
|
||||
@@ -3214,6 +3279,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Profile update will be sent to your contacts." xml:space="preserve">
|
||||
<source>Profile update will be sent to your contacts.</source>
|
||||
<target>个人资料更新将被发送给您的联系人。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit audio/video calls." xml:space="preserve">
|
||||
@@ -3228,10 +3294,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit message reactions." xml:space="preserve">
|
||||
<source>Prohibit message reactions.</source>
|
||||
<target>禁止消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit messages reactions." xml:space="preserve">
|
||||
<source>Prohibit messages reactions.</source>
|
||||
<target>禁止消息回应。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
||||
@@ -3276,6 +3344,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="React..." xml:space="preserve">
|
||||
<source>React...</source>
|
||||
<target>回应……</target>
|
||||
<note>chat item menu</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Read" xml:space="preserve">
|
||||
@@ -3285,14 +3354,17 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Read more" xml:space="preserve">
|
||||
<source>Read more</source>
|
||||
<target>阅读更多</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve">
|
||||
<source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source>
|
||||
<target>在 [用户指南](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address) 中阅读更多内容。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve">
|
||||
<source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source>
|
||||
<target>在 [用户指南](https://simplex.chat/docs/guide/readme.html#connect-to-friends) 中阅读更多内容。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Read more in our GitHub repository." xml:space="preserve">
|
||||
@@ -3307,10 +3379,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Received at" xml:space="preserve">
|
||||
<source>Received at</source>
|
||||
<target>已收到于</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Received at: %@" xml:space="preserve">
|
||||
<source>Received at: %@</source>
|
||||
<target>已收到于:%@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Received file event" xml:space="preserve">
|
||||
@@ -3320,6 +3394,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Received message" xml:space="preserve">
|
||||
<source>Received message</source>
|
||||
<target>收到的信息</target>
|
||||
<note>message info title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Receiving file will be stopped." xml:space="preserve">
|
||||
@@ -3339,10 +3414,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Record updated at" xml:space="preserve">
|
||||
<source>Record updated at</source>
|
||||
<target>记录更新于</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Record updated at: %@" xml:space="preserve">
|
||||
<source>Record updated at: %@</source>
|
||||
<target>记录更新于:%@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Reduced battery usage" xml:space="preserve">
|
||||
@@ -3522,6 +3599,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Save auto-accept settings" xml:space="preserve">
|
||||
<source>Save auto-accept settings</source>
|
||||
<target>保存自动接受设置</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save group profile" xml:space="preserve">
|
||||
@@ -3561,6 +3639,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Save settings?" xml:space="preserve">
|
||||
<source>Save settings?</source>
|
||||
<target>保存设置?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save welcome message?" xml:space="preserve">
|
||||
@@ -3615,22 +3694,27 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Select" xml:space="preserve">
|
||||
<source>Select</source>
|
||||
<target>选择</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Self-destruct" xml:space="preserve">
|
||||
<source>Self-destruct</source>
|
||||
<target>自毁</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Self-destruct passcode" xml:space="preserve">
|
||||
<source>Self-destruct passcode</source>
|
||||
<target>自毁密码</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Self-destruct passcode changed!" xml:space="preserve">
|
||||
<source>Self-destruct passcode changed!</source>
|
||||
<target>自毁密码已更改!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Self-destruct passcode enabled!" xml:space="preserve">
|
||||
<source>Self-destruct passcode enabled!</source>
|
||||
<target>自毁密码已启用!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send" xml:space="preserve">
|
||||
@@ -3650,6 +3734,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Send disappearing message" xml:space="preserve">
|
||||
<source>Send disappearing message</source>
|
||||
<target>发送限时消息中</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
@@ -3704,10 +3789,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent at" xml:space="preserve">
|
||||
<source>Sent at</source>
|
||||
<target>已发送于</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent at: %@" xml:space="preserve">
|
||||
<source>Sent at: %@</source>
|
||||
<target>已发送于:%@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent file event" xml:space="preserve">
|
||||
@@ -3717,6 +3804,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent message" xml:space="preserve">
|
||||
<source>Sent message</source>
|
||||
<target>已发信息</target>
|
||||
<note>message info title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent messages will be deleted after set time." xml:space="preserve">
|
||||
@@ -3766,6 +3854,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passcode" xml:space="preserve">
|
||||
<source>Set passcode</source>
|
||||
<target>设置密码</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Set passphrase to export" xml:space="preserve">
|
||||
@@ -3800,10 +3889,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Share address" xml:space="preserve">
|
||||
<source>Share address</source>
|
||||
<target>分享地址</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share address with contacts?" xml:space="preserve">
|
||||
<source>Share address with contacts?</source>
|
||||
<target>与联系人分享地址?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Share link" xml:space="preserve">
|
||||
@@ -3818,6 +3909,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Share with contacts" xml:space="preserve">
|
||||
<source>Share with contacts</source>
|
||||
<target>与联系人分享</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show calls in phone history" xml:space="preserve">
|
||||
@@ -3842,6 +3934,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX Address" xml:space="preserve">
|
||||
<source>SimpleX Address</source>
|
||||
<target>SimpleX 地址</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve">
|
||||
@@ -3871,6 +3964,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX address" xml:space="preserve">
|
||||
<source>SimpleX address</source>
|
||||
<target>SimpleX 地址</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="SimpleX contact address" xml:space="preserve">
|
||||
@@ -3970,10 +4064,12 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop sharing" xml:space="preserve">
|
||||
<source>Stop sharing</source>
|
||||
<target>停止分享</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Stop sharing address?" xml:space="preserve">
|
||||
<source>Stop sharing address?</source>
|
||||
<target>停止分享地址?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Submit" xml:space="preserve">
|
||||
@@ -4220,6 +4316,7 @@ It can happen because of some bug or when the connection is compromised.</source
|
||||
</trans-unit>
|
||||
<trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve">
|
||||
<source>To connect, your contact can scan QR code or use the link in the app.</source>
|
||||
<target>您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve">
|
||||
@@ -4331,6 +4428,7 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
</trans-unit>
|
||||
<trans-unit id="Unit" xml:space="preserve">
|
||||
<source>Unit</source>
|
||||
<target>单位</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unknown caller" xml:space="preserve">
|
||||
@@ -4582,6 +4680,7 @@ To connect, please ask your contact to create another connection link and check
|
||||
</trans-unit>
|
||||
<trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve">
|
||||
<source>When people request to connect, you can accept or reject it.</source>
|
||||
<target>当人们请求连接时,您可以接受或拒绝它。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve">
|
||||
@@ -4656,6 +4755,7 @@ To connect, please ask your contact to create another connection link and check
|
||||
</trans-unit>
|
||||
<trans-unit id="You can create it later" xml:space="preserve">
|
||||
<source>You can create it later</source>
|
||||
<target>您可以以后创建它</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can hide or mute a user profile - swipe it to the right. SimpleX Lock must be enabled." xml:space="preserve">
|
||||
@@ -4682,6 +4782,7 @@ SimpleX Lock must be enabled.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve">
|
||||
<source>You can share this address with your contacts to let them connect with **%@**.</source>
|
||||
<target>您可以与您的联系人分享该地址,让他们与 **%@** 联系。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve">
|
||||
@@ -4801,7 +4902,7 @@ SimpleX Lock must be enabled.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve">
|
||||
<source>You won't lose your contacts if you later delete your address.</source>
|
||||
<target>如果您以后删除它,您不会丢失您的联系人。</target>
|
||||
<target>如果您以后删除您的地址,您不会丢失您的联系人。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve">
|
||||
@@ -4894,10 +4995,13 @@ You can cancel this connection and remove the contact (and try later with a new
|
||||
<trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve">
|
||||
<source>Your contacts in SimpleX will see it.
|
||||
You can change it in Settings.</source>
|
||||
<target>您的 SimpleX 的联系人会看到它。
|
||||
您可以在设置中更改它。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your contacts will remain connected." xml:space="preserve">
|
||||
<source>Your contacts will remain connected.</source>
|
||||
<target>与您的联系人保持连接。</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve">
|
||||
@@ -5109,7 +5213,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting (introduction invitation)" xml:space="preserve">
|
||||
<source>connecting (introduction invitation)</source>
|
||||
<target>连接(介绍邀请)</target>
|
||||
<target>连接中(介绍邀请)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="connecting call" xml:space="preserve">
|
||||
@@ -5149,6 +5253,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="custom" xml:space="preserve">
|
||||
<source>custom</source>
|
||||
<target>自定义</target>
|
||||
<note>dropdown time picker choice</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve">
|
||||
@@ -5158,6 +5263,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="days" xml:space="preserve">
|
||||
<source>days</source>
|
||||
<target>天</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="default (%@)" xml:space="preserve">
|
||||
@@ -5237,6 +5343,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="hours" xml:space="preserve">
|
||||
<source>hours</source>
|
||||
<target>小时</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve">
|
||||
@@ -5346,6 +5453,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="minutes" xml:space="preserve">
|
||||
<source>minutes</source>
|
||||
<target>分钟</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="missed call" xml:space="preserve">
|
||||
@@ -5365,6 +5473,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="months" xml:space="preserve">
|
||||
<source>months</source>
|
||||
<target>月</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="never" xml:space="preserve">
|
||||
@@ -5389,6 +5498,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="no text" xml:space="preserve">
|
||||
<source>no text</source>
|
||||
<target>无文本</target>
|
||||
<note>copied message info in history</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="observer" xml:space="preserve">
|
||||
@@ -5469,6 +5579,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="seconds" xml:space="preserve">
|
||||
<source>seconds</source>
|
||||
<target>秒</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="secret" xml:space="preserve">
|
||||
@@ -5548,6 +5659,7 @@ SimpleX 服务器无法看到您的资料。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="weeks" xml:space="preserve">
|
||||
<source>weeks</source>
|
||||
<target>周</target>
|
||||
<note>time unit</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="yes" xml:space="preserve">
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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í";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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!";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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 */
|
||||
"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>你好!</p>\n<p><a href=\"%@\">通过 SimpleX Chat </a></p>与我联系";
|
||||
|
||||
/* 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." = "您当前的聊天数据库将被删除并替换为导入的数据库。";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (..))
|
||||
|
||||
@@ -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
|
||||
|
||||
+20
-648
@@ -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
|
||||
|
||||
@@ -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"
|
||||
+12
-10
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+11
-6
@@ -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 <// 100000 >> 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
|
||||
|
||||
+12
-11
@@ -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
|
||||
|
||||
+46
-24
@@ -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 [<dir>/ | <path>] 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 [<dir>/ | <path>] 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 [<dir>/ | <path>] 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 [<dir>/ | <path>] 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user