Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aba2e2ecf |
@@ -62,14 +62,6 @@ jobs:
|
||||
cache_path: C:/cabal
|
||||
asset_name: simplex-chat-windows-x86-64
|
||||
steps:
|
||||
- name: Configure pagefile (Windows)
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: al-cheb/configure-pagefile-action@v1.3
|
||||
with:
|
||||
minimum-size: 16GB
|
||||
maximum-size: 16GB
|
||||
disk-root: "C:"
|
||||
|
||||
- name: Clone project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -119,6 +111,12 @@ 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
|
||||
@@ -128,12 +126,6 @@ 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-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).
|
||||
[\#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).
|
||||
|
||||
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,10 +101,8 @@ 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/)||
|
||||
|
||||
@@ -114,7 +112,6 @@ Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https:/
|
||||
|
||||
We would love to have you join the development! You can help us with:
|
||||
|
||||
- [share the color theme](./docs/THEMES.md) you use in Android app!
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- contributing to SimpleX Chat knowledge-base.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
@@ -200,8 +197,6 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent updates:
|
||||
|
||||
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
|
||||
|
||||
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md).
|
||||
|
||||
[Mar 28, 2023. v4.6 released - with Android 8+ and ARMv7a support, hidden profiles, community moderation, improved audio/video calls and reduced battery usage](./blog/20230328-simplex-chat-v4-6-hidden-profiles.md).
|
||||
@@ -250,15 +245,13 @@ 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](./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.
|
||||
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.
|
||||
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](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
|
||||
5. Several levels of content 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.
|
||||
@@ -323,27 +316,25 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- ✅ Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
- ✅ Video messages.
|
||||
- ✅ App access passcode.
|
||||
- ✅ Improved Android app UI design.
|
||||
- ✅ Optional alternative access password.
|
||||
- ✅ Message reactions
|
||||
- ✅ Message editing history
|
||||
- ✅ Reduced battery and traffic usage in large groups.
|
||||
- 🏗 Desktop client.
|
||||
- 🏗 Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Improved Android app UI design.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Reduced battery and traffic usage in large groups.
|
||||
- Include optional message into connection request sent via contact address.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Optional alternative access password.
|
||||
- Local app files encryption.
|
||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||
- Large groups, communities and public channels.
|
||||
- Feeds/broadcasts.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- Privately share your location.
|
||||
- Feeds/broadcasts.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
||||
- Supporting the same profile on multiple devices.
|
||||
- Desktop client.
|
||||
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- the server doesn't have information about your contacts and groups.
|
||||
- Hosting server for large groups, communities and public channels.
|
||||
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- High capacity multi-node SMP relays.
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
targetSdk 32
|
||||
// !!!
|
||||
// skip version code after release to F-Droid, as it uses two version codes
|
||||
versionCode 125
|
||||
versionName "5.1.2"
|
||||
versionCode 120
|
||||
versionName "5.1-beta.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -582,7 +582,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
}
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
chatModel.clearOverlays.value = true
|
||||
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel)
|
||||
if (cInfo != null) openChat(cInfo, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ external fun chatParseServer(str: String): String
|
||||
external fun chatPasswordHash(pwd: String, salt: String): String
|
||||
|
||||
class SimplexApp: Application(), LifecycleEventObserver {
|
||||
lateinit var chatController: ChatController
|
||||
|
||||
var isAppOnForeground: Boolean = false
|
||||
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
@@ -51,7 +53,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
val ctrl = if (res is DBMigrationResult.OK) {
|
||||
migrated[1] as Long
|
||||
} else null
|
||||
chatController.ctrl = ctrl
|
||||
if (::chatController.isInitialized) {
|
||||
chatController.ctrl = ctrl
|
||||
} else {
|
||||
chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences)
|
||||
}
|
||||
chatModel.chatDbEncrypted.value = dbKey != ""
|
||||
chatModel.chatDbStatus.value = res
|
||||
if (res != DBMigrationResult.OK) {
|
||||
@@ -94,20 +100,15 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
AppPreferences(applicationContext)
|
||||
}
|
||||
|
||||
val chatController: ChatController by lazy {
|
||||
ChatController(0L, ntfManager, applicationContext, appPreferences)
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
|
||||
withBGApi {
|
||||
runBlocking {
|
||||
initChatController()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||
runMigrations()
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
|
||||
@@ -9,9 +9,9 @@ import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.ChatController
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
// based on:
|
||||
// https://robertohuertas.com/2019/06/29/android_foreground_services/
|
||||
@@ -86,7 +86,6 @@ class SimplexService: Service() {
|
||||
isStartingService = true
|
||||
withApi {
|
||||
val chatController = (application as SimplexApp).chatController
|
||||
waitDbMigrationEnds(chatController)
|
||||
try {
|
||||
Log.w(TAG, "Starting foreground service")
|
||||
val chatDbStatus = chatController.chatModel.chatDbStatus.value
|
||||
@@ -325,18 +324,6 @@ class SimplexService: Service() {
|
||||
notificationManager.cancel(PASSPHRASE_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
/*
|
||||
* When the app starts the database is in migration process. It can take from seconds to tens of seconds.
|
||||
* It happens in background thread, so other places that relies on database should wait til the end of migration
|
||||
* */
|
||||
suspend fun waitDbMigrationEnds(chatController: ChatController) {
|
||||
var maxWaitTime = 50_000
|
||||
while (chatController.chatModel.chatDbStatus.value == null && maxWaitTime > 0) {
|
||||
delay(50)
|
||||
maxWaitTime -= 50
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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): List<ArchiveError> {
|
||||
suspend fun apiImportArchive(config: ArchiveConfig) {
|
||||
val r = sendCmd(CC.ApiImportArchive(config))
|
||||
if (r is CR.ArchiveImported) return r.archiveErrors
|
||||
if (r is CR.CmdOk) return
|
||||
throw Error("failed to import archive: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
@@ -2573,7 +2573,7 @@ data class TimedMessagesPreference(
|
||||
): ChatPreference {
|
||||
companion object {
|
||||
val ttlValues: List<Int?>
|
||||
get() = listOf(600, 3600, 86400, 7 * 86400, 30 * 86400, 3 * 30 * 86400, null)
|
||||
get() = listOf(3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3369,7 +3369,6 @@ 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()
|
||||
|
||||
@@ -3479,7 +3478,6 @@ sealed class CR {
|
||||
is CmdOk -> "cmdOk"
|
||||
is ChatCmdError -> "chatCmdError"
|
||||
is ChatRespError -> "chatError"
|
||||
is ArchiveImported -> "archiveImported"
|
||||
is Response -> "* $type"
|
||||
is Invalid -> "* invalid json"
|
||||
}
|
||||
@@ -3592,7 +3590,6 @@ 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
|
||||
}
|
||||
@@ -3681,7 +3678,6 @@ 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()
|
||||
@@ -3689,7 +3685,6 @@ 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
|
||||
@@ -3901,13 +3896,3 @@ 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()
|
||||
}
|
||||
|
||||
@@ -376,12 +376,8 @@ fun ComposeView(
|
||||
live = live,
|
||||
ttl = ttl
|
||||
)
|
||||
if (aChatItem != null) {
|
||||
chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
if (file != null) removeFile(context, file)
|
||||
return null
|
||||
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
return aChatItem?.chatItem
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -114,8 +114,12 @@ private fun GroupWelcomeLayout(
|
||||
|
||||
@Composable
|
||||
private fun TextPreview(text: String, linkMode: SimplexLinkMode, markdown: Boolean = true) {
|
||||
Column {
|
||||
SelectionContainer(Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
Modifier.height(140.dp)
|
||||
) {
|
||||
SelectionContainer(
|
||||
Modifier.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
MarkdownText(
|
||||
text,
|
||||
formattedText = if (markdown) remember(text) { parseToMarkdown(text) } else null,
|
||||
|
||||
@@ -288,14 +288,6 @@ 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()
|
||||
@@ -322,8 +314,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 -> ModeratedItem()
|
||||
is CIContent.RcvModerated -> ModeratedItem()
|
||||
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
if (chatModel.chats.size > 0) {
|
||||
if (chatModel.chats.size >= 8) {
|
||||
barButtons.add {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(painterResource(R.drawable.ic_search_500), stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary)
|
||||
|
||||
@@ -178,7 +178,8 @@ 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,
|
||||
@@ -573,17 +574,11 @@ private fun importArchive(
|
||||
m.controller.apiDeleteStorage()
|
||||
try {
|
||||
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
|
||||
val archiveErrors = m.controller.apiImportArchive(config)
|
||||
m.controller.apiImportArchive(config)
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
|
||||
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))
|
||||
}
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
|
||||
}
|
||||
} catch (e: Error) {
|
||||
operationEnded(m, progressIndicator) {
|
||||
|
||||
@@ -56,7 +56,6 @@ class MessagesFetcherWork(
|
||||
try {
|
||||
withTimeout(durationSeconds * 1000L) {
|
||||
val chatController = (applicationContext as SimplexApp).chatController
|
||||
SimplexService.waitDbMigrationEnds(chatController)
|
||||
val chatDbStatus = chatController.chatModel.chatDbStatus.value
|
||||
if (chatDbStatus != DBMigrationResult.OK) {
|
||||
Log.w(TAG, "Worker: problem with the database: $chatDbStatus")
|
||||
|
||||
@@ -303,10 +303,7 @@ fun SimplexLockView(
|
||||
LockDelaySelector(remember { laLockDelay.state }) { laLockDelay.set(it) }
|
||||
if (showChangePasscode.value && laMode.value == LAMode.PASSCODE) {
|
||||
SectionItemView({ changeLAPassword() }) {
|
||||
Text(
|
||||
generalGetString(R.string.la_change_app_passcode),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
Text(generalGetString(R.string.la_change_app_passcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,10 +342,7 @@ fun SimplexLockView(
|
||||
}
|
||||
}
|
||||
SectionItemView({ changeSelfDestructPassword() }) {
|
||||
Text(
|
||||
stringResource(R.string.change_self_destruct_passcode),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
Text(stringResource(R.string.change_self_destruct_passcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">nabízeno %s: %2s</string>
|
||||
<string name="feature_offered_item_with_param">offered %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">pozval <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></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_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: %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="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="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: %s</string>
|
||||
<string name="share_text_deleted_at">Smazáno v: %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,5 +1261,4 @@
|
||||
<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,5 +1341,4 @@
|
||||
<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">y después:</string>
|
||||
<string name="above_then_preposition_continuation">arriba y luego:</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 (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="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="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">Crea enlace de invitación de un uso</string>
|
||||
<string name="create_group">Crea grupo secreto</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="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 base de datos</string>
|
||||
<string name="info_row_database_id">ID de 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 en</string>
|
||||
<string name="delete_messages_after">Eliminar después de</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>
|
||||
@@ -141,13 +141,13 @@
|
||||
<string name="group_member_status_intro_invitation">conectando (invitación de presentación )</string>
|
||||
<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">Conecta vía enlace / Código QR</string>
|
||||
<string name="connection_local_display_name">conexión<xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"connection ID\" example=\"1\"></string>
|
||||
<string name="connect_via_link_or_qr">Conectar mediante 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">Contacto</string>
|
||||
<string name="notification_preview_mode_contact">Nombre del 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 en</string>
|
||||
<string name="delete_after">Eliminar después de</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">Crea enlace de invitación de un uso.</string>
|
||||
<string name="share_one_time_link">Crear enlace de invitación de un solo 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 la conexion pendiente\?</string>
|
||||
<string name="delete_pending_connection__question">¿Eliminar 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">por conexión</string>
|
||||
<string name="network_session_mode_entity">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">por perfil</string>
|
||||
<string name="network_session_mode_user">Perfil de Chat</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">Sin vista previa</string>
|
||||
<string name="notification_preview_mode_hidden">Oculta</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">Contacto y texto</string>
|
||||
<string name="notification_preview_mode_message">Texto del mensaje</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</string>
|
||||
<string name="settings_notifications_mode_title">Servicio de notificaciones</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">Sólo con aplicación abierta</string>
|
||||
<string name="notifications_mode_off">Se ejecuta sólo cuando la aplicación está 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">Cíclico</string>
|
||||
<string name="notifications_mode_periodic">Se ejecuta periódicamente</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 tus 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_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 disponibles</string>
|
||||
<string name="network_use_onion_hosts_prefer">Si disponible</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 del perfil oculto</string>
|
||||
<string name="hidden_profile_password">Contraseña de 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 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>
|
||||
\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>
|
||||
<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">Configura <i>Usar hosts .onion</i> a No si el proxy SOCKS no los admite.</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="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 en</string>
|
||||
<string name="lock_mode">Modo bloqueo</string>
|
||||
<string name="lock_after">Bloquear después de</string>
|
||||
<string name="lock_mode">Modo de 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 eliminar 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 borrar 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</string>
|
||||
<string name="info_row_received_at">Recibido</string>
|
||||
<string name="info_row_sent_at">Enviado</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="item_info_current">(actual)</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="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="current_version_timestamp">%s (actual)</string>
|
||||
<string name="share_text_sent_at">Enviado: %s</string>
|
||||
<string name="info_row_disappears_at">Desaparecerá</string>
|
||||
<string name="share_text_sent_at">Enviado el: %s</string>
|
||||
<string name="info_row_disappears_at">Desaparecerá el</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 actualiz</string>
|
||||
<string name="info_row_updated_at">Registro actualizado</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</string>
|
||||
<string name="info_row_deleted_at">Eliminado el</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,11 +1256,10 @@
|
||||
\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 eliminados.</string>
|
||||
<string name="v5_1_self_destruct_passcode_descr">Al introducirlo todos los datos son borrados.</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îtra le : %s</string>
|
||||
<string name="info_row_disappears_at">Disparaîtra le</string>
|
||||
<string name="share_text_disappears_at">Disparaît à : %s</string>
|
||||
<string name="info_row_disappears_at">Disparaît à</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,5 +1261,4 @@
|
||||
<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>
|
||||
@@ -473,7 +473,7 @@
|
||||
<string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string>
|
||||
<string name="button_add_members">Nodig leden uit</string>
|
||||
<string name="notification_display_mode_hidden_desc">Verberg contact en bericht</string>
|
||||
<string name="turn_off_battery_optimization">Om het te gebruiken <b>batterijoptimalisatie uitschakelen</b> voor <xliff:g xmlns:xliff="urn:oasis: names:tc:xliff:document:1.2" id="appName">SimpleX</xliff:g> in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.</string>
|
||||
<string name="turn_off_battery_optimization">Om het te gebruiken <b xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">batterijoptimalisatie uitschakelen</b> voor <xliff:g xmlns:xliff=\"urn:oasis: names:tc:xliff:document:1.2\" id=\"appName\">SimpleX</xliff:g> in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld.</string>
|
||||
<string name="onboarding_notifications_mode_service">Onmiddellijk</string>
|
||||
<string name="rcv_group_event_member_added">heeft <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> uitgenodigd</string>
|
||||
@@ -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> achtergrond service</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> achtergrondservice</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,5 +1260,4 @@
|
||||
<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 zawsze działa w tle - powiadomienia są wyświetlane, gdy tylko wiadomości są dostępne.</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="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 (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="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="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,5 +1261,4 @@
|
||||
<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>
|
||||
@@ -687,7 +687,7 @@
|
||||
<string name="settings_section_title_you">VOCÊ</string>
|
||||
<string name="settings_section_title_messages">MENSAGENS E ARQUIVOS</string>
|
||||
<string name="your_chat_database">Seu banco de dados de chat</string>
|
||||
<string name="snd_group_event_member_deleted">Você removeu <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_member_deleted">Você removeu<xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"member profile\" example=\"alice (Alice)\"></string>
|
||||
<string name="group_member_status_removed">removido</string>
|
||||
<string name="group_member_status_left">saiu</string>
|
||||
<string name="group_member_role_owner">proprietário</string>
|
||||
@@ -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">Marcar como verificado</string>
|
||||
<string name="mark_code_verified">Marcado 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>
|
||||
@@ -794,7 +794,7 @@
|
||||
<string name="videos_limit_title">Excesso de vídeos!</string>
|
||||
<string name="icon_descr_video_asked_to_receive">Solicitou receber o vídeo</string>
|
||||
<string name="videos_limit_desc">Apenas 10 vídeos podem ser enviados ao mesmo tempo</string>
|
||||
<string name="contact_sent_large_file">Seu contato enviou um arquivo maior que o tamanho máximo permitido (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="contact_sent_large_file">Seu contato enviou um arquivo maior que o tamanho máximo permitido (<xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"maxFileSize\">).</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">O arquivo será recebido quando seu contato concluir o upload.</string>
|
||||
<string name="smp_servers_test_some_failed">Alguns servidores falharam o teste:</string>
|
||||
<string name="smp_servers_save">Salvar servidores</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">Configurações</string>
|
||||
<string name="your_settings">Suas 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>
|
||||
@@ -893,7 +893,7 @@
|
||||
<string name="v4_5_private_filenames_descr">Para proteger o fuso horário, os arquivos de imagem/voz usam UTC.</string>
|
||||
<string name="unknown_message_format">formato de mensagem desconhecido</string>
|
||||
<string name="description_via_group_link">via link de grupo</string>
|
||||
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
<string name="simplex_link_connection">via<xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"serverHost\" example=\"smp.simplex.im\"></string>
|
||||
<string name="connection_error_auth_desc">A menos que seu contato tenha excluído a conexão ou este link já tenha sido usado, pode ser um bug - por favor, relate-o.
|
||||
\nPara se conectar, peça ao seu contato para criar outro link de conexão e verifique se você tem uma conexão de rede estável.</string>
|
||||
<string name="error_smp_test_failed_at_step">O teste falhou na etapa %s.</string>
|
||||
@@ -1258,5 +1258,4 @@
|
||||
<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>
|
||||
@@ -117,8 +117,8 @@
|
||||
<string name="icon_descr_audio_on">开启音频</string>
|
||||
<string name="icon_descr_asked_to_receive">已要求接收图片</string>
|
||||
<string name="network_session_mode_user_description">一个单独的TCP连接(和SOCKS凭证)将被用于<b>,用于您在应用程序中的每个聊天资料</b> 。</string>
|
||||
<string name="network_session_mode_entity_description">每个联系人和群组成员 <b>将使用单独的 TCP 连接(和 SOCKS 凭证)</b>。
|
||||
\n<b>请注意</b>:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。</string>
|
||||
<string name="network_session_mode_entity_description">每个联系人和群组成员</b> 将使用单独的 TCP 连接(和 SOCKS 凭证)<b>。
|
||||
\n<b>请注意</b>:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>添加新联系人</b>:为您的联系人创建一次性二维码。</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b> 最长续航 </b>。您只会在应用程序运行时收到通知(无后台服务)。</string>
|
||||
@@ -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">停止聊天程序</string>
|
||||
<string name="auth_stop_chat">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>
|
||||
@@ -950,7 +950,7 @@
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="waiting_for_file">等待文件中</string>
|
||||
<string name="contact_sent_large_file">您的联系人发送的文件大于当前支持的最大大小 (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="contact_sent_large_file">您的联系人发送的文件大于当前支持的最大大小 (<xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"maxFileSize\">%1$s</xliff :g>).</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">当您的连接请求被接受后,您将可以连接,请稍等或稍后检查!</string>
|
||||
<string name="smp_servers_use_server">使用服务器</string>
|
||||
<string name="smp_servers_your_server_address">您的服务器地址</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">停止</string>
|
||||
<string name="stop_file__confirm">Stop</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,90 +1176,7 @@
|
||||
<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="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>
|
||||
<string name="info_row_deleted_at">已删除在</string>
|
||||
<string name="share_text_deleted_at">已删除在:%s</string>
|
||||
<string name="share_text_disappears_at">消失在:%s</string>
|
||||
</resources>
|
||||
@@ -149,7 +149,7 @@
|
||||
<string name="failed_to_create_user_duplicate_title">重複的顯示名稱!</string>
|
||||
<string name="network_session_mode_entity_description">一個單獨的 TCP 連接(和 SOCKS 憑證)將用於<b>每個聯絡人和群組內的成員</b>。
|
||||
\n<b>請注意</b>:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。</string>
|
||||
<string name="network_session_mode_user_description">一個單獨的 TCP 連接(和 SOCKS 憑證)將用於 <b>每個聊天室的設定</b>。</string>
|
||||
<string name="network_session_mode_user_description">一個單獨的 TCP 連接(和 SOCKS 憑證)將用於<b>每個聊天室的設定<b>。</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="turning_off_service_and_periodic">省電模式運行中,關閉了背景通知服務和定期更新接收訊息。你可以在通知設定內重新啟用。</string>
|
||||
<string name="settings_section_title_incognito">匿名聊天模式</string>
|
||||
@@ -366,7 +366,7 @@
|
||||
<string name="send_live_message_desc">傳送實況訊息 - 這會即時顯示你在輸入中的文字</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(掃描或使用剪貼薄貼上)</string>
|
||||
<string name="toast_permission_denied">權限被拒絕!</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">為了保護你的私隱,<b><xliff:g id="appName">SimpleX</xliff:g></b> 有一個後台通知服務 – 它每天使用你幾 % 的電量。</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">為了保護你的私隱,<b xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">SimpleX</xliff:g> 有一個後台通知服務 – 它每天使用你幾 % 的電量。</string>
|
||||
<string name="periodic_notifications">定期通知</string>
|
||||
<string name="notifications_mode_periodic_desc">每十分鐘會檢查一次訊息,最快可設為每分鐘檢查一次</string>
|
||||
<string name="notification_preview_mode_message">訊息文字</string>
|
||||
@@ -424,7 +424,7 @@
|
||||
<string name="colored">顏色</string>
|
||||
<string name="how_to_use_your_servers">如何使用你的伺服器</string>
|
||||
<string name="to_connect_via_link_title">使用連結連接</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 桌面版:在應用程式內掃描一個已存在的二維碼,透過 <b>掃描二維碼</b>。</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 桌面版:在應用程式內掃描一個已存在的二維碼,透過 <b>掃描二維碼 <b>。</string>
|
||||
<string name="icon_descr_settings">設定</string>
|
||||
<string name="this_QR_code_is_not_a_link">這個二維碼不是一個連結!</string>
|
||||
<string name="create_one_time_link">建立一次性邀請連結</string>
|
||||
@@ -879,7 +879,7 @@
|
||||
\n1. 訊息將在傳送至客戶端後兩天或在伺服器內三十天時過時。
|
||||
\n2. 訊息解密失敗,因為你或你的聯絡人用了舊的數據庫備份
|
||||
\n3. 連接被破壞。</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">只有客戶端裝置才會儲存你的個人檔案、聯絡人,群組,所有訊息都會經過 <b>兩層的端對端加密</b>。</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">只有客戶端裝置才會儲存你的個人檔案、聯絡人,群組,所有訊息都會經過<b>兩層的端對端加密<b>。</string>
|
||||
<string name="store_passphrase_securely">請放置你的密碼於安全的地方,如果你遺失了密碼,將不可能修改你的密碼。</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">停止聊天室以匯出對話,匯入或刪除對話數據庫。當聊天室停止後你將不能接收或傳送訊息。</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">你正在使用匿名聊天模式進入此群組 - 為了避免分享你的真實個人檔案,邀請聯絡人是不允許的。</string>
|
||||
|
||||
@@ -892,7 +892,6 @@
|
||||
<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>
|
||||
|
||||
@@ -18,7 +18,6 @@ struct ContentView: View {
|
||||
@Binding var userAuthorized: Bool?
|
||||
@Binding var canConnectCall: Bool
|
||||
@Binding var lastSuccessfulUnlock: TimeInterval?
|
||||
@Binding var showInitializationView: Bool
|
||||
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@@ -28,6 +27,7 @@ struct ContentView: View {
|
||||
@State private var showWhatsNew = false
|
||||
@State private var showChooseLAMode = false
|
||||
@State private var showSetPasscode = false
|
||||
@State private var showInitializationView = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -53,6 +53,9 @@ struct ContentView: View {
|
||||
.onAppear {
|
||||
if prefPerformLA { requestNtfAuthorization() }
|
||||
initAuthenticate()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
showInitializationView = true
|
||||
}
|
||||
}
|
||||
.onChange(of: doAuthenticate) { _ in
|
||||
initAuthenticate()
|
||||
|
||||
@@ -86,7 +86,6 @@ private func withBGTask<T>(bgDelay: Double? = nil, f: @escaping () -> T) -> T {
|
||||
|
||||
func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil) -> ChatResponse {
|
||||
logger.debug("chatSendCmd \(cmd.cmdType)")
|
||||
let start = Date.now
|
||||
let resp = bgTask
|
||||
? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd) }
|
||||
: sendSimpleXCmd(cmd)
|
||||
@@ -95,7 +94,7 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.addTerminalItem(.cmd(start, cmd.obfuscated))
|
||||
ChatModel.shared.addTerminalItem(.cmd(.now, cmd.obfuscated))
|
||||
ChatModel.shared.addTerminalItem(.resp(.now, resp))
|
||||
}
|
||||
return resp
|
||||
@@ -244,10 +243,8 @@ func apiExportArchive(config: ArchiveConfig) async throws {
|
||||
try await sendCommandOkResp(.apiExportArchive(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 apiImportArchive(config: ArchiveConfig) async throws {
|
||||
try await sendCommandOkResp(.apiImportArchive(config: config))
|
||||
}
|
||||
|
||||
func apiDeleteStorage() async throws {
|
||||
@@ -541,6 +538,7 @@ 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)
|
||||
|
||||
@@ -23,7 +23,6 @@ struct SimpleXApp: App {
|
||||
@State private var enteredBackground: TimeInterval? = nil
|
||||
@State private var canConnectCall = false
|
||||
@State private var lastSuccessfulUnlock: TimeInterval? = nil
|
||||
@State private var showInitializationView = false
|
||||
|
||||
init() {
|
||||
hs_init(0, nil)
|
||||
@@ -37,23 +36,14 @@ struct SimpleXApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
return WindowGroup {
|
||||
ContentView(
|
||||
doAuthenticate: $doAuthenticate,
|
||||
userAuthorized: $userAuthorized,
|
||||
canConnectCall: $canConnectCall,
|
||||
lastSuccessfulUnlock: $lastSuccessfulUnlock,
|
||||
showInitializationView: $showInitializationView
|
||||
)
|
||||
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized, canConnectCall: $canConnectCall, lastSuccessfulUnlock: $lastSuccessfulUnlock)
|
||||
.environmentObject(chatModel)
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
chatModel.appOpenUrl = url
|
||||
}
|
||||
.onAppear() {
|
||||
showInitializationView = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
initChatAndMigrate()
|
||||
}
|
||||
initChatAndMigrate()
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
|
||||
|
||||
@@ -726,9 +726,6 @@ struct ComposeView: View {
|
||||
}
|
||||
return chatItem
|
||||
}
|
||||
if let file = file {
|
||||
removeFile(file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -29,33 +29,43 @@ struct GroupLinkView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Text("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.")
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
Section {
|
||||
ScrollView {
|
||||
VStack (alignment: .leading) {
|
||||
Text("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.")
|
||||
.padding(.bottom)
|
||||
if let groupLink = groupLink {
|
||||
Picker("Initial role", selection: $groupLinkMemberRole) {
|
||||
ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in
|
||||
Text(role.text)
|
||||
HStack {
|
||||
Text("Initial role")
|
||||
Picker("Initial role", selection: $groupLinkMemberRole) {
|
||||
ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in
|
||||
Text(role.text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
}
|
||||
.frame(height: 36)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
QRCode(uri: groupLink)
|
||||
Button {
|
||||
showShareSheet(items: [groupLink])
|
||||
} label: {
|
||||
Label("Share link", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
HStack {
|
||||
Button {
|
||||
showShareSheet(items: [groupLink])
|
||||
} label: {
|
||||
Label("Share link", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.padding()
|
||||
|
||||
Button(role: .destructive) { alert = .deleteLink } label: {
|
||||
Label("Delete link", systemImage: "trash")
|
||||
Button(role: .destructive) { alert = .deleteLink } label: {
|
||||
Label("Delete link", systemImage: "trash")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
Button(action: createGroupLink) {
|
||||
Label("Create link", systemImage: "link.badge.plus")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.disabled(creatingLink)
|
||||
.padding(.bottom)
|
||||
if creatingLink {
|
||||
ProgressView()
|
||||
.scaleEffect(2)
|
||||
@@ -63,6 +73,8 @@ struct GroupLinkView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case .deleteLink:
|
||||
|
||||
@@ -20,7 +20,6 @@ struct GroupMemberInfoView: View {
|
||||
@State private var newRole: GroupMemberRole = .member
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var justOpened = true
|
||||
|
||||
enum GroupMemberInfoViewAlert: Identifiable {
|
||||
case removeMemberAlert(mem: GroupMember)
|
||||
@@ -147,10 +146,6 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.onAppear {
|
||||
if #unavailable(iOS 16) {
|
||||
// this condition prevents re-setting picker
|
||||
if !justOpened { return }
|
||||
}
|
||||
newRole = member.memberRole
|
||||
do {
|
||||
let stats = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId)
|
||||
@@ -161,7 +156,6 @@ struct GroupMemberInfoView: View {
|
||||
} catch let error {
|
||||
logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))")
|
||||
}
|
||||
justOpened = false
|
||||
}
|
||||
.onChange(of: newRole) { _ in
|
||||
if newRole != member.memberRole {
|
||||
|
||||
@@ -53,10 +53,10 @@ struct GroupWelcomeView: View {
|
||||
}
|
||||
|
||||
private func textPreview() -> some View {
|
||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil)
|
||||
.allowsHitTesting(false)
|
||||
.frame(minHeight: 140, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
ScrollView {
|
||||
messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func editorView() -> some View {
|
||||
@@ -80,6 +80,7 @@ struct GroupWelcomeView: View {
|
||||
}
|
||||
} else {
|
||||
textPreview()
|
||||
.frame(height: 140, alignment: .topLeading)
|
||||
}
|
||||
|
||||
Button {
|
||||
|
||||
@@ -29,7 +29,11 @@ struct ChatListView: View {
|
||||
if chatModel.chats.isEmpty {
|
||||
onboardingButtons()
|
||||
}
|
||||
chatListView
|
||||
if chatModel.chats.count > 8 {
|
||||
chatList.searchable(text: $searchText)
|
||||
} else {
|
||||
chatList
|
||||
}
|
||||
}
|
||||
}
|
||||
if userPickerVisible {
|
||||
@@ -43,12 +47,18 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var chatListView: some View {
|
||||
VStack {
|
||||
if chatModel.chats.count > 0 {
|
||||
chatList.searchable(text: $searchText)
|
||||
} else {
|
||||
chatList
|
||||
var chatList: some View {
|
||||
List {
|
||||
ForEach(filteredChats(), id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if chatModel.chatId == nil, let chatId = chatModel.chatToTop {
|
||||
chatModel.chatToTop = nil
|
||||
chatModel.popChat(chatId)
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
|
||||
@@ -106,22 +116,6 @@ struct ChatListView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var chatList: some View {
|
||||
List {
|
||||
ForEach(filteredChats(), id: \.viewId) { chat in
|
||||
ChatListNavLink(chat: chat)
|
||||
.padding(.trailing, -16)
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
}
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if chatModel.chatId == nil, let chatId = chatModel.chatToTop {
|
||||
chatModel.chatToTop = nil
|
||||
chatModel.popChat(chatId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
|
||||
Circle()
|
||||
.frame(width: size, height: size)
|
||||
|
||||
@@ -14,7 +14,6 @@ enum DatabaseAlert: Identifiable {
|
||||
case exportProhibited
|
||||
case importArchive
|
||||
case archiveImported
|
||||
case archiveImportedWithErrors(archiveErrors: [ArchiveError])
|
||||
case deleteChat
|
||||
case chatDeleted
|
||||
case deleteLegacyDatabase
|
||||
@@ -28,7 +27,6 @@ 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"
|
||||
@@ -253,11 +251,7 @@ 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?"),
|
||||
@@ -357,13 +351,9 @@ struct DatabaseView: View {
|
||||
try await apiDeleteStorage()
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
try await apiImportArchive(config: config)
|
||||
_ = kcDatabasePassword.remove()
|
||||
if archiveErrors.isEmpty {
|
||||
await operationEnded(.archiveImported)
|
||||
} else {
|
||||
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors))
|
||||
}
|
||||
await operationEnded(.archiveImported)
|
||||
} 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) }
|
||||
let _ = try await apiImportArchive(config: config)
|
||||
try await apiImportArchive(config: config)
|
||||
await MainActor.run { setV3DBMigration(.migrated) }
|
||||
} catch let error {
|
||||
dbContainerGroupDefault.set(.documents)
|
||||
|
||||
@@ -147,7 +147,6 @@ struct DropdownCustomTimePicker: View {
|
||||
@State private var dropdownSelection: DropdownSelection = .dropdownValue(value: nil)
|
||||
@State private var showCustomTimePicker = false
|
||||
@State private var selectedCustomTime: Int? = nil
|
||||
@State private var justOpened = true
|
||||
|
||||
enum DropdownSelection: Hashable {
|
||||
case dropdownValue(value: Int?)
|
||||
@@ -168,12 +167,7 @@ struct DropdownCustomTimePicker: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if #unavailable(iOS 16) {
|
||||
// this condition prevents re-setting picker
|
||||
if !justOpened { return }
|
||||
}
|
||||
dropdownSelection = .dropdownValue(value: selection)
|
||||
justOpened = false
|
||||
}
|
||||
.onChange(of: selection) { v in
|
||||
logger.debug("*** .onChange(of: selection)")
|
||||
|
||||
@@ -5498,7 +5498,6 @@ 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,7 +5498,6 @@ 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 eliminados.</target>
|
||||
<target>Al introducirlo todos los datos son borrados.</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>Conecta vía enlace / Código QR</target>
|
||||
<target>Conectar mediante 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>Contacto</target>
|
||||
<target>Nombre del 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>Crea enlace de invitación de un uso</target>
|
||||
<target>Crear enlace de invitación de un solo 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>Crea grupo secreto</target>
|
||||
<target>Crear 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 base de datos</target>
|
||||
<target>ID de 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 base de datos: %d</target>
|
||||
<target>ID de 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 en</target>
|
||||
<target>Eliminar después de</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 en</target>
|
||||
<target>Eliminar después de</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 la conexion pendiente?</target>
|
||||
<target>¿Eliminar 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</target>
|
||||
<target>Eliminado el</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Deleted at: %@" xml:space="preserve">
|
||||
<source>Deleted at: %@</source>
|
||||
<target>Eliminado: %@</target>
|
||||
<target>Eliminado el: %@</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á</target>
|
||||
<target>Desaparecerá el</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at: %@" xml:space="preserve">
|
||||
<source>Disappears at: %@</source>
|
||||
<target>Desaparecerá: %@</target>
|
||||
<target>Desaparecerá el: %@</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 del perfil oculto</target>
|
||||
<target>Contraseña de perfil oculto</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hide" xml:space="preserve">
|
||||
<source>Hide</source>
|
||||
<target>Sin vista previa</target>
|
||||
<target>Ocultar</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 en</target>
|
||||
<target>Bloquear después de</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Lock mode" xml:space="preserve">
|
||||
<source>Lock mode</source>
|
||||
<target>Modo bloqueo</target>
|
||||
<target>Modo de 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>Contacto y texto</target>
|
||||
<target>Texto del mensaje</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</target>
|
||||
<target>Moderado el</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Moderated at: %@" xml:space="preserve">
|
||||
<source>Moderated at: %@</source>
|
||||
<target>Moderado: %@</target>
|
||||
<target>Moderado el: %@</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:
|
||||
- eliminar mensajes de los miembros.
|
||||
- desactivar el rol miembro (a rol "observador")</target>
|
||||
- borrar mensajes de los miembros.
|
||||
- desactivar el rol a miembros (pasando 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</target>
|
||||
<target>Recibido el</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Received at: %@" xml:space="preserve">
|
||||
<source>Received at: %@</source>
|
||||
<target>Recibido: %@</target>
|
||||
<target>Recibido el: %@</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 actualiz.</target>
|
||||
<target>Registro actualizado</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 actualiz: %@</target>
|
||||
<target>Registro actualizado: %@</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</target>
|
||||
<target>Enviado el</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sent at: %@" xml:space="preserve">
|
||||
<source>Sent at: %@</source>
|
||||
<target>Enviado: %@</target>
|
||||
<target>Enviado el: %@</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 **Mis perfiles**.</target>
|
||||
<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>
|
||||
<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 disponibles</target>
|
||||
<target>Si disponible</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 tus mensajes:</target>
|
||||
<target>Puedes usar la sintaxis markdown para dar formato a los 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>y después elige:</target>
|
||||
<target>arriba y luego elige:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="accepted call" xml:space="preserve">
|
||||
@@ -5499,7 +5499,6 @@ 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îtra le</target>
|
||||
<target>Disparaît à</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disappears at: %@" xml:space="preserve">
|
||||
<source>Disappears at: %@</source>
|
||||
<target>Disparaîtra le : %@</target>
|
||||
<target>Disparaît à : %@</target>
|
||||
<note>copied message info</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Disconnect" xml:space="preserve">
|
||||
@@ -5498,7 +5498,6 @@ 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">
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld file(s) with total size of %@" xml:space="preserve">
|
||||
<source>%lld file(s) with total size of %@</source>
|
||||
<target>%lld 個のファイル(合計サイズ: %@)</target>
|
||||
<target>合計サイズ %@ の %lld 件のファイル</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="%lld members" xml:space="preserve">
|
||||
|
||||
@@ -5497,7 +5497,6 @@ 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,7 +5498,6 @@ 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,12 +59,10 @@
|
||||
</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">
|
||||
@@ -99,7 +97,6 @@
|
||||
</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">
|
||||
@@ -301,9 +298,6 @@
|
||||
<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">
|
||||
@@ -313,7 +307,6 @@
|
||||
</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">
|
||||
@@ -348,7 +341,6 @@
|
||||
</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">
|
||||
@@ -358,7 +350,6 @@
|
||||
</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">
|
||||
@@ -369,8 +360,6 @@
|
||||
<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">
|
||||
@@ -412,7 +401,6 @@
|
||||
</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">
|
||||
@@ -443,7 +431,6 @@
|
||||
</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">
|
||||
@@ -478,7 +465,6 @@
|
||||
</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">
|
||||
@@ -493,7 +479,6 @@
|
||||
</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">
|
||||
@@ -503,7 +488,6 @@
|
||||
</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">
|
||||
@@ -523,7 +507,6 @@
|
||||
</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">
|
||||
@@ -548,12 +531,10 @@
|
||||
</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">
|
||||
@@ -588,7 +569,6 @@
|
||||
</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">
|
||||
@@ -623,7 +603,6 @@
|
||||
</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">
|
||||
@@ -648,7 +627,6 @@
|
||||
</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">
|
||||
@@ -713,7 +691,6 @@
|
||||
</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">
|
||||
@@ -743,12 +720,10 @@
|
||||
</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">
|
||||
@@ -858,12 +833,10 @@
|
||||
</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>
|
||||
@@ -1114,7 +1087,6 @@
|
||||
</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">
|
||||
@@ -1134,12 +1106,10 @@
|
||||
</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">
|
||||
@@ -1199,7 +1169,6 @@
|
||||
</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">
|
||||
@@ -1214,7 +1183,6 @@
|
||||
</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">
|
||||
@@ -1492,12 +1460,10 @@
|
||||
</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">
|
||||
@@ -1552,7 +1518,6 @@
|
||||
</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">
|
||||
@@ -1572,12 +1537,10 @@
|
||||
</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">
|
||||
@@ -1607,7 +1570,6 @@
|
||||
</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">
|
||||
@@ -1632,7 +1594,6 @@
|
||||
</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">
|
||||
@@ -1687,12 +1648,10 @@
|
||||
</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">
|
||||
@@ -1767,12 +1726,10 @@
|
||||
</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">
|
||||
@@ -1942,7 +1899,6 @@
|
||||
</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">
|
||||
@@ -2067,7 +2023,6 @@
|
||||
</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">
|
||||
@@ -2157,7 +2112,6 @@
|
||||
</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">
|
||||
@@ -2262,7 +2216,6 @@
|
||||
</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">
|
||||
@@ -2297,7 +2250,6 @@
|
||||
</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">
|
||||
@@ -2307,12 +2259,10 @@
|
||||
</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">
|
||||
@@ -2422,7 +2372,6 @@
|
||||
</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">
|
||||
@@ -2469,7 +2418,6 @@
|
||||
</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">
|
||||
@@ -2530,7 +2478,6 @@
|
||||
</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">
|
||||
@@ -2575,7 +2522,6 @@
|
||||
</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">
|
||||
@@ -2595,7 +2541,6 @@
|
||||
</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">
|
||||
@@ -2720,17 +2665,14 @@
|
||||
</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">
|
||||
@@ -2780,12 +2722,10 @@
|
||||
</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">
|
||||
@@ -2855,7 +2795,6 @@
|
||||
</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">
|
||||
@@ -2999,7 +2938,6 @@
|
||||
</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">
|
||||
@@ -3024,7 +2962,6 @@
|
||||
</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">
|
||||
@@ -3074,7 +3011,6 @@
|
||||
</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">
|
||||
@@ -3244,7 +3180,6 @@
|
||||
</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">
|
||||
@@ -3279,7 +3214,6 @@
|
||||
</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">
|
||||
@@ -3294,12 +3228,10 @@
|
||||
</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">
|
||||
@@ -3344,7 +3276,6 @@
|
||||
</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">
|
||||
@@ -3354,17 +3285,14 @@
|
||||
</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">
|
||||
@@ -3379,12 +3307,10 @@
|
||||
</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">
|
||||
@@ -3394,7 +3320,6 @@
|
||||
</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">
|
||||
@@ -3414,12 +3339,10 @@
|
||||
</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">
|
||||
@@ -3599,7 +3522,6 @@
|
||||
</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">
|
||||
@@ -3639,7 +3561,6 @@
|
||||
</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">
|
||||
@@ -3694,27 +3615,22 @@
|
||||
</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">
|
||||
@@ -3734,7 +3650,6 @@
|
||||
</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">
|
||||
@@ -3789,12 +3704,10 @@
|
||||
</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">
|
||||
@@ -3804,7 +3717,6 @@
|
||||
</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">
|
||||
@@ -3854,7 +3766,6 @@
|
||||
</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">
|
||||
@@ -3889,12 +3800,10 @@
|
||||
</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">
|
||||
@@ -3909,7 +3818,6 @@
|
||||
</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">
|
||||
@@ -3934,7 +3842,6 @@
|
||||
</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">
|
||||
@@ -3964,7 +3871,6 @@
|
||||
</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">
|
||||
@@ -4064,12 +3970,10 @@
|
||||
</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">
|
||||
@@ -4316,7 +4220,6 @@ 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">
|
||||
@@ -4428,7 +4331,6 @@ 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">
|
||||
@@ -4680,7 +4582,6 @@ 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">
|
||||
@@ -4755,7 +4656,6 @@ 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">
|
||||
@@ -4782,7 +4682,6 @@ 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">
|
||||
@@ -4902,7 +4801,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">
|
||||
@@ -4995,13 +4894,10 @@ 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">
|
||||
@@ -5213,7 +5109,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">
|
||||
@@ -5253,7 +5149,6 @@ 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">
|
||||
@@ -5263,7 +5158,6 @@ 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">
|
||||
@@ -5343,7 +5237,6 @@ 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">
|
||||
@@ -5453,7 +5346,6 @@ 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">
|
||||
@@ -5473,7 +5365,6 @@ 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">
|
||||
@@ -5498,7 +5389,6 @@ 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">
|
||||
@@ -5579,7 +5469,6 @@ 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">
|
||||
@@ -5659,7 +5548,6 @@ 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">
|
||||
|
||||
@@ -83,12 +83,12 @@
|
||||
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */; };
|
||||
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; };
|
||||
5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; };
|
||||
5CA4874A2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */; };
|
||||
5CA4874B2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */; };
|
||||
5CA4874C2A228AF400409F23 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487472A228AF300409F23 /* libgmp.a */; };
|
||||
5CA4874D2A228AF400409F23 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487482A228AF300409F23 /* libffi.a */; };
|
||||
5CA4874E2A228AF400409F23 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA487492A228AF400409F23 /* libgmpxx.a */; };
|
||||
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */; };
|
||||
5CAC41212A1985AC00C331A2 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAC411C2A1985AB00C331A2 /* libgmp.a */; };
|
||||
5CAC41222A1985AC00C331A2 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAC411D2A1985AB00C331A2 /* libffi.a */; };
|
||||
5CAC41232A1985AC00C331A2 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAC411E2A1985AB00C331A2 /* libgmpxx.a */; };
|
||||
5CAC41242A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAC411F2A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH-ghc8.10.7.a */; };
|
||||
5CAC41252A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CAC41202A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH.a */; };
|
||||
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; };
|
||||
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; };
|
||||
5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; };
|
||||
@@ -344,11 +344,6 @@
|
||||
5CA059D7279559F40002BEB4 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = "<group>"; };
|
||||
5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = "<group>"; };
|
||||
5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a"; sourceTree = "<group>"; };
|
||||
5CA487472A228AF300409F23 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CA487482A228AF300409F23 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CA487492A228AF400409F23 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSheet.swift; sourceTree = "<group>"; };
|
||||
5CA85D0A297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CA85D0B297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -360,6 +355,11 @@
|
||||
5CAC41192A192D8400C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CAC411A2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = "ja.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CAC411B2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5CAC411C2A1985AB00C331A2 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CAC411D2A1985AB00C331A2 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CAC411E2A1985AB00C331A2 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CAC411F2A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CAC41202A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH.a"; sourceTree = "<group>"; };
|
||||
5CADE79929211BB900072E13 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||
5CADE79B292131E900072E13 /* ContactPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPreferencesView.swift; sourceTree = "<group>"; };
|
||||
5CB0BA872826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@@ -498,12 +498,12 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CA4874D2A228AF400409F23 /* libffi.a in Frameworks */,
|
||||
5CA4874C2A228AF400409F23 /* libgmp.a in Frameworks */,
|
||||
5CA4874B2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a in Frameworks */,
|
||||
5CA4874A2A228AF400409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a in Frameworks */,
|
||||
5CAC41232A1985AC00C331A2 /* libgmpxx.a in Frameworks */,
|
||||
5CAC41212A1985AC00C331A2 /* libgmp.a in Frameworks */,
|
||||
5CAC41242A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH-ghc8.10.7.a in Frameworks */,
|
||||
5CAC41252A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CA4874E2A228AF400409F23 /* libgmpxx.a in Frameworks */,
|
||||
5CAC41222A1985AC00C331A2 /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -564,11 +564,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CA487482A228AF300409F23 /* libffi.a */,
|
||||
5CA487472A228AF300409F23 /* libgmp.a */,
|
||||
5CA487492A228AF400409F23 /* libgmpxx.a */,
|
||||
5CA487452A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB-ghc8.10.7.a */,
|
||||
5CA487462A228AF300409F23 /* libHSsimplex-chat-5.1.2.0-6dy8xe3EoSOA3hcKQSY0MB.a */,
|
||||
5CAC411D2A1985AB00C331A2 /* libffi.a */,
|
||||
5CAC411C2A1985AB00C331A2 /* libgmp.a */,
|
||||
5CAC411E2A1985AB00C331A2 /* libgmpxx.a */,
|
||||
5CAC411F2A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH-ghc8.10.7.a */,
|
||||
5CAC41202A1985AC00C331A2 /* libHSsimplex-chat-5.1.0.1-LZsKMJmAS7yI1az6ngjXWH.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1470,7 +1470,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 149;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1491,7 +1491,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.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 = 149;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1533,7 +1533,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.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 = 149;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.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 = 149;
|
||||
CURRENT_PROJECT_VERSION = 146;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1637,7 +1637,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 5.1.2;
|
||||
MARKETING_VERSION = 5.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
||||
@@ -495,7 +495,6 @@ 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 {
|
||||
@@ -610,7 +609,6 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .cmdOk: return "cmdOk"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
case .chatError: return "chatError"
|
||||
case .archiveImported: return "archiveImported"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -728,7 +726,6 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1251,7 +1248,6 @@ public enum ChatErrorType: Decodable {
|
||||
case invalidChatItemDelete
|
||||
case agentVersion
|
||||
case commandError(message: String)
|
||||
case exception(message: String)
|
||||
}
|
||||
|
||||
public enum StoreError: Decodable {
|
||||
@@ -1390,8 +1386,3 @@ public enum SMPAgentError: Decodable {
|
||||
case A_VERSION
|
||||
case A_ENCRYPTION
|
||||
}
|
||||
|
||||
public enum ArchiveError: Decodable {
|
||||
case `import`(chatError: ChatError)
|
||||
case importFile(file: String, chatError: ChatError)
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ public struct TimedMessagesPreference: Preference {
|
||||
}
|
||||
|
||||
public static var ttlValues: [Int?] {
|
||||
[600, 3600, 86400, 7 * 86400, 30 * 86400, 3 * 30 * 86400, nil]
|
||||
[3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, nil]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1995,9 +1995,6 @@
|
||||
/* 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,9 +1995,6 @@
|
||||
/* 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:" = "y después elige:";
|
||||
"above, then choose:" = "arriba y luego 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 eliminados.";
|
||||
"All data is erased when it is entered." = "Al introducirlo todos los datos son borrados.";
|
||||
|
||||
/* 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" = "Conecta vía enlace / Código QR";
|
||||
"Connect via link / QR code" = "Conectar mediante 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" = "Contacto";
|
||||
"Contact name" = "Nombre del 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" = "Crea enlace de invitación de un uso";
|
||||
"Create one-time invitation link" = "Crear enlace de invitación de un solo uso";
|
||||
|
||||
/* server test step */
|
||||
"Create queue" = "Crear cola";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Create secret group" = "Crea grupo secreto";
|
||||
"Create secret group" = "Crear 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 base de datos";
|
||||
"Database ID" = "ID de base de datos";
|
||||
|
||||
/* copied message info */
|
||||
"Database ID: %d" = "ID base de datos: %d";
|
||||
"Database ID: %d" = "ID de 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 en";
|
||||
"Delete after" = "Eliminar después de";
|
||||
|
||||
/* 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 en";
|
||||
"Delete messages after" = "Eliminar después de";
|
||||
|
||||
/* 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 la conexion pendiente?";
|
||||
"Delete pending connection?" = "¿Eliminar 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";
|
||||
"Deleted at" = "Eliminado el";
|
||||
|
||||
/* copied message info */
|
||||
"Deleted at: %@" = "Eliminado: %@";
|
||||
"Deleted at: %@" = "Eliminado el: %@";
|
||||
|
||||
/* 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á";
|
||||
"Disappears at" = "Desaparecerá el";
|
||||
|
||||
/* copied message info */
|
||||
"Disappears at: %@" = "Desaparecerá: %@";
|
||||
"Disappears at: %@" = "Desaparecerá el: %@";
|
||||
|
||||
/* 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 del perfil oculto";
|
||||
"Hidden profile password" = "Contraseña de perfil oculto";
|
||||
|
||||
/* chat item action */
|
||||
"Hide" = "Sin vista previa";
|
||||
"Hide" = "Ocultar";
|
||||
|
||||
/* 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 en";
|
||||
"Lock after" = "Bloquear después de";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Lock mode" = "Modo bloqueo";
|
||||
"Lock mode" = "Modo de 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" = "Contacto y texto";
|
||||
"Message text" = "Texto del mensaje";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Messages" = "Mensajes";
|
||||
@@ -1894,10 +1894,10 @@
|
||||
"moderated" = "moderado";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Moderated at" = "Moderado";
|
||||
"Moderated at" = "Moderado el";
|
||||
|
||||
/* copied message info */
|
||||
"Moderated at: %@" = "Moderado: %@";
|
||||
"Moderated at: %@" = "Moderado el: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"moderated by %@" = "moderado por %@";
|
||||
@@ -1995,9 +1995,6 @@
|
||||
/* 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";
|
||||
|
||||
@@ -2005,7 +2002,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- eliminar mensajes de los miembros.\n- desactivar el rol miembro (a rol \"observador\")";
|
||||
"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\")";
|
||||
|
||||
/* member role */
|
||||
"observer" = "observador";
|
||||
@@ -2300,10 +2297,10 @@
|
||||
"received answer…" = "respuesta recibida…";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Received at" = "Recibido";
|
||||
"Received at" = "Recibido el";
|
||||
|
||||
/* copied message info */
|
||||
"Received at: %@" = "Recibido: %@";
|
||||
"Received at: %@" = "Recibido el: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"received confirmation…" = "confirmación recibida…";
|
||||
@@ -2324,10 +2321,10 @@
|
||||
"Recipients see updates as you type them." = "Los destinatarios ven la actualizacion mientras escribes.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Record updated at" = "Registro actualiz.";
|
||||
"Record updated at" = "Registro actualizado";
|
||||
|
||||
/* copied message info */
|
||||
"Record updated at: %@" = "Registro actualiz: %@";
|
||||
"Record updated at: %@" = "Registro actualizado: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Reduced battery usage" = "Reducción del uso de la batería";
|
||||
@@ -2567,10 +2564,10 @@
|
||||
"Sending via" = "Enviando vía";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sent at" = "Enviado";
|
||||
"Sent at" = "Enviado el";
|
||||
|
||||
/* copied message info */
|
||||
"Sent at: %@" = "Enviado: %@";
|
||||
"Sent at: %@" = "Enviado el: %@";
|
||||
|
||||
/* notification */
|
||||
"Sent file event" = "Evento de archivo enviado";
|
||||
@@ -2915,7 +2912,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 **Mis perfiles**.";
|
||||
"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**.";
|
||||
|
||||
/* 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.";
|
||||
@@ -3143,7 +3140,7 @@
|
||||
"What's new" = "Novedades";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"When available" = "Si disponibles";
|
||||
"When available" = "Si disponible";
|
||||
|
||||
/* 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.";
|
||||
@@ -3227,7 +3224,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 tus mensajes:";
|
||||
"You can use markdown to format messages:" = "Puedes usar la sintaxis markdown para dar formato a los 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îtra le";
|
||||
"Disappears at" = "Disparaît à";
|
||||
|
||||
/* copied message info */
|
||||
"Disappears at: %@" = "Disparaîtra le : %@";
|
||||
"Disappears at: %@" = "Disparaît à : %@";
|
||||
|
||||
/* server test step */
|
||||
"Disconnect" = "Se déconnecter";
|
||||
@@ -1995,9 +1995,6 @@
|
||||
/* 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";
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
"%lld contact(s) selected" = "%lld 件の連絡先が選択されました";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"%lld file(s) with total size of %@" = "%lld 個のファイル(合計サイズ: %@)";
|
||||
"%lld file(s) with total size of %@" = "合計サイズ %@ の %lld 件のファイル";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"%lld members" = "%lld 人のメンバー";
|
||||
|
||||
@@ -1995,9 +1995,6 @@
|
||||
/* 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,9 +1995,6 @@
|
||||
/* 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,9 +19,6 @@
|
||||
/* 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. */
|
||||
", " = ", ";
|
||||
|
||||
@@ -91,12 +88,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"%@" = "%@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"%@ (current)" = "%@(当前)";
|
||||
|
||||
/* copied message info */
|
||||
"%@ (current):" = "%@(当前):";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"%@ / %@" = "%@ / %@";
|
||||
|
||||
@@ -118,9 +109,6 @@
|
||||
/* notification title */
|
||||
"%@ wants to connect!" = "%@ 要连接!";
|
||||
|
||||
/* copied message info */
|
||||
"%@:" = "%@:";
|
||||
|
||||
/* time interval */
|
||||
"%d days" = "%d 天";
|
||||
|
||||
@@ -196,15 +184,9 @@
|
||||
/* 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天";
|
||||
|
||||
@@ -223,15 +205,9 @@
|
||||
/* 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" = "新联系人";
|
||||
|
||||
@@ -250,9 +226,6 @@
|
||||
/* 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";
|
||||
|
||||
@@ -278,9 +251,6 @@
|
||||
/* 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" = "添加预设服务器";
|
||||
|
||||
@@ -299,9 +269,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Add welcome message" = "添加欢迎信息";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Address" = "地址";
|
||||
|
||||
/* member role */
|
||||
"admin" = "管理员";
|
||||
|
||||
@@ -311,15 +278,9 @@
|
||||
/* 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." = "所有群组成员将保持连接。";
|
||||
|
||||
@@ -329,9 +290,6 @@
|
||||
/* 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" = "允许";
|
||||
|
||||
@@ -344,12 +302,6 @@
|
||||
/* 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." = "允许向成员发送私信。";
|
||||
|
||||
@@ -368,9 +320,6 @@
|
||||
/* 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." = "允许您的联系人给您打电话。";
|
||||
|
||||
@@ -392,9 +341,6 @@
|
||||
/* 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" = "接听来电";
|
||||
|
||||
@@ -407,9 +353,6 @@
|
||||
/* 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" = "应用程序版本";
|
||||
|
||||
@@ -449,9 +392,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Authentication unavailable" = "身份验证不可用";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Auto-accept" = "自动接受";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Auto-accept contact requests" = "自动接受联系人请求";
|
||||
|
||||
@@ -473,15 +413,9 @@
|
||||
/* 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." = "您和您的联系人都可以不可逆转地删除已发送的消息。";
|
||||
|
||||
@@ -557,13 +491,6 @@
|
||||
/* 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" = "为您更改地址";
|
||||
|
||||
@@ -700,7 +627,7 @@
|
||||
"connecting (introduced)" = "连接中(已介绍)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"connecting (introduction invitation)" = "连接中(介绍邀请)";
|
||||
"connecting (introduction invitation)" = "连接(介绍邀请)";
|
||||
|
||||
/* call status */
|
||||
"connecting call" = "连接通话中……";
|
||||
@@ -771,9 +698,6 @@
|
||||
/* 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" = "复制";
|
||||
|
||||
@@ -783,9 +707,6 @@
|
||||
/* 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" = "创建文件";
|
||||
|
||||
@@ -804,9 +725,6 @@
|
||||
/* 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" = "创建您的资料";
|
||||
|
||||
@@ -825,12 +743,6 @@
|
||||
/* 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" = "深色";
|
||||
|
||||
@@ -852,9 +764,6 @@
|
||||
/* 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 和传输隔离选项。";
|
||||
|
||||
@@ -891,9 +800,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Database will be migrated when the app restarts" = "应用程序重新启动时将迁移数据库";
|
||||
|
||||
/* time unit */
|
||||
"days" = "天";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Decentralized" = "分散式";
|
||||
|
||||
@@ -1011,12 +917,6 @@
|
||||
/* deleted chat item */
|
||||
"deleted" = "已删除";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Deleted at" = "已删除于";
|
||||
|
||||
/* copied message info */
|
||||
"Deleted at: %@" = "已删除于:%@";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"deleted group" = "已删除群组";
|
||||
|
||||
@@ -1056,9 +956,6 @@
|
||||
/* authentication reason */
|
||||
"Disable SimpleX Lock" = "禁用 SimpleX 锁定";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Disappearing message" = "限时消息";
|
||||
|
||||
/* chat feature */
|
||||
"Disappearing messages" = "限时消息";
|
||||
|
||||
@@ -1068,12 +965,6 @@
|
||||
/* 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" = "断开连接";
|
||||
|
||||
@@ -1089,9 +980,6 @@
|
||||
/* 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" = "不再显示";
|
||||
|
||||
@@ -1107,9 +995,6 @@
|
||||
/* integrity error chat item */
|
||||
"duplicate message" = "重复的消息";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Duration" = "时长";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"e2e encrypted" = "端到端加密";
|
||||
|
||||
@@ -1137,12 +1022,6 @@
|
||||
/* 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 锁定";
|
||||
|
||||
@@ -1206,12 +1085,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Enter server manually" = "手动输入服务器";
|
||||
|
||||
/* placeholder */
|
||||
"Enter welcome message…" = "输入欢迎消息……";
|
||||
|
||||
/* placeholder */
|
||||
"Enter welcome message… (optional)" = "输入欢迎消息……(可选)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"error" = "错误";
|
||||
|
||||
@@ -1314,9 +1187,6 @@
|
||||
/* 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" = "发送消息错误";
|
||||
|
||||
@@ -1389,9 +1259,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Files & media" = "文件和媒体";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Finally, we have them! 🚀" = "终于我们有它们了! 🚀";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"For console" = "用于控制台";
|
||||
|
||||
@@ -1446,9 +1313,6 @@
|
||||
/* 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." = "群组成员可以不可撤回地删除已发送的消息。";
|
||||
|
||||
@@ -1512,12 +1376,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Hide:" = "隐藏:";
|
||||
|
||||
/* copied message info */
|
||||
"History" = "历史记录";
|
||||
|
||||
/* time unit */
|
||||
"hours" = "小时";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"How it works" = "工作原理";
|
||||
|
||||
@@ -1536,18 +1394,9 @@
|
||||
/* 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)." = "如果您现在需要使用聊天,请点击下面的**稍后再做**(当您重新启动应用程序时,系统会提示您迁移数据库)。";
|
||||
|
||||
@@ -1623,9 +1472,6 @@
|
||||
/* connection level description */
|
||||
"indirect (%d)" = "间接(%d)";
|
||||
|
||||
/* chat item action */
|
||||
"Info" = "信息";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Initial role" = "初始角色";
|
||||
|
||||
@@ -1662,9 +1508,6 @@
|
||||
/* group name */
|
||||
"invitation to group %@" = "邀请您加入群组 %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Invite friends" = "邀请朋友";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Invite members" = "邀请成员";
|
||||
|
||||
@@ -1716,9 +1559,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"italic" = "斜体";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Japanese interface" = "日语界面";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Join" = "加入";
|
||||
|
||||
@@ -1743,9 +1583,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Large file!" = "大文件!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Learn more" = "了解更多";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Leave" = "离开";
|
||||
|
||||
@@ -1758,9 +1595,6 @@
|
||||
/* rcv group event chat item */
|
||||
"left" = "已离开";
|
||||
|
||||
/* email subject */
|
||||
"Let's talk in SimpleX Chat" = "让我们一起在 SimpleX Chat 里聊天";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Light" = "浅色";
|
||||
|
||||
@@ -1845,15 +1679,6 @@
|
||||
/* 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" = "消息已收到";
|
||||
|
||||
@@ -1881,9 +1706,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Migrations: %@" = "迁移:%@";
|
||||
|
||||
/* time unit */
|
||||
"minutes" = "分钟";
|
||||
|
||||
/* call status */
|
||||
"missed call" = "未接来电";
|
||||
|
||||
@@ -1893,18 +1715,9 @@
|
||||
/* 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!" = "更多改进即将推出!";
|
||||
|
||||
@@ -1944,9 +1757,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"New database archive" = "新数据库存档";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"New display name" = "新显示名";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"New in %@" = "%@ 的新内容";
|
||||
|
||||
@@ -1995,9 +1805,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"No received or sent files" = "未收到或发送文件";
|
||||
|
||||
/* copied message info in history */
|
||||
"no text" = "无文本";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Notifications" = "通知";
|
||||
|
||||
@@ -2059,9 +1866,6 @@
|
||||
/* 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)." = "只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。";
|
||||
|
||||
@@ -2074,9 +1878,6 @@
|
||||
/* 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)." = "只有您的联系人才能不可撤回地删除消息(您可以将它们标记为删除)。";
|
||||
|
||||
@@ -2104,9 +1905,6 @@
|
||||
/* 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 上不受信任的链接将显示为红色。";
|
||||
|
||||
@@ -2215,9 +2013,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Preset server address" = "预设服务器地址";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Preview" = "预览";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Privacy & security" = "隐私和安全";
|
||||
|
||||
@@ -2236,21 +2031,12 @@
|
||||
/* 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." = "禁止向成员发送私信。";
|
||||
|
||||
@@ -2275,21 +2061,9 @@
|
||||
/* 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) 中阅读更多信息。";
|
||||
|
||||
@@ -2299,21 +2073,12 @@
|
||||
/* 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." = "即将停止接收文件。";
|
||||
|
||||
@@ -2323,12 +2088,6 @@
|
||||
/* 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" = "减少电池使用量";
|
||||
|
||||
@@ -2443,9 +2202,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Save archive" = "保存存档";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save auto-accept settings" = "保存自动接受设置";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save group profile" = "保存群组资料";
|
||||
|
||||
@@ -2467,9 +2223,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Save servers?" = "保存服务器?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save settings?" = "保存设置?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Save welcome message?" = "保存欢迎信息?";
|
||||
|
||||
@@ -2494,9 +2247,6 @@
|
||||
/* network option */
|
||||
"sec" = "秒";
|
||||
|
||||
/* time unit */
|
||||
"seconds" = "秒";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"secret" = "秘密";
|
||||
|
||||
@@ -2509,21 +2259,6 @@
|
||||
/* 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" = "发送";
|
||||
|
||||
@@ -2533,9 +2268,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Send direct message" = "发送私信";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send disappearing message" = "发送限时消息中";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send link previews" = "发送链接预览";
|
||||
|
||||
@@ -2566,18 +2298,9 @@
|
||||
/* 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." = "已发送的消息将在设定的时间后被删除。";
|
||||
|
||||
@@ -2605,9 +2328,6 @@
|
||||
/* 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" = "设置密码来导出";
|
||||
|
||||
@@ -2626,21 +2346,12 @@
|
||||
/* 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" = "在电话历史记录中显示通话";
|
||||
|
||||
@@ -2653,12 +2364,6 @@
|
||||
/* 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 审核。";
|
||||
|
||||
@@ -2734,12 +2439,6 @@
|
||||
/* 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";
|
||||
|
||||
@@ -2893,9 +2592,6 @@
|
||||
/* 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." = "要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。";
|
||||
|
||||
@@ -2959,9 +2655,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Unhide profile" = "取消隐藏个人资料";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unit" = "单位";
|
||||
|
||||
/* connection info */
|
||||
"unknown" = "未知";
|
||||
|
||||
@@ -3130,9 +2823,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"WebRTC ICE servers" = "WebRTC ICE 服务器";
|
||||
|
||||
/* time unit */
|
||||
"weeks" = "周";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome %@!" = "欢迎%@!";
|
||||
|
||||
@@ -3145,9 +2835,6 @@
|
||||
/* 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." = "当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。";
|
||||
|
||||
@@ -3199,9 +2886,6 @@
|
||||
/* 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。";
|
||||
|
||||
@@ -3214,9 +2898,6 @@
|
||||
/* 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." = "您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。";
|
||||
|
||||
@@ -3311,7 +2992,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: " = "您: ";
|
||||
@@ -3355,12 +3036,6 @@
|
||||
/* 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,7 +14,6 @@ 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,7 +16,6 @@ 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)
|
||||
|
||||
@@ -1,127 +1,13 @@
|
||||
---
|
||||
layout: layouts/article.html
|
||||
title: "SimpleX Chat v5.1 released: message reactions and self-destruct passcode"
|
||||
title: "SimpleX Chat: v5.1 released with message reactions and self-destruct passcode"
|
||||
date: 2023-05-23
|
||||
image: images/20230523-reactions.png
|
||||
imageBottom: true
|
||||
previewBody: blog_previews/20230523.html
|
||||
# image: images/20230422-video.png
|
||||
# imageBottom: true
|
||||
# previewBody: blog_previews/20230422.html
|
||||
permalink: "/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html"
|
||||
---
|
||||
|
||||
# SimpleX Chat v5.1 released: message reactions and self-destruct passcode
|
||||
# SimpleX Chat: v5.1 released with message reactions and self-destruct passcode
|
||||
|
||||
**Published:** May 23, 2023
|
||||
|
||||
What's new in v5.1:
|
||||
|
||||
- [message reactions](#message-reactions) 🚀
|
||||
- [improved messages](#improved-messages):
|
||||
- [voice messages up to 5 minutes](#voice-messages-up-to-5-minutes-better-quality-playback-control).
|
||||
- [custom time to disappear](#disappearing-messages-choose-any-time).
|
||||
- [message editing history](#message-editing-history).
|
||||
- new design and [customizable themes](#customize-and-share-color-themes) that you can share!<sup>*</sup>
|
||||
- [self-destruct passcode](#self-destruct-passcode)
|
||||
|
||||
There are lots of small improvements and fixes in this release:
|
||||
- setting to disable audio/video calls per contact.
|
||||
- group welcome message visible in a group profile - add the link to join the group there.
|
||||
- fixes for sending files.
|
||||
- configurable SOCKS proxy host and port<sup>*</sup>.
|
||||
- improved calls on lock screen<sup>*</sup>.
|
||||
- locale-dependent formatting of time and date<sup>*</sup>.
|
||||
|
||||
Also, we added Japanese and Portuguese (Brazil)<sup>*</sup> interface languages, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat).
|
||||
|
||||
\* These features are available in the Android version only.
|
||||
|
||||
## Message reactions
|
||||
|
||||
<img src="./images/20230523-reactions.png" width="288">
|
||||
|
||||
No idea why it took us so long to add them – finally we have them, and they are great.
|
||||
|
||||
We didn't like the view of other messengers plagued by 40 different reactions on a message, and we were not quite sure what can be done about it, so we initially decided to limit available reactions to just 6: thumbs up 👍, down 👎, smile 😀, tear 😢, heart ❤️ and rocket 🚀. But unlike some other messengers, we don't limit the reactions to just one per message - you can put up to 3.
|
||||
|
||||
You can add them via the menu, or just by tapping the reaction already on the message.
|
||||
|
||||
The next app version will allow prohibiting the reactions per conversation, as you asked. Some groups work better with more substantive responses.
|
||||
|
||||
## Improved messages
|
||||
|
||||
### Voice messages: up to 5 minutes, better quality, playback control
|
||||
|
||||
<img src="./images/20230523-voice.png" width="288">
|
||||
|
||||
Since [v4.3](./20221206-simplex-chat-v4.3-voice-messages.md#instant-voice-messages) voice messages were sent in small 16kb chunks, so we had to limit them to 30-40 seconds for better user experience, as sending larger files would require the sender to be online.
|
||||
|
||||
With the new XFTP protocol that allows sending files up to 1gb it is now possible to increase the message size - we decided to limit it at 5 min, increasing their quality 2x (the bitrate is increased from 16 to 32kbps). We also added a slider to control the playback.
|
||||
|
||||
### Disappearing messages: choose any time!
|
||||
|
||||
<img src="./images/20230523-disappearing1.png" width="288"> <img src="./images/20230523-disappearing2.png" width="288">
|
||||
|
||||
Since [v4.4](./20230103-simplex-chat-v4.4-disappearing-messages.md#disappearing-messages) disappearing messages were only configurable for the whole conversation, with a limited choice of allowed times between 30 seconds and 1 month.
|
||||
|
||||
This version allows to configure the time for messages to disappear more granularly, in the range from 1 second to 3 months. You can also set a different time to disappear when you send the message (hold the Send button), as long as disappearing messages are enabled in the conversation.
|
||||
|
||||
### Message editing history
|
||||
|
||||
<img src="./images/20230523-info.png" width="288">
|
||||
|
||||
I [wrote previously](./20221206-simplex-chat-v4.3-voice-messages.md#irreversible-message-deletion) why we decided to require the recipient concent before the messages can be fully deleted by the sender - in short, it is to support recipient's data sovereignty and prevent the possibility of offensive messages being removed without any trace. By default, when the sender deletes the message it is marked as deleted, rather than fully deleted, and you can reveal the original message.
|
||||
|
||||
You've found the workaround for it of course - it's enough to simply edit the message to hide the original content. This version adds a visible message editing history, together with other detailed information about when the message was sent, received or deleted, that wasn't visible in the conversation.
|
||||
|
||||
## Customize and share color themes
|
||||
|
||||
<img src="./images/20230523-theme.png" width="288">
|
||||
|
||||
Android app now allows choosing between three color themes - Light, Dark and SimpleX (a dark blue theme). You can customize any theme by setting 9 different colors used in the app, including titles, menus, accent colors and colors for sent and received messages.
|
||||
|
||||
You can share your theme with other users by exporting it to a file and sending to them. You can also [contribute your favorite themes](../docs/THEMES.md) to our GitHub repository.
|
||||
|
||||
## Self-destruct passcode
|
||||
|
||||
<img src="./images/20230523-self-destruct.png" width="288">
|
||||
|
||||
This is something many of you asked before - when asked to enter the app passcode under duress, to be able to enter a special self-destruct code that would remove the app data. This feature is offered in many security tools, and now you can configure it in SimpleX Chat as well.
|
||||
|
||||
To enable it:
|
||||
- go to app Settings > Privacy & security > SimpleX Lock.
|
||||
- switch lock mode to Passcode and, if necessary, enable lock – Self-destruct passcode will become available.
|
||||
- enable it, enter the main passcode and then enter and confirm self-destruct passcode.
|
||||
- optionally, you can set the new display name of the profile that will be created after the data was wiped. If it is left blank, a new empty profile will have a random name.
|
||||
|
||||
## SimpleX platform
|
||||
|
||||
Some links to answer the most common questions:
|
||||
|
||||
[SimpleX Chat security assessment](./20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
[How can SimpleX deliver messages without user identifiers](https://simplex.chat/#how-simplex-works).
|
||||
|
||||
[What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy).
|
||||
|
||||
[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations).
|
||||
|
||||
[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions).
|
||||
|
||||
Visit our [website](https://simplex.chat) to learn more.
|
||||
|
||||
## Help us with donations
|
||||
|
||||
Huge thank you to everybody who donated to SimpleX Chat!
|
||||
|
||||
We are prioritizing users privacy and security - it would be impossible without your support.
|
||||
|
||||
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
|
||||
|
||||
Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us.
|
||||
|
||||
See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate.
|
||||
|
||||
Thank you,
|
||||
|
||||
Evgeny
|
||||
|
||||
SimpleX Chat founder
|
||||
This is a placeholder for the release announcement.
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
# Blog
|
||||
|
||||
May 23, 2023 [SimpleX Chat v5.1 released](./20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md)
|
||||
|
||||
What's new in v5.1:
|
||||
- message reactions 🚀
|
||||
- improved messages:
|
||||
- voice messages up to 5 minutes.
|
||||
- custom time to disappear.
|
||||
- message editing history.
|
||||
- new design and customizable themes that you can share!<sup>*</sup>
|
||||
- self-destruct passcode
|
||||
|
||||
Also, we added Japanese and Portuguese (Brazil)<sup>*</sup> interface languages, thanks to our users and Weblate.
|
||||
|
||||
\* These features are available in the Android version only.language interface, thanks to our users and Weblate!
|
||||
|
||||
---
|
||||
|
||||
Apr 22, 2023 [SimpleX Chat: vision and funding, v5.0 released](./20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md)
|
||||
|
||||
SimpleX Chat vision and funding:
|
||||
@@ -31,8 +14,6 @@ What's new in v5.0:
|
||||
|
||||
Also, we added Polish language interface, thanks to our users and Weblate!
|
||||
|
||||
---
|
||||
|
||||
Mar 28, 2023 [SimpleX Chat v4.6 released](./20230328-simplex-chat-v4-6-hidden-profiles.md)
|
||||
|
||||
- ARMv7a and Android 8+ support!
|
||||
@@ -45,14 +26,10 @@ Mar 28, 2023 [SimpleX Chat v4.6 released](./20230328-simplex-chat-v4-6-hidden-pr
|
||||
|
||||
Also, we added Chinese and Spanish language interface, thanks to our users and Weblate!
|
||||
|
||||
---
|
||||
|
||||
Mar 1, 2023 [SimpleX File Transfer Protocol](./20230301-simplex-file-transfer-protocol.md)
|
||||
|
||||
A new protocol for sending large files efficiently, privately and securely. It will be soon integrated into SimpleX Chat apps.
|
||||
|
||||
---
|
||||
|
||||
Feb 4, 2023 [SimpleX Chat v4.5 released](./20230204-simplex-chat-v4-5-user-chat-profiles.md)
|
||||
|
||||
- multiple chat profiles.
|
||||
@@ -63,8 +40,6 @@ Feb 4, 2023 [SimpleX Chat v4.5 released](./20230204-simplex-chat-v4-5-user-chat-
|
||||
|
||||
Also, we added Italian language interface, thanks to our users and Weblate!
|
||||
|
||||
---
|
||||
|
||||
Jan 3, 2023 [SimpleX Chat v4.4 released](./20230103-simplex-chat-v4.4-disappearing-messages.md)
|
||||
|
||||
- disappearing messages.
|
||||
@@ -74,8 +49,6 @@ Jan 3, 2023 [SimpleX Chat v4.4 released](./20230103-simplex-chat-v4.4-disappeari
|
||||
|
||||
Also, we added French language interface, thanks to our users and Weblate!
|
||||
|
||||
---
|
||||
|
||||
Dec 6, 2022 [SimpleX Chat reviews and v4.3 released](./20221206-simplex-chat-v4.3-voice-messages.md)
|
||||
|
||||
November reviews:
|
||||
@@ -85,8 +58,6 @@ November reviews:
|
||||
- [The messenger matrix](https://www.messenger-matrix.de).
|
||||
- [Supernova review](https://supernova.tilde.team/detailed_reviews.html#simplex) and [messenger ratings](https://supernova.tilde.team/messengers.html).
|
||||
|
||||
---
|
||||
|
||||
v4.3 is released:
|
||||
|
||||
- instant voice messages!
|
||||
@@ -102,8 +73,6 @@ SimpleX Chat has now been audited by [Trail of Bits](https://www.trailofbits.com
|
||||
|
||||
The new website is live: https://simplex.chat
|
||||
|
||||
---
|
||||
|
||||
v4.2 is released:
|
||||
|
||||
- group links - group admins can create the links for new members to join
|
||||
@@ -112,8 +81,6 @@ v4.2 is released:
|
||||
- manually switch contact or member to another address / server (BETA)
|
||||
- receive files faster (BETA)
|
||||
|
||||
---
|
||||
|
||||
Sep 28, 2022 [v4: local database encryption](./20220928-simplex-chat-v4-encrypted-database.md)
|
||||
|
||||
- encrypted local chat database - if you already use the app, you can encrypt the database in the app settings
|
||||
@@ -124,8 +91,6 @@ Sep 28, 2022 [v4: local database encryption](./20220928-simplex-chat-v4-encrypte
|
||||
- support animated images in Android app
|
||||
- German language in mobile apps UI
|
||||
|
||||
---
|
||||
|
||||
Sep 1, 2022 [v3.2: Incognito mode](./20220901-simplex-chat-v3.2-incognito-mode.md)
|
||||
|
||||
- Incognito mode - use a new random profile name for each contact
|
||||
@@ -136,8 +101,6 @@ Sep 1, 2022 [v3.2: Incognito mode](./20220901-simplex-chat-v3.2-incognito-mode.m
|
||||
|
||||
Implementation audit is arranged for October!
|
||||
|
||||
---
|
||||
|
||||
Aug 8, 2022 [v3.1: chat groups](./20220808-simplex-chat-v3.1-chat-groups.md)
|
||||
|
||||
- finally, secret chat groups - nobody but members know they exist!
|
||||
@@ -146,8 +109,6 @@ Aug 8, 2022 [v3.1: chat groups](./20220808-simplex-chat-v3.1-chat-groups.md)
|
||||
- published chat protocol
|
||||
- new app icons
|
||||
|
||||
---
|
||||
|
||||
Jul 23, 2022 [v3.1-beta: access servers via Tor](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md)
|
||||
|
||||
- terminal app: access to messaging servers via SOCKS5 proxy (e.g., Tor).
|
||||
@@ -155,54 +116,30 @@ Jul 23, 2022 [v3.1-beta: access servers via Tor](./20220723-simplex-chat-v3.1-to
|
||||
- optimized battery and traffic usage - up to 90x reduction!
|
||||
- two docker configurations for self-hosted SMP servers.
|
||||
|
||||
---
|
||||
|
||||
Jul 11, 2022 [v3: instant push notifications for iOS and audio/video calls](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md):
|
||||
|
||||
- chat database export and import
|
||||
- end-to-end encrypted audio/video calls
|
||||
- protocol privacy and performance improvements
|
||||
|
||||
---
|
||||
|
||||
Jun 4, 2022 [v2.2: the new Privacy and Security settings](./20220604-simplex-chat-new-privacy-security-settings.md)
|
||||
|
||||
---
|
||||
|
||||
May 24, 2022 [v2.1: clearing messages for better conversation privacy](./20220524-simplex-chat-better-privacy.md)
|
||||
|
||||
---
|
||||
|
||||
May 11, 2022 [v2.0 released - sending images and files in mobile apps](./20220511-simplex-chat-v2-images-files.md)
|
||||
|
||||
---
|
||||
|
||||
Apr 04, 2022 [Instant notifications for SimpleX Chat mobile apps](./20220404-simplex-chat-instant-notifications.md)
|
||||
|
||||
---
|
||||
|
||||
Mar 08, 2022 [Mobile apps for iOS and Android released!](./20220308-simplex-chat-mobile-apps.md)
|
||||
|
||||
---
|
||||
|
||||
Feb 14, 2022. [SimpleX Chat: join our public beta for iOS!](./20220214-simplex-chat-ios-public-beta.md)
|
||||
|
||||
---
|
||||
|
||||
Jan 12, 2022. [SimpleX Chat v1 released: the most private and secure chat and application platform](./20220112-simplex-chat-v1-released.md)
|
||||
|
||||
---
|
||||
|
||||
Dec 08, 2021. [SimpleX Chat v0.5 released: the first chat platform that is 100% private by design - no access to your connections graph](./20211208-simplex-chat-v0.5-released.md)
|
||||
|
||||
---
|
||||
|
||||
Sep 14, 2021. [SimpleX Chat v0.4 released: open-source chat that uses privacy-preserving message routing protocol](./20210914-simplex-chat-v0.4-released.md)
|
||||
|
||||
---
|
||||
|
||||
May 12, 2021. [SimpleX Chat Prototype!](./20210512-simplex-chat-terminal-ui.md)
|
||||
|
||||
---
|
||||
|
||||
Oct 22, 2020. [SimpleX Chat](./20201022-simplex-chat.md)
|
||||
|
||||
|
Before Width: | Height: | Size: 378 KiB |
|
Before Width: | Height: | Size: 350 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 531 KiB |
|
Before Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 363 KiB |
|
Before Width: | Height: | Size: 110 KiB |
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 89caf5572980b776bd750caa3c918ae4488612d8
|
||||
tag: 6c6f22051d693e00aaa4c4e6c5b6bff7e9a40b36
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
# 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).
|
||||
@@ -1,82 +0,0 @@
|
||||
# Identity layer for SimpleX Chat
|
||||
|
||||
## Motivation
|
||||
|
||||
While lack of global identity provides high level of meta-data privacy and there are communication scenarios where not having to use public identity is desirable (e.g., a customer asking a question in online store before making a purchase), many other communication scenarios require identity (e.g., online store itself would benefit from having a public address via which it can be connected to).
|
||||
|
||||
We have already added [user contact addresses]() represented as links/QR codes, but they are impossible to memorize or to say in the conversation - they require digital transmission, while in many scenarios analogue transmission would be more convenient.
|
||||
|
||||
There is already a global addressing system in use - email addresses. Email is the only communication system in existence that is fully decentralized and has a quality called "address portability" - that is, users can keep an address while changing the server provider. It is not true in 100% of cases - e.g. if you have an address in gmail.com domain, you cannot move it to another provider. But if you use Google Mail to host your email address in another domain, e.g. as I do with my evgeny@poberezkin.com address, you can move it to another provider without any restriction - it only requires a simple change in the domain DNS record.
|
||||
|
||||
Email identities have multiple problems though:
|
||||
- your providers do not only know that you have an identity on the network, they also know everybody who sends you messages.
|
||||
- you cannot change your address without losing your contacts - you have to tell all your contacts your new address.
|
||||
- providers cannot guarantee, in most cases, that the sender of the email has the address they claim they have. At best, they can check the sending domain using DKIM and SPKIF, but these measures are not enabled by default, require complex additional configuration and most small users do not configure it correctly.
|
||||
|
||||
The problems with our current user contact address:
|
||||
- it allows to send traffic directly to user's device (via SMP server), there is no proxy to implement anti-spam/anti-DoS protection. While we can mitigate it by throttling the reception of contact requests on the client side, the attacker can keep the queue full and make the address unreachable. And if we don't throttle, the client can receive unlimited amount of traffic via contact requests and run out of disk space. Having identity server as a proxy allows to manage this problem.
|
||||
|
||||
The identity layer we add to SimpleX Chat has to keep the advantages of email addressing and to solve these problems as well.
|
||||
|
||||
## Product requirements
|
||||
|
||||
- the identity of the user in SimpleX Chat must be the same as the email address they have access to - the identity server should prove it while provisioning the address.
|
||||
- the email access should not be used to access SimpleX address once it's provisioned.
|
||||
- the identity server and network observers should not know who sends messages to the user's identity - sender's addresses should be only visible to the recipient.
|
||||
- changing or deleting address/identity should not make you losing the contacts - we already have this quality with our user contact addresses.
|
||||
- for every email address user possesses, they should be able to create multiple SimpleX addresses that can be used in different contexts using SMTP `+` extension syntax (TBC what it is called). This extension in case of SimpleX should be meaningful - that is if a given address does not exist, the address should not be reachable.
|
||||
- Possibly, the user should be able to decide whether the server should screen for known `+` addresses or if it should be screened on the device, where the server only checks the main part of the address or uses some patterns for `+` part. It's a trade-off - screening on the device hides the actual addresses from the identity server, but increases the amount of spam traffic the device has to process, while screening on the server reduces the spam traffic, but reduces user privacy, as the server knows all the sub-addresses. From the UX point of view there would be no difference - the user will not see non-existing `+` extensions, unless they choose to receive connection requests to their main address (that could be put in a separate folder).
|
||||
- The user should be able to decide whether they want to receive connection requests from users without verified SimpleX addresses (all addresses are verified).
|
||||
- The senders' addresses should be cryptographically verified by delivering some probe to that address and receiving the reply, transparently to the user, before the connection request is shown to the user.
|
||||
- Identity server should be accessed via chat clients, it should not have its own separate UI.
|
||||
- The client of the address owner should be able to periodically confirm that the identity server does not perform MITM attack on the address to see the senders (by substituting the owner's public key for that address).
|
||||
|
||||
Post MVP:
|
||||
- we should support identity transfer, e.g. if email changes the owner.
|
||||
- we should have some cool-down period for identity transfer, to the reduce risks of malicious address takeover.
|
||||
|
||||
## Design considerations
|
||||
|
||||
- Identity server should communicate with the clients via SMP protocol
|
||||
- It probably should use the same message format as we use for chat, but in a different namespace (e.g., `i` - we currently use `x` for chat messages).
|
||||
- Having received the request to provision the address, it should send an email to this address to verify it, following the best anti-SPAM practices and allowing recipients to opt out of further emails.
|
||||
- Client should determine which SimpleX identity server to use via some DNS record we decide on - we should support TXT and see if we can register something in IANA for DNS servers that support custom records.
|
||||
- In case domain does not have registered identity server, client could default to hard-coded identity server we provide, that would have a list of domains we allow creating addresses in.
|
||||
|
||||
## Preliminary protocol design
|
||||
|
||||
It is assumed that identity server has it's own contact address, with the same syntax as user's contact address, but with a different mode, e.g. `identity`. Or, possibly, we should use the same `contact` namespace. TBC
|
||||
|
||||
### Establishing permanent connections with identity server
|
||||
|
||||
This connection is only needed for requesting and managing provisioned addresses - this connection identifies the user to the server. As some actions must be anonymous, they should not be sent via this connection.
|
||||
|
||||
To provision the connection the client would send `INV` agent message, same as when connecting with other users, but without any user profile.
|
||||
|
||||
### Requesting the address
|
||||
|
||||
1. Client sends `i.addr.new <email> <contact_connection_request> <public_signature_key>` message to identity server via established permanent connection (`contact_connection_request` will be used by the server to forward connection requests to, `public_signature_key` would be used to confirm the identity to the server on all operations with the address. Possibly we don't need it and can rely on SMP authorization/connections, TBC). `email` can include `+` extension or be catch all.
|
||||
2. Identity server responds with `i.addr.id <address_id>` message to confirm that the request is received and being processed.
|
||||
3. Unless the main address was already confirmed (e.g. when the client requests additional `+` extension to the previously created address), the identity server will send email to this address with the unique connection request uri that the user should use to confirm this address. Possibly it should have separate mode, e.g. `confirm_address` TBC.
|
||||
4. Once the user confirms or rejects the address.
|
||||
- If address is confirmed (see below), the server will:
|
||||
- create new SMP queue(s) for the address that it would process.
|
||||
- send `i.addr.conf <address_id> <connection_request_uri>` message to confirm that this identity is now associated with the user (including the created queue(s)). Public key in `connection_request_uri` must be the same as the one the user passed to the server, if it is different it means server does not function correctly and this address should not be used.
|
||||
- if the address is rejected, the server will send `i.addr.rejected <address_id> <error_code>`. Servers can have some policies to allow limited number of rejections per confirmed connection, and also block creating the new connections from IP addresses that create too many address requests (and in any case should throttle it per IP address - not MVP problem to solve).
|
||||
|
||||
Once the address is confirmed the chat client will notify the user, and the user can configure in the client which `+` extensions should be allowed (in case catch-all was registered with the server) - not MVP. The user can now share this address with other users.
|
||||
|
||||
### Sending contact requests to the address
|
||||
|
||||
- the sender's client should determine which identity server to use - either via DNS lookup and in case the server is absent, via our identity server.
|
||||
- the sender's client should send message to the identity server's unsecured public queue. From SMP syntax perspective it should probably be SMP confirmation, TBC, the message itself will be `i.addr.req <email>` - note that this does not expose sender's identity in any way.
|
||||
- the identity server should respond with a message queue provisioned specifically for this address with `i.addr.con <connection_request>`, where `connection_request` is the one created specifically for this address and contains recipient's public key.
|
||||
|
||||
The process above can be used by the address owner to periodically verify that the server does not substitute recipient's public key.
|
||||
|
||||
The server should always respond with `i.addr.con <connection_request>` even if the address is not registered, to avoid address-scanning - the response time should be consistent, so the server would have a pool of queues for such invalid responses.
|
||||
|
||||
- having received this response, the sender would send the message encrypted with the recipient's public key (or derived key from recipient's public key and sender's public key sent together with the message) e.g. `i.addr.hello <sender_address> <public_key>`
|
||||
- the server forwards it to the recipient (the server cannot decrypt this message), with some anti-DDoS filters.
|
||||
- recipient decrypts the message and repeats the same process with the sender's address. This message would be double encrypted, to avoid sharing recipient's address, in case sender used somebody else's address.
|
||||
- if the sender used the address they control they would receive the confirmation from the recipient, and would be able to send this confirmation back at which point the recipient user will be notified.
|
||||
@@ -1,176 +0,0 @@
|
||||
# Groups
|
||||
|
||||
## Problems
|
||||
|
||||
1. The current group design is based on message broadcast by the sender to all group members, requiring a fully connected graph between members. This results in the linear scaling of the traffic for senders, making it not viable for large groups.
|
||||
|
||||
2. As connection handshake is mediated by the introducer, there are multiple steps to establish direct connections between members, and while all these messages are asynchronous thanks to the relays, they require multiple steps, and can take arbitrarily long time to complete.
|
||||
|
||||
3. Group state is incosistent in all parts: group profile, group membership and group messages. While _some_ efforts are made to avoid this inconsistency (pending messages while members are being connected, profile changes only allowed to group owners), there is no conflict resolution strategy, and no mechanism to validate group integrity. In addition to race conditions and slow connections between members that may result in inconsistent state between members, there is also nothing that prevents members from sending different messages to different members - integrity validation is only present withing each connection between members, but not for the whole group.
|
||||
|
||||
## Design goals
|
||||
|
||||
1. Strong consistency of messages, membership and group configuration - with conflict resolution and integrity validation.
|
||||
2. Availability of past messages, as defined by group policy. This implies the need for multi-hop delivery, and it also that a fully connected graph is not required.
|
||||
3. Better scaling of sending of the group messages - as `O(log N)` or as `O(log log N)`, at the cost of increasing the cost to receive the messages. It will result on higher average traffic per member with much lower variance of that traffic.
|
||||
4. Member roles would imply their different weight in conflict resolution and different per-member traffic - groups should be more costly for owners and admins, in terms of traffic, although there may be a separate flag for "high-traffic members".
|
||||
5. Group integrity validation to ensure the consistent state for all members - message DAG (with support for irreversible deletion, but otherwise immutable), conversation state (mutable), membership roster, group profile and policies/preferences. That implies the requirement for deterministic conflict resolution strategy.
|
||||
6. Efficient mechanism to catch up with missed messages (e.g., member was offline, or the group delivery was suspended - there are many requests for such features) - via files, with explicit support for history gaps.
|
||||
|
||||
## Design ideas / approaches
|
||||
|
||||
### Abstractions for group state, integrity and conflict resolution
|
||||
|
||||
Group can be considered as a distributed state machinem, that requires some state machine replication protocol to achieve consistency, with the following components of the state:
|
||||
|
||||
#### Events history.
|
||||
|
||||
There are some recent attempts [1], [2] to agree on a single linear history of events, when initially due to a concurrency a state machine history forks into a tree, and then the job of participating nodes to agree on a single linear history of events. This is a general approach in most cryptocurrency blockchains where linear history of transations prevents double spends.
|
||||
|
||||
The consensus about linear history seems costly to achieve and in the context of group communication seems unnecessary in most cases. This is different from cryptocurrency event history, where all events represent the change of ownership in some asset, and therefore there should be a consensus about the sequence of all events. But even in cryptocurrency blockchains this could have been achieved without a single linear history if there were some additional restriction imposed on each asset record that there can be no more than one transaction per asset between some comparatively rare "finalization/commit" records that merge all forks, and no linearity required between these events. In case of cryptocurrency blockchain it would substantially increase throughput of all transactions at the cost of reducing the frequency of transactions with each given asset.
|
||||
|
||||
```
|
||||
A (finalized state)
|
||||
/ \
|
||||
B C
|
||||
\ / \
|
||||
D E
|
||||
\ /
|
||||
F (finalized state)
|
||||
```
|
||||
|
||||
The diagram above is a sample of events graph (directed acyclic graph, or DAG) where each event references one or several parents by incuding their identifiers (consisting of peer identifier and sequential message number from this peer) and hashes to validate the integrity of this graph for all peers (Merkle DAG). If each event represents some irreversible action (e.g. transaction with the asset in a blockchain), then only one such event is allowed between finalized states A and F, and if more than one event happens between these states then all such events are rejected.
|
||||
|
||||
The advantage of group communication over blockchain is that there is no strong requirement to have an agreement on the linear history of events, except when events change some parts of group state (see below) in a contradictory way. Most events in the group communication are simple message additions to the conversation history, and while it has to be generally consistently ordered, some inconsistency can be preferable from the point of user experience.
|
||||
|
||||
Consider different approaches to the user experience in the presence of concurrently sent messages. Approach 1 dictates that messages can be only appended to the group history as it is seen by the user, but without gaps in DAG. That means that once the peer appends the message nothing can be appended above it, but the received messages can be inserted in linearised view of message history (this needs to be better formalized what "avoiding gaps in DAG" means, but in general, event parents should be above the child in the linearized history, even if they are received after the child, and the ordering between many parents of the same child, and between many children of the same set of parents, can be determined by some timestamp or some other attribute of these parents or children. It cannot be determined by the child itself, as there can be more than one child referencing the same set of parents, and they would define a different order).
|
||||
|
||||
```
|
||||
A B
|
||||
| X |
|
||||
C D - both these childs would refer to parents A and B, but they may define different order
|
||||
```
|
||||
|
||||
Such user experience is present, for example, in WhatsApp, and its benefit is that a peer is less likely to miss messages added to the past of the message history.
|
||||
|
||||
Approach 2 dictates that all peers should have the same linear history of messages, even if it means inserting messages very early in the conversation history. In case of centralized system this order can be maintained quite easily by using the server timestamp. In case of decentralized system we cannot rely on timestamps at all, as clocks can be out of sync, and timestamp-based ordering (that SimpleX Chat uses now) would contradict parent-is-always-above principle (such violations are experienced in SimpleX Chat as replies to messages are shown above the messages themselves). There are multiple approaches on resolving these conflicts, including sorting messages by their hash.
|
||||
|
||||
The advantage of approach 2 is that events will be in same order for all peers, and in case some members see local peer replies under some messages (that may be unrelated to this reply), all other members, including the local peer. The disadvantage is that messages can be missed.
|
||||
|
||||
Imagine this conversation in both approaches:
|
||||
|
||||
```
|
||||
Approach 1:
|
||||
|
||||
Alice view Bob view Charlie view
|
||||
---------- ---------- ------------
|
||||
B: let's make smth
|
||||
B: let's make smth B: let's make smth
|
||||
A: I'm in! A: I'm in! C: better let's break smth
|
||||
C: better let's break smth C: better let's break smth A: I'm in!
|
||||
|
||||
or Bob can see C, A
|
||||
|
||||
|
||||
Approach 2:
|
||||
|
||||
Alice view Bob view Charlie view
|
||||
---------- ---------- ------------
|
||||
B: let's make smth
|
||||
B: let's make smth B: let's make smth
|
||||
C: better let's break smth C: better let's break smth
|
||||
C: better let's break smth A: I'm in! A: I'm in!
|
||||
A: I'm in!
|
||||
|
||||
```
|
||||
|
||||
Alice clearly agreed to make something, but to Charlie it looks like she agreed to break something, and to Bob it can look either way, depending to the approach to ordering of two children. What is worse in this case, compared to the approach 2, is that Alice is unaware of it, but even if she were aware, she still cannot prove later to what she agreed without access to the parents of the event.
|
||||
|
||||
In case of approach 2 the conversation history changes in a way that it can be missed, but in the end it converges to the same linear view.
|
||||
|
||||
In both cases having only the linear history is insufficient for peers to prove what was the context of their messages, and this context defines the meaning - all peers should have visibility of the actual graph.
|
||||
|
||||
In the presense of such graph visibility, the Approach 1 seems better, as it reduces changes in the linear conversation history, provides a better user experience, and reduces the likelihood of missing the message.
|
||||
|
||||
Irrespective of how the DAG of events is presented and interpreted, DAG itself can be synchronised between members, as in the absense of malicious peers it has no conflicts by design - members can concurrently extend this graph at any time, referencing all peers visible to them at a time.
|
||||
|
||||
Some peers may fail to include all parents visible to them, and that would reduce graph linearity, but it won't by itself create conflicts. This can be addressed by a policy that messages failing to reference parents older than some threshold are rejected, so that group cannot be in a partitioned state indefinitely.
|
||||
|
||||
This graph acts as a proof that messages were delivered.
|
||||
|
||||
#### Group profile.
|
||||
|
||||
Group profile includes all any visible or invisible data about group available to all members, and also all conversation preferences and policies. Currently we only allow modifying the group profile to group owners, to reduce the possibility of the conflicts, without any conflict resolution strategy.
|
||||
|
||||
To achieve consistency of group profile changes the following can be considered:
|
||||
|
||||
1. Profile changes can be split to multiple event types, to reduce the possibility of conflicts. While it doesn't solve the problem of conflict resolution, it makes them less likely, and improves user experience.
|
||||
|
||||
2. Even without splitting profile changes to multiple vent types, the scope of the change can be determined as the diff between the previous and the new state.
|
||||
|
||||
3. Similar idea to having scoped or unscoped "commit" events can be applied to profile changes, so that if two changes happen between commit events, only one of them will be accepted. This requires sending such "commit" events periodically, even in the absense of changes.
|
||||
|
||||
#### Group membership
|
||||
|
||||
This is the list of all members and their permissions. Events adding members do not create conflicts, even if they happen in different branches of the trea. Events removing members and changing members roles can create conflicts, for example in cases when two admin members attempt to remove each other concurrently.
|
||||
|
||||
To resolve conflict, there can be three approaches (that also applies to group profile changes):
|
||||
|
||||
- agree on event ordering. That will make one event succeed, and another rejected. This ordering has to be agreed consistently by all members, so either some consensus protocol is needed, which is complex and slow, or some arbitrary prioritization, e.g. ordering by peer ID, but this seems arbitrary, unfair or revealing additional metadata (e.g. if seniority of peers is determined by the time of joining the group).
|
||||
- let both conflicting events succeed, when possible. This can be possible when members try to remove each other concurrently, or to downgrade the role. This approach seems bad, as it can leave the group in the unmanageable state (all owners removed), and it is not possible in general case when it is applied to the same member or to group profile.
|
||||
- let both events fail. This seems the most reasonable approach, but it means that the action either should be delayed until "commit" event (e.g., when the action is destructive, irreversible, or can prevent some other actions before commit - e.g. removing member or role downgrade), or it may need to be reversed, and, possibly, subsequent actions need to be replaced.
|
||||
|
||||
#### Group operation
|
||||
|
||||
So, while most group events can be happening at any time (message additions, updates and deletions), some events require linearization and "proposed"/"accepted"/"committed" states.
|
||||
|
||||
Some sort of consensus protocol is still needed for all membership changes other than member addition, when the members who have appropriate permissions can vote for the change, requiring defined share of the members who can perform this action to accept this change, before it is committed.
|
||||
|
||||
### Scalable message dissemination
|
||||
|
||||
The previous part presented approaches to the consistency of all parts of the group state. It relies on the efficient approach to distibuting each group event to all members of the group.
|
||||
|
||||
The current approach when the event creator simply sends it to all members is not viable:
|
||||
|
||||
- too much traffic for the sender.
|
||||
- too large delay in message delivery in the period before the new member connected to all members.
|
||||
- creating a fully connected graph is expensive and not scalable.
|
||||
|
||||
An open question is whether each member in the group should even have the full list of group members and their profile data - for some group sizes it may also be expensive. Even with the client-centric approach to group design it may be enough to have only "high capacity peers" storing the full member list, and the other members only having the knowledge of the active members - who contributed at least one message in the observable history.
|
||||
|
||||
[3] describes an efficient randomized algorithm for disseminating updates between distributed database sites that while doesn't guarantee the full propagation of the update, achieves a very high degree of the probability that all members receives the updates. The algorithm considers mechanics of distribution similar to epidemics, and apply the same approach to estimate the probability. When there is new update, the peer that created this update sends it to other randomly chosen peers with some fixed interval between sending these updates. As soon as a peer receives the update, they start start sending it other randomly chosen peers. In one of the variants of this algorithm, the peers that receive the update provide the feedback to the sender whether they saw this update before or not. If the recipient saw the update, then the sender stops broadcasting it with `1/k` probability. In the "blind" variant, the sender decides to stop broadcasting with `1/k` probability without the feedback. In addition to "feedback"/"blind" variants, there are also "coin" (described)/"counter" variants. In "counter" variant the sender sends the update exactly `k` times. The paper shows that "feedback" has little impact on the dissemination completeness, and that the "counter" is more efficient than "coin". It also shows that the "residue" - the share of the peers that didn't receive the update - is `e ^ (-k)` where `k` is the number of messages each peer sends (in case of "coin" variant - on average).
|
||||
|
||||
The suggestion is that we can apply a similar idea to disseminating the messages (events) in the group. Given that we validate the integrity of the group using Merkle DAG, the peers will be able to detect any missed updates, and request them. Instead of deciding which members to send the updates to we can first decide which members to connect to, and then choose members to receive the updates - randomly in both cases. If we aim to the average number of members **not** receiving updates being 0.1 (that is, one member misses one out of 10 messages, until the next one is sent), then depending on the group size `N` each member should broadcast the message to `k` peers, where `k` is:
|
||||
|
||||
| N | k |
|
||||
| ---- | --- |
|
||||
| 100 | ~7 |
|
||||
| 1000 | ~9 |
|
||||
| 10k | ~13 |
|
||||
| 100k | ~14 |
|
||||
| 1m | ~16 |
|
||||
|
||||
Reducing the group size 10x in above table would reduce probability of one member missing any given message to 1%.
|
||||
|
||||
We can also consider that some members will send a higher number of messages (e.g., group owners/admins or members with high bandwidth devices), but it has to be estimated whether this will make a material impact on the number of messages sent.
|
||||
|
||||
Whether to use "coin" or "feedback" approach can depend on whether delivery receipts are enabled, although that would complicate the algorithm, and might not work well in case of some members have delivery receipts disabled.
|
||||
|
||||
### Achieving consensus for group level events
|
||||
|
||||
A small share of group events can create contraditions in group state:
|
||||
- removing members
|
||||
- changing members roles
|
||||
- changing group profile
|
||||
|
||||
Currently these events are sent to all members as soon as they are created, and members act on these events as soon as they receive them. For group state to be consistent, all members will have to wait until the consensus between group admins is achieved.
|
||||
|
||||
We could delay broadcasts to all members only after the consensus between admins is achieved. We also could have admins using the old "direct mail" approach (using the terminology from [3] - which is the current approach). So one of the options could be to achieve the consensus between group admin and owners before broadcasting updates about group profile and membership to all members.
|
||||
|
||||
### References
|
||||
|
||||
[1] Trees and Turtles: Modular Abstractions for State Machine Replication Protocols. https://arxiv.org/pdf/2304.07850.pdf
|
||||
|
||||
[2] Efficient Replication via Timestamp Stability. https://arxiv.org/pdf/2104.01142.pdf
|
||||
|
||||
[3] Epidemic Algorithms for Replicated Database Maintenance. https://dl.acm.org/doi/pdf/10.1145/43921.43922
|
||||
@@ -1,28 +0,0 @@
|
||||
# Groups/Community moderation and participation
|
||||
|
||||
## Problem
|
||||
|
||||
There are a lot of advantages to the anonymous participation in communities, and our users are really excited about it.
|
||||
Users can express yourself freely without fear of being judged, and without these opinions having impact on their employment and/or relationships. The positive effects of anonymous participation can already be seen on Reddit, even though on Reddit users usually have a single identity and global platform-wide reputation (aka karma) - the engagement and polarity of views expressed on Reddit is much higher than in platforms with a single real-life identity. Participating in different communities under different identities gives people additional choices, of not being associated with some discussions, and yet participating in them. Quoting Oscar Wilde: "Give a man a mask and he’ll tell you the truth."
|
||||
|
||||
But anonymous participation comes with a large downside: people are able to abuse anonymity, by posting inappropriate content and comments, spamming group, and making community inappropriate to all other members. That is what happened with Usenet - its growth killed it. Currently we have basic moderation capabilities allowing admins to remove messages of the members and prevent them from sending messages, but for groups with the public link nothing stops them from joining again with a different identity. The only current solution is to make all new members joining without posting permissions, and then manually grant them to the trusted users, but it doesn't scale well to large groups.
|
||||
|
||||
## Solution
|
||||
|
||||
There are two possible solutions:
|
||||
- rely on members reputation score, that will be calculated based on their actions and community reactions. Similar approaches are used in Reddit and in Habr.com (a Russian IT community). When a member joins a community, they are assigned 0 or some low score that either prevents posting completely or allows very limited posting. As the member participation is seen by the community as positive (e.g., more positive than negative reactions), the reputation score would grow allowing more participation (longer posts, image posts, more frequent posts, etc.). Community rules can allow transferring some of the reputation score to the new members, so members joining by recommendation can have higher initial score (Reddit doesn't allow it, Habr.com does), but if group admins/owners block a member such reputation transfers should also be blocked.
|
||||
- for public groups that are registered with the discovery / search servers, these search servers can employ these policies as a condition:
|
||||
- discovery server must be granted administrative rights to be able to moderate messages and block members.
|
||||
- an advertised link to join the group should be controlled by discovery server (so that the group owners can't make discovery server see a content that is different from the actual group content).
|
||||
- group policy within the acceptable range should be required (e.g. reputation scoring, limited participation for the new members, depending on group size, etc.).
|
||||
- discovery server would run language and image recognition models to detect inappropriate content and remove it automatically - what is appropriate would be determined by the group policy.
|
||||
|
||||
These approaches are not mutually exclusive, most likely they need to be combined.
|
||||
|
||||
As the group design remains decentralized, some members can obviously modify the code of the client software to post content in violation of their current permissions. But as the same restrictions would be applied by other clients, they simply won't see this content, and the scoring mechanics can further penalize it.
|
||||
|
||||
Both solutions rely on group state being consistent, so some of the improvements for group consistency from [the previous RFC](./2023-05-02-groups.md) are required for them to work reliably.
|
||||
|
||||
The actual scoring mechanics require formalization. The initial point can be made the same as on some combination of Reddit and Habr.com ideas, and then tweaked to take into account that the reputation scores are community-wide and group-wide.
|
||||
|
||||
Once SimpleX adds [identity layer](./2021-12-11-identity.md), the discovery server can start supporting the reputation scoring for the identity across multiple communities for people who opt in into it.
|
||||
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 5.1.2.0
|
||||
version: 5.1.0.1
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."89caf5572980b776bd750caa3c918ae4488612d8" = "0glils00403s4xxl84xa22wylfsrcpjj5j3v7srqhrmikm1jds4p";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."6c6f22051d693e00aaa4c4e6c5b6bff7e9a40b36" = "0mi4p2s5klm2w6za8xljl3yaiva814phhm5n5q36f04a016i6llm";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||
|
||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 5.1.2.0
|
||||
version: 5.1.0.1
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
@@ -33,7 +33,6 @@ 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
|
||||
@@ -98,7 +97,6 @@ library
|
||||
Simplex.Chat.Migrations.M20230505_chat_item_versions
|
||||
Simplex.Chat.Migrations.M20230511_reactions
|
||||
Simplex.Chat.Migrations.M20230519_item_deleted_ts
|
||||
Simplex.Chat.Migrations.M20230526_indexes
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.WebRTC
|
||||
Simplex.Chat.Options
|
||||
|
||||
@@ -54,7 +54,6 @@ 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
|
||||
@@ -120,9 +119,7 @@ defaultChatConfig =
|
||||
subscriptionEvents = False,
|
||||
hostEvents = False,
|
||||
testView = False,
|
||||
initialCleanupManagerDelay = 30 * 1000000, -- 30 seconds
|
||||
cleanupManagerInterval = 30 * 60, -- 30 minutes
|
||||
ciExpirationInterval = 30 * 60 * 1000000 -- 30 minutes
|
||||
ciExpirationInterval = 1800 * 1000000 -- 30 minutes
|
||||
}
|
||||
|
||||
_defaultSMPServers :: NonEmpty SMPServerWithAuth
|
||||
@@ -451,10 +448,7 @@ processChatCommand = \case
|
||||
ts <- liftIO getCurrentTime
|
||||
let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip"
|
||||
processChatCommand $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing
|
||||
APIImportArchive cfg -> checkChatStopped $ do
|
||||
fileErrs <- importArchive cfg
|
||||
setStoreChanged
|
||||
pure $ CRArchiveImported fileErrs
|
||||
APIImportArchive cfg -> withStoreChanged $ importArchive cfg
|
||||
APIDeleteStorage -> withStoreChanged deleteStorage
|
||||
APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg
|
||||
ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query)
|
||||
@@ -1282,7 +1276,7 @@ processChatCommand = \case
|
||||
chatRef <- getChatRef user chatName
|
||||
let mc = MCText msg
|
||||
processChatCommand $ APIUpdateChatItem chatRef chatItemId live mc
|
||||
ReactToMessage add reaction chatName msg -> withUser $ \user -> do
|
||||
ReactToMessage add reaction chatName msg -> withUser $ \user -> do
|
||||
chatRef <- getChatRef user chatName
|
||||
chatItemId <- getChatItemIdByText user chatRef msg
|
||||
processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction
|
||||
@@ -2351,33 +2345,26 @@ subscribeUserConnections agentBatchSubscribe user = do
|
||||
Just _ -> Nothing
|
||||
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
|
||||
|
||||
cleanupManagerInterval :: NominalDiffTime
|
||||
cleanupManagerInterval = 1800 -- 30 minutes
|
||||
|
||||
cleanupManager :: forall m. ChatMonad m => m ()
|
||||
cleanupManager = do
|
||||
interval <- asks (cleanupManagerInterval . config)
|
||||
runWithoutInitialDelay interval
|
||||
delay <- asks (initialCleanupManagerDelay . config)
|
||||
liftIO $ threadDelay' delay
|
||||
forever $ do
|
||||
flip catchError (toView . CRChatError Nothing) $ do
|
||||
waitChatStarted
|
||||
users <- withStore' getUsers
|
||||
let (us, us') = partition activeUser users
|
||||
forM_ us $ cleanupUser interval
|
||||
forM_ us' $ cleanupUser interval
|
||||
forM_ us cleanupUser
|
||||
forM_ us' cleanupUser
|
||||
cleanupMessages `catchError` (toView . CRChatError Nothing)
|
||||
liftIO $ threadDelay' $ diffToMicroseconds interval
|
||||
liftIO $ threadDelay' $ diffToMicroseconds cleanupManagerInterval
|
||||
where
|
||||
runWithoutInitialDelay cleanupInterval = flip catchError (toView . CRChatError Nothing) $ do
|
||||
waitChatStarted
|
||||
users <- withStore' getUsers
|
||||
let (us, us') = partition activeUser users
|
||||
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchError` (toView . CRChatError (Just u))
|
||||
forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchError` (toView . CRChatError (Just u))
|
||||
cleanupUser cleanupInterval user =
|
||||
cleanupTimedItems cleanupInterval user `catchError` (toView . CRChatError (Just user))
|
||||
cleanupTimedItems cleanupInterval user = do
|
||||
cleanupUser user =
|
||||
cleanupTimedItems user `catchError` (toView . CRChatError (Just user))
|
||||
cleanupTimedItems user = do
|
||||
ts <- liftIO getCurrentTime
|
||||
let startTimedThreadCutoff = addUTCTime cleanupInterval ts
|
||||
let startTimedThreadCutoff = addUTCTime cleanupManagerInterval ts
|
||||
timedItems <- withStore' $ \db -> getTimedItems db user startTimedThreadCutoff
|
||||
forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchError` const (pure ())
|
||||
cleanupMessages = do
|
||||
@@ -2387,9 +2374,8 @@ cleanupManager = do
|
||||
|
||||
startProximateTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
|
||||
startProximateTimedItemThread user itemRef deleteAt = do
|
||||
interval <- asks (cleanupManagerInterval . config)
|
||||
ts <- liftIO getCurrentTime
|
||||
when (diffUTCTime deleteAt ts <= interval) $
|
||||
when (diffUTCTime deleteAt ts <= cleanupManagerInterval) $
|
||||
startTimedItemThread user itemRef deleteAt
|
||||
|
||||
startTimedItemThread :: ChatMonad m => User -> (ChatRef, ChatItemId) -> UTCTime -> m ()
|
||||
@@ -2738,7 +2724,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
_ -> messageError $ "unsupported message: " <> T.pack (show event)
|
||||
CONF confId _ connInfo -> do
|
||||
-- confirming direct connection with a member
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo _memId _memProfile -> do
|
||||
-- TODO check member ID
|
||||
@@ -2747,7 +2733,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
allowAgentConnectionAsync user conn confId XOk
|
||||
_ -> messageError "CONF from member must have x.grp.mem.info"
|
||||
INFO connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo _memId _memProfile -> do
|
||||
-- TODO check member ID
|
||||
@@ -2863,7 +2849,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
_ -> throwChatError $ CECommandError "unexpected cmdFunction"
|
||||
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
|
||||
CONF confId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case memberCategory m of
|
||||
GCInviteeMember ->
|
||||
case chatMsgEvent of
|
||||
@@ -2884,7 +2870,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
|
||||
_ -> messageError "CONF from member must have x.grp.mem.info"
|
||||
INFO connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XGrpMemInfo memId _memProfile
|
||||
| sameMemberId memId m -> do
|
||||
@@ -3028,7 +3014,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- SMP CONF for SndFileConnection happens for direct file protocol
|
||||
-- when recipient of the file "joins" connection created by the sender
|
||||
CONF confId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
-- TODO save XFileAcpt message
|
||||
XFileAcpt name
|
||||
@@ -3098,7 +3084,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- when sender of the file "joins" connection created by the recipient
|
||||
-- (sender doesn't create connections for all group members)
|
||||
CONF confId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XOk -> allowAgentConnectionAsync user conn confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability
|
||||
_ -> pure ()
|
||||
@@ -3161,7 +3147,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m ()
|
||||
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
|
||||
REQ invId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XContact p xContactId_ -> profileContactRequest invId p xContactId_
|
||||
XInfo p -> profileContactRequest invId p Nothing
|
||||
@@ -3959,7 +3945,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
|
||||
saveConnInfo :: Connection -> ConnInfo -> m ()
|
||||
saveConnInfo activeConn connInfo = do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage activeConn connInfo
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XInfo p -> do
|
||||
ct <- withStore $ \db -> createDirectContact db user activeConn p
|
||||
@@ -4153,18 +4139,16 @@ sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg =
|
||||
else sendChunks (chunkNo + 1) rest
|
||||
chSize = fromIntegral chunkSize
|
||||
|
||||
parseChatMessage :: ChatMonad m => Connection -> ByteString -> m (ChatMessage 'Json)
|
||||
parseChatMessage conn = parseChatMessage_ conn Nothing
|
||||
parseChatMessage :: ChatMonad m => ByteString -> m (ChatMessage 'Json)
|
||||
parseChatMessage = parseChatMessage_
|
||||
{-# INLINE parseChatMessage #-}
|
||||
|
||||
parseAChatMessage :: ChatMonad m => Connection -> MsgMeta -> ByteString -> m AChatMessage
|
||||
parseAChatMessage conn msgMeta = parseChatMessage_ conn (Just msgMeta)
|
||||
parseAChatMessage :: ChatMonad m => ByteString -> m AChatMessage
|
||||
parseAChatMessage = parseChatMessage_
|
||||
{-# INLINE parseAChatMessage #-}
|
||||
|
||||
parseChatMessage_ :: (ChatMonad m, StrEncoding s) => Connection -> Maybe MsgMeta -> ByteString -> m s
|
||||
parseChatMessage_ conn msgMeta s = liftEither . first (ChatError . errType) $ strDecode s
|
||||
where
|
||||
errType = CEInvalidChatMessage conn (msgMetaToJson <$> msgMeta) (safeDecodeUtf8 s)
|
||||
parseChatMessage_ :: (ChatMonad m, StrEncoding s) => ByteString -> m s
|
||||
parseChatMessage_ s = liftEither . first (ChatError . CEInvalidChatMessage (safeDecodeUtf8 s)) $ strDecode s
|
||||
|
||||
sendFileChunk :: ChatMonad m => User -> SndFileTransfer -> m ()
|
||||
sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentConnId acId} =
|
||||
@@ -4378,8 +4362,8 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
|
||||
_ -> pure ()
|
||||
|
||||
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m RcvMessage
|
||||
saveRcvMSG conn@Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
|
||||
ACMsg _ ChatMessage {msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody
|
||||
saveRcvMSG Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
|
||||
ACMsg _ ChatMessage {msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage msgBody
|
||||
let agentMsgId = fst $ recipient agentMsgMeta
|
||||
newMsg = NewMessage {chatMsgEvent, msgBody}
|
||||
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
|
||||
|
||||
@@ -25,7 +25,6 @@ import Simplex.Messaging.Util
|
||||
import System.FilePath
|
||||
import UnliftIO.Directory
|
||||
import UnliftIO.Exception (SomeException, bracket, catch)
|
||||
import qualified UnliftIO.Exception as E
|
||||
import UnliftIO.STM
|
||||
import UnliftIO.Temporary
|
||||
|
||||
@@ -49,7 +48,7 @@ exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
|
||||
let method = if disableCompression == Just True then Z.Store else Z.Deflate
|
||||
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
|
||||
|
||||
importArchive :: ChatMonad m => ArchiveConfig -> m [ArchiveError]
|
||||
importArchive :: ChatMonad m => ArchiveConfig -> m ()
|
||||
importArchive cfg@ArchiveConfig {archivePath} =
|
||||
withTempDir cfg "simplex-chat." $ \dir -> do
|
||||
Z.withArchive archivePath $ Z.unpackInto dir
|
||||
@@ -58,38 +57,26 @@ importArchive cfg@ArchiveConfig {archivePath} =
|
||||
backup agentDb
|
||||
copyFile (dir </> archiveChatDbFile) chatDb
|
||||
copyFile (dir </> archiveAgentDbFile) agentDb
|
||||
copyFiles dir filesPath
|
||||
`E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e]
|
||||
let filesDir = dir </> archiveFilesFolder
|
||||
forM_ filesPath $ \fp ->
|
||||
whenM (doesDirectoryExist filesDir) $
|
||||
copyDirectoryFiles filesDir fp
|
||||
where
|
||||
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
|
||||
copyFiles dir filesPath = do
|
||||
let filesDir = dir </> archiveFilesFolder
|
||||
case filesPath of
|
||||
Just fp ->
|
||||
ifM
|
||||
(doesDirectoryExist filesDir)
|
||||
(copyDirectoryFiles filesDir fp)
|
||||
(pure [])
|
||||
_ -> pure []
|
||||
|
||||
withTempDir :: ChatMonad m => ArchiveConfig -> (String -> (FilePath -> m a) -> m a)
|
||||
withTempDir :: ChatMonad m => ArchiveConfig -> (String -> (FilePath -> m ()) -> m ())
|
||||
withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
|
||||
Just tmpDir -> withTempDirectory tmpDir
|
||||
_ -> withSystemTempDirectory
|
||||
|
||||
copyDirectoryFiles :: ChatMonad m => FilePath -> FilePath -> m [ArchiveError]
|
||||
copyDirectoryFiles :: MonadIO m => FilePath -> FilePath -> m ()
|
||||
copyDirectoryFiles fromDir toDir = do
|
||||
createDirectoryIfMissing False toDir
|
||||
fs <- listDirectory fromDir
|
||||
foldM copyFileCatchError [] fs
|
||||
where
|
||||
copyFileCatchError fileErrs f =
|
||||
(copyDirectoryFile f $> fileErrs)
|
||||
`E.catch` \(e :: E.SomeException) -> pure (AEImportFile f (ChatError . CEException $ show e) : fileErrs)
|
||||
copyDirectoryFile f = do
|
||||
let fn = takeFileName f
|
||||
f' = fromDir </> fn
|
||||
whenM (doesFileExist f') $ copyFile f' $ toDir </> fn
|
||||
forM_ fs $ \f -> do
|
||||
let fn = takeFileName f
|
||||
f' = fromDir </> fn
|
||||
whenM (doesFileExist f') $ copyFile f' $ toDir </> fn
|
||||
|
||||
deleteStorage :: ChatMonad m => m ()
|
||||
deleteStorage = do
|
||||
|
||||
@@ -16,7 +16,6 @@ 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 (..))
|
||||
|
||||
@@ -32,7 +32,7 @@ import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Map.Strict (Map)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
import Data.Time (NominalDiffTime, ZonedTime)
|
||||
import Data.Time (ZonedTime)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Version (showVersion)
|
||||
import GHC.Generics (Generic)
|
||||
@@ -42,7 +42,6 @@ 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
|
||||
@@ -110,8 +109,6 @@ data ChatConfig = ChatConfig
|
||||
hostEvents :: Bool,
|
||||
logLevel :: ChatLogLevel,
|
||||
testView :: Bool,
|
||||
initialCleanupManagerDelay :: Int64,
|
||||
cleanupManagerInterval :: NominalDiffTime,
|
||||
ciExpirationInterval :: Int64 -- microseconds
|
||||
}
|
||||
|
||||
@@ -527,7 +524,6 @@ data ChatResponse
|
||||
| CRMessageError {user :: User, severity :: Text, errorMessage :: Text}
|
||||
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
|
||||
| CRChatError {user_ :: Maybe User, chatError :: ChatError}
|
||||
| CRArchiveImported {archiveErrors :: [ArchiveError]}
|
||||
| CRTimedAction {action :: String, durationMilliseconds :: Int64}
|
||||
deriving (Show, Generic)
|
||||
|
||||
@@ -779,7 +775,7 @@ data ChatErrorType
|
||||
| CEChatNotStopped
|
||||
| CEChatStoreChanged
|
||||
| CEInvalidConnReq
|
||||
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
|
||||
| CEInvalidChatMessage {messageData :: Text, message :: String}
|
||||
| CEContactNotReady {contact :: Contact}
|
||||
| CEContactDisabled {contact :: Contact}
|
||||
| CEConnectionDisabled {connection :: Connection}
|
||||
@@ -830,7 +826,6 @@ data ChatErrorType
|
||||
| CEAgentCommandError {message :: String}
|
||||
| CEInvalidFileDescription {message :: String}
|
||||
| CEInternalError {message :: String}
|
||||
| CEException {message :: String}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON ChatErrorType where
|
||||
@@ -873,12 +868,3 @@ unsetActive :: (MonadUnliftIO m, MonadReader ChatController m) => ActiveTo -> m
|
||||
unsetActive a = asks activeTo >>= atomically . (`modifyTVar` unset)
|
||||
where
|
||||
unset a' = if a == a' then ActiveNone else a'
|
||||
|
||||
data ArchiveError
|
||||
= AEImport {chatError :: ChatError}
|
||||
| AEImportFile {file :: String, chatError :: ChatError}
|
||||
deriving (Show, Exception, Generic)
|
||||
|
||||
instance ToJSON ArchiveError where
|
||||
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "AE"
|
||||
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "AE"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
@@ -17,10 +18,11 @@ import qualified Data.Attoparsec.Text as A
|
||||
import Data.Char (isDigit)
|
||||
import Data.Either (fromRight)
|
||||
import Data.Functor (($>))
|
||||
import Data.List (intercalate)
|
||||
import Data.List (foldl', intercalate, minimumBy)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (fromMaybe, isNothing)
|
||||
import Data.Ord (comparing)
|
||||
import Data.Semigroup (sconcat)
|
||||
import Data.String
|
||||
import Data.Text (Text)
|
||||
@@ -105,13 +107,19 @@ data FormattedText = FormattedText {format :: Maybe Format, text :: Text}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON FormattedText where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
|
||||
instance IsString FormattedText where
|
||||
fromString = FormattedText Nothing . T.pack
|
||||
|
||||
type MarkdownList = [FormattedText]
|
||||
|
||||
data EditedText = EditedText {format :: Maybe Format, text :: Text, added :: Maybe Bool}
|
||||
deriving (Eq, Show, Generic)
|
||||
|
||||
instance ToJSON EditedText where
|
||||
toEncoding = J.genericToEncoding $ sumTypeJSON id
|
||||
|
||||
data ParsedMarkdown = ParsedMarkdown {formattedText :: Maybe MarkdownList}
|
||||
deriving (Generic)
|
||||
|
||||
@@ -125,7 +133,7 @@ unmarked = Markdown Nothing
|
||||
parseMaybeMarkdownList :: Text -> Maybe MarkdownList
|
||||
parseMaybeMarkdownList s =
|
||||
let m = intercalate ["\n"] . map (markdownToList . parseMarkdown) $ T.lines s
|
||||
in if all (isNothing . format) m then Nothing else Just m
|
||||
in if all (isNothing . (format :: FormattedText -> Maybe Format)) m then Nothing else Just m
|
||||
|
||||
parseMarkdownList :: Text -> MarkdownList
|
||||
parseMarkdownList = markdownToList . parseMarkdown
|
||||
@@ -234,3 +242,60 @@ markdownP = mconcat <$> A.many' fragmentP
|
||||
linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of
|
||||
Just (CRDataGroup _) -> XLGroup
|
||||
Nothing -> XLContact
|
||||
|
||||
data EditedChar = EditedChar {format :: Maybe Format, char :: Char, operation :: Maybe EditingOperation}
|
||||
deriving (Show, Eq)
|
||||
|
||||
data EditingOperation = EOAdd | EODelete | EOSubstitute
|
||||
deriving (Show, Eq)
|
||||
|
||||
type EditedString = [EditedChar]
|
||||
|
||||
formattedEditedText :: [FormattedText] -> [FormattedText] -> [EditedChar]
|
||||
formattedEditedText s s' = wagnerFisher (toEditedChars s) (toEditedChars s')
|
||||
|
||||
toEditedChars :: [FormattedText] -> [EditedChar]
|
||||
toEditedChars = concatMap toChars
|
||||
where
|
||||
toChars FormattedText {format, text} =
|
||||
map (\char -> EditedChar {format, char, operation = Nothing}) $ T.unpack $ text
|
||||
|
||||
-- fromEditedChars :: [EditedChar] -> [EditedText]
|
||||
-- fromEditedChars = reverse . foldl' addChar []
|
||||
-- where
|
||||
-- addChar :: [EditedText] -> EditedChar -> [EditedText]
|
||||
-- addChar [] c = [toText c]
|
||||
-- addChar ts@(t : rest) c
|
||||
-- | sameFormat t c = appendChar t c : rest
|
||||
-- | otherwise = toText c : ts
|
||||
-- toText :: EditedChar -> EditedText
|
||||
-- toText EditedChar {format, char, added} = EditedText {format, text = T.singleton char, added}
|
||||
-- sameFormat :: EditedText -> EditedChar -> Bool
|
||||
-- sameFormat EditedText {format, added} EditedChar {format = format', added = added'} = format == format' && added == added'
|
||||
-- appendChar :: EditedText -> EditedChar -> EditedText
|
||||
-- appendChar t@EditedText {text} EditedChar {char} = t {text = text <> T.singleton char}
|
||||
|
||||
wagnerFisher :: [EditedChar] -> [EditedChar] -> EditedString
|
||||
wagnerFisher s1 s2 = extractEdits $ foldl' computeRow initialRow s2
|
||||
where
|
||||
computeRow :: ([(Int, EditedString)], [(Int, EditedString)]) -> EditedChar -> ([(Int, EditedString)], [(Int, EditedString)])
|
||||
computeRow (prevRow, currRow) c2 = (currRow, computeNewRow prevRow currRow c2)
|
||||
|
||||
computeNewRow :: [(Int, EditedString)] -> [(Int, EditedString)] -> EditedChar -> [(Int, EditedString)]
|
||||
computeNewRow prevRow currRow c2 = foldl' (computeCell c2 prevRow) [(head currRow)] (zip3 s1 prevRow (tail currRow))
|
||||
|
||||
computeCell :: EditedChar -> [(Int, EditedString)] -> [(Int, EditedString)] -> (EditedChar, (Int, EditedString), (Int, EditedString)) -> [(Int, EditedString)]
|
||||
computeCell c2 prevRow newRow (c1, (diagScore, _), (leftScore, _)) = newRow ++ [minimumBy (comparing fst)
|
||||
[ (diagScore + if char c1 == char c2 then 0 else 1, if char c1 == char c2 then snd (prevRow !! (length newRow)) ++ [EditedChar (format' c1) (char c1) Nothing] else snd (prevRow !! (length newRow)) ++ [EditedChar (format' c1) (char c1) (Just EOSubstitute), EditedChar (format' c2) (char c2) (Just EOSubstitute)])
|
||||
, (leftScore + 1, snd (prevRow !! (length newRow)) ++ [EditedChar (format' c2) (char c2) (Just EOAdd)])
|
||||
, (fst (prevRow !! (length newRow)) + 1, snd (newRow !! (length prevRow)) ++ [EditedChar (format' c1) (char c1) (Just EODelete)])
|
||||
]]
|
||||
|
||||
initialRow :: ([(Int, EditedString)], [(Int, EditedString)])
|
||||
initialRow = ([(0, [])] ++ zipWith (\i c -> (i + 1, [EditedChar (format' c) (char c) (Just EODelete)])) [1..] s1,
|
||||
[(0, [])] ++ zipWith (\i c -> (i + 1, [EditedChar (format' c) (char c) (Just EOAdd)])) [1..] s2)
|
||||
|
||||
extractEdits :: ([(Int, EditedString)], [(Int, EditedString)]) -> EditedString
|
||||
extractEdits = snd . last . snd
|
||||
|
||||
format' = format :: EditedChar -> Maybe Format
|
||||
|
||||
@@ -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 (isJust, isNothing)
|
||||
import Data.Maybe (isNothing, isJust)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
@@ -29,18 +29,21 @@ import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay)
|
||||
import Data.Time.LocalTime (TimeZone, ZonedTime, utcToZonedTime)
|
||||
import Data.Type.Equality
|
||||
import Data.Typeable (Typeable)
|
||||
import Database.SQLite.Simple.FromField (FromField (..))
|
||||
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.Markdown
|
||||
import Simplex.Chat.Messages.ChatItemContent
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..))
|
||||
import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgErrorType (..), MsgMeta (..), SwitchPhase (..))
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, sumTypeJSON)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (MsgBody)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, tshow, (<$?>))
|
||||
|
||||
data ChatType = CTDirect | CTGroup | CTContactRequest | CTContactConnection
|
||||
deriving (Eq, Show, Ord, Generic)
|
||||
@@ -209,10 +212,6 @@ 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
|
||||
@@ -634,6 +633,11 @@ 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
|
||||
@@ -690,16 +694,6 @@ 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
|
||||
@@ -710,6 +704,573 @@ 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
|
||||
@@ -762,6 +1323,73 @@ 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
|
||||
|
||||
@@ -1,660 +0,0 @@
|
||||
{-# 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"
|
||||
@@ -1,22 +0,0 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230526_indexes where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230526_indexes :: Query
|
||||
m20230526_indexes =
|
||||
[sql|
|
||||
CREATE INDEX idx_messages_created_at ON messages(created_at);
|
||||
|
||||
CREATE INDEX idx_chat_item_reactions_created_by_msg_id ON chat_item_reactions(created_by_msg_id);
|
||||
|]
|
||||
|
||||
down_m20230526_indexes :: Query
|
||||
down_m20230526_indexes =
|
||||
[sql|
|
||||
DROP INDEX idx_chat_item_reactions_created_by_msg_id;
|
||||
|
||||
DROP INDEX idx_messages_created_at;
|
||||
|]
|
||||
@@ -640,7 +640,3 @@ CREATE INDEX idx_chat_item_reactions_group ON chat_item_reactions(
|
||||
group_id,
|
||||
shared_msg_id
|
||||
);
|
||||
CREATE INDEX idx_messages_created_at ON messages(created_at);
|
||||
CREATE INDEX idx_chat_item_reactions_created_by_msg_id ON chat_item_reactions(
|
||||
created_by_msg_id
|
||||
);
|
||||
|
||||
@@ -329,7 +329,6 @@ 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
|
||||
@@ -394,7 +393,6 @@ import Simplex.Chat.Migrations.M20230504_recreate_msg_delivery_events_cleanup_me
|
||||
import Simplex.Chat.Migrations.M20230505_chat_item_versions
|
||||
import Simplex.Chat.Migrations.M20230511_reactions
|
||||
import Simplex.Chat.Migrations.M20230519_item_deleted_ts
|
||||
import Simplex.Chat.Migrations.M20230526_indexes
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (week)
|
||||
@@ -474,8 +472,7 @@ schemaMigrations =
|
||||
("20230504_recreate_msg_delivery_events_cleanup_messages", m20230504_recreate_msg_delivery_events_cleanup_messages, Just down_m20230504_recreate_msg_delivery_events_cleanup_messages),
|
||||
("20230505_chat_item_versions", m20230505_chat_item_versions, Just down_m20230505_chat_item_versions),
|
||||
("20230511_reactions", m20230511_reactions, Just down_m20230511_reactions),
|
||||
("20230519_item_deleted_ts", m20230519_item_deleted_ts, Just down_m20230519_item_deleted_ts),
|
||||
("20230526_indexes", m20230526_indexes, Just down_m20230526_indexes)
|
||||
("20230519_item_deleted_ts", m20230519_item_deleted_ts, Just down_m20230519_item_deleted_ts)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -4869,8 +4866,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)
|
||||
@@ -4882,8 +4879,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)
|
||||
@@ -4908,15 +4905,14 @@ 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)
|
||||
|
||||
@@ -4925,8 +4921,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
|
||||
@@ -4957,8 +4953,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,7 +31,6 @@ 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 (..))
|
||||
@@ -323,7 +322,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,7 +37,6 @@ 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
|
||||
@@ -50,7 +49,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..))
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (bshow, tshow)
|
||||
@@ -249,7 +248,6 @@ responseToView user_ ChatConfig {logLevel, showReactions, testView} liveItems ts
|
||||
CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning]
|
||||
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel e
|
||||
CRChatError u e -> ttyUser' u $ viewChatError logLevel e
|
||||
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
|
||||
CRTimedAction _ _ -> []
|
||||
where
|
||||
ttyUser :: User -> [StyledString] -> [StyledString]
|
||||
@@ -835,8 +833,8 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho
|
||||
|
||||
viewUserServers :: AUserProtoServers -> Bool -> [StyledString]
|
||||
viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) testView =
|
||||
customServers
|
||||
<> if testView
|
||||
customServers <>
|
||||
if testView
|
||||
then []
|
||||
else
|
||||
[ "",
|
||||
@@ -844,9 +842,9 @@ viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, preset
|
||||
"use " <> highlight (srvCmd <> " <srv1[,srv2,...]>") <> " to configure " <> pName <> " servers",
|
||||
"use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets"
|
||||
]
|
||||
<> case p of
|
||||
SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"]
|
||||
SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"]
|
||||
<> case p of
|
||||
SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"]
|
||||
SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"]
|
||||
where
|
||||
srvCmd = "/" <> strEncode p
|
||||
pName = protocolName p
|
||||
@@ -1384,12 +1382,7 @@ viewChatError logLevel = \case
|
||||
CEChatNotStopped -> ["error: chat not stopped"]
|
||||
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
|
||||
CEInvalidConnReq -> viewInvalidConnReq
|
||||
CEInvalidChatMessage Connection {connId} msgMeta_ msg e ->
|
||||
[ plain $
|
||||
("chat message error: " <> e <> " (" <> T.unpack (T.take 120 msg) <> ")")
|
||||
<> (", connection id: " <> show connId)
|
||||
<> maybe "" (\MsgMetaJSON {rcvId} -> ", agent msg rcv id: " <> show rcvId) msgMeta_
|
||||
]
|
||||
CEInvalidChatMessage msg e -> [plain $ "chat message error: " <> e <> " (" <> T.unpack (T.take 120 msg) <> ")"]
|
||||
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
||||
CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
||||
CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning]
|
||||
@@ -1443,7 +1436,6 @@ viewChatError logLevel = \case
|
||||
CEAgentCommandError e -> ["agent command error: " <> plain e]
|
||||
CEInvalidFileDescription e -> ["invalid file description: " <> plain e]
|
||||
CEInternalError e -> ["internal chat error: " <> plain e]
|
||||
CEException e -> ["exception: " <> plain e]
|
||||
-- e -> ["chat error: " <> sShow e]
|
||||
ChatErrorStore err -> case err of
|
||||
SEDuplicateName -> ["this display name is already used by user, contact or group"]
|
||||
|
||||
@@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 89caf5572980b776bd750caa3c918ae4488612d8
|
||||
commit: 6c6f22051d693e00aaa4c4e6c5b6bff7e9a40b36
|
||||
- github: kazu-yamamoto/http2
|
||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
# - ../direct-sqlcipher
|
||||
|
||||
@@ -95,8 +95,7 @@ data TestCC = TestCC
|
||||
virtualTerminal :: VirtualTerminal,
|
||||
chatAsync :: Async (),
|
||||
termAsync :: Async (),
|
||||
termQ :: TQueue String,
|
||||
printOutput :: Bool
|
||||
termQ :: TQueue String
|
||||
}
|
||||
|
||||
aCfg :: AgentConfig
|
||||
@@ -150,7 +149,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, printOutput = False}
|
||||
pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ}
|
||||
|
||||
stopTestChat :: TestCC -> IO ()
|
||||
stopTestChat TestCC {chatController = cc, chatAsync, termAsync} = do
|
||||
@@ -193,9 +192,6 @@ 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
|
||||
@@ -243,15 +239,14 @@ getTermLine :: HasCallStack => TestCC -> IO String
|
||||
getTermLine cc =
|
||||
5000000 `timeout` atomically (readTQueue $ termQ cc) >>= \case
|
||||
Just s -> do
|
||||
-- remove condition to always echo virtual terminal
|
||||
when (printOutput cc) $ do
|
||||
name <- userName cc
|
||||
putStrLn $ name <> ": " <> s
|
||||
-- uncomment 2 lines below to echo virtual terminal
|
||||
-- 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
|
||||
|
||||
@@ -895,8 +895,8 @@ testMaintenanceModeWithFiles tmp = do
|
||||
|
||||
testDatabaseEncryption :: HasCallStack => FilePath -> IO ()
|
||||
testDatabaseEncryption tmp = do
|
||||
withNewTestChat tmp "bob" bobProfile $ \b -> withTestOutput b $ \bob -> do
|
||||
withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \a -> withTestOutput a $ \alice -> do
|
||||
withNewTestChat tmp "bob" bobProfile $ \bob -> do
|
||||
withNewTestChatOpts tmp testOpts {maintenance = True} "alice" aliceProfile $ \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" $ \a -> withTestOutput a $ \alice -> do
|
||||
withTestChatOpts tmp (getTestOpts True "mykey") "alice" $ \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" $ \a -> withTestOutput a $ \alice -> do
|
||||
withTestChatOpts tmp (getTestOpts True "anotherkey") "alice" $ \alice -> do
|
||||
alice ##> "/_start"
|
||||
alice <## "chat started"
|
||||
testChatWorking alice bob
|
||||
@@ -934,8 +934,7 @@ testDatabaseEncryption tmp = do
|
||||
alice <## "chat stopped"
|
||||
alice ##> "/db decrypt anotherkey"
|
||||
alice <## "ok"
|
||||
withTestChat tmp "alice" $ \a -> withTestOutput a $ \alice -> do
|
||||
testChatWorking alice bob
|
||||
withTestChat tmp "alice" $ \alice -> testChatWorking alice bob
|
||||
|
||||
testMuteContact :: HasCallStack => FilePath -> IO ()
|
||||
testMuteContact =
|
||||
@@ -1316,13 +1315,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 2", id, "ok")
|
||||
alice #$> ("/_ttl 1 1", id, "ok")
|
||||
connectUsers alice bob
|
||||
|
||||
-- create second user and set ttl
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/_ttl 2 5", id, "ok")
|
||||
alice #$> ("/_ttl 2 3", id, "ok")
|
||||
connectUsers alice bob
|
||||
|
||||
-- first user messages
|
||||
@@ -1354,7 +1353,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: 2 second(s)")
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 1 second(s)")
|
||||
|
||||
alice #> "@bob alice 3"
|
||||
bob <# "alice> alice 3"
|
||||
@@ -1366,7 +1365,7 @@ testUsersRestartCIExpiration tmp = do
|
||||
-- second user messages
|
||||
alice ##> "/user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 5 second(s)")
|
||||
alice #$> ("/ttl", id, "old messages are set to be deleted after: 3 second(s)")
|
||||
|
||||
alice #> "@bob alisa 3"
|
||||
bob <# "alisa> alisa 3"
|
||||
@@ -1375,7 +1374,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 3000000
|
||||
threadDelay 2000000
|
||||
|
||||
-- messages both before and after restart are deleted
|
||||
-- first user messages
|
||||
@@ -1388,7 +1387,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 3000000
|
||||
threadDelay 2000000
|
||||
|
||||
alice #$> ("/_get chat @4 count=100", chat, [])
|
||||
where
|
||||
|
||||
@@ -50,7 +50,7 @@ chatFileTests = do
|
||||
describe "async sending and receiving files" $ do
|
||||
-- fails on CI
|
||||
xit'' "send and receive file, sender restarts" testAsyncFileTransferSenderRestarts
|
||||
xit'' "send and receive file, receiver restarts" testAsyncFileTransferReceiverRestarts
|
||||
it "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
|
||||
xit' "receive file marked to receive on chat start" testXFTPMarkToReceive
|
||||
it "receive file marked to receive on chat start" testXFTPMarkToReceive
|
||||
it "error receiving file" testXFTPRcvError
|
||||
it "cancel receiving file, repeat receive" testXFTPCancelRcvRepeat
|
||||
|
||||
@@ -986,17 +986,13 @@ 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"
|
||||
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 <## "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"
|
||||
bob <## "completed receiving file 1 (test.pdf) from alice"
|
||||
|
||||
alice ##> "/fs 1"
|
||||
@@ -1026,10 +1022,8 @@ testXFTPAcceptAfterUpload =
|
||||
threadDelay 100000
|
||||
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob
|
||||
<### [ "saving file 1 from alice to ./tests/tmp/test.pdf",
|
||||
"started receiving file 1 (test.pdf) from alice"
|
||||
]
|
||||
bob <## "started receiving file 1 (test.pdf) from alice"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
||||
bob <## "completed receiving file 1 (test.pdf) from alice"
|
||||
|
||||
src <- B.readFile "./tests/fixtures/test.pdf"
|
||||
@@ -1172,17 +1166,13 @@ 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"
|
||||
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 <## "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"
|
||||
bob <## "completed receiving file 1 (test.pdf) from alice"
|
||||
|
||||
src <- B.readFile "./tests/fixtures/test.pdf"
|
||||
@@ -1215,17 +1205,13 @@ 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"
|
||||
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 <## "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"
|
||||
bob <## "completed receiving file 1 (test.pdf) from alice"
|
||||
|
||||
src <- B.readFile "./tests/fixtures/test.pdf"
|
||||
@@ -1252,10 +1238,8 @@ testXFTPContinueRcv tmp = do
|
||||
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob
|
||||
<### [ "saving file 1 from alice to ./tests/tmp/test.pdf",
|
||||
"started receiving file 1 (test.pdf) from alice"
|
||||
]
|
||||
bob <## "started receiving file 1 (test.pdf) from alice"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
||||
|
||||
bob ##> "/fs 1"
|
||||
bob <## "receiving file 1 (test.pdf) progress 0% of 266.0 KiB"
|
||||
@@ -1326,10 +1310,8 @@ testXFTPRcvError tmp = do
|
||||
withTestChatCfg tmp cfg "bob" $ \bob -> do
|
||||
bob <## "1 contacts connected (use /cs for the list)"
|
||||
bob ##> "/fr 1 ./tests/tmp"
|
||||
bob
|
||||
<### [ "saving file 1 from alice to ./tests/tmp/test.pdf",
|
||||
"started receiving file 1 (test.pdf) from alice"
|
||||
]
|
||||
bob <## "started receiving file 1 (test.pdf) from alice"
|
||||
bob <## "saving file 1 from alice to ./tests/tmp/test.pdf"
|
||||
bob <## "error receiving file 1 (test.pdf) from alice"
|
||||
|
||||
bob ##> "/fs 1"
|
||||
@@ -1347,17 +1329,13 @@ 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"
|
||||
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"
|
||||
]
|
||||
]
|
||||
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"
|
||||
|
||||
threadDelay 100000
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
module ChatTests.Utils where
|
||||
|
||||
import ChatClient
|
||||
import Control.Concurrent (threadDelay)
|
||||
import Control.Concurrent.Async (concurrently_)
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad (unless, when)
|
||||
@@ -200,20 +199,18 @@ 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
|
||||
(@@@) cc res = do
|
||||
threadDelay 10000
|
||||
getChats mapChats cc res
|
||||
(@@@) = getChats mapChats
|
||||
|
||||
mapChats :: [(String, String, Maybe ConnStatus)] -> [(String, String)]
|
||||
mapChats = map $ \(ldn, msg, _) -> (ldn, msg)
|
||||
@@ -410,7 +407,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
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ markdownTests = do
|
||||
textWithEmail
|
||||
textWithPhone
|
||||
multilineMarkdownList
|
||||
formattedEditedTextTests
|
||||
|
||||
textFormat :: Spec
|
||||
textFormat = describe "text format (bold)" do
|
||||
@@ -206,3 +207,22 @@ multilineMarkdownList = describe "multiline markdown" do
|
||||
parseMaybeMarkdownList "http://simplex.chat\nhttp://app.simplex.chat" `shouldBe` Just [uri' "http://simplex.chat", "\n", uri' "http://app.simplex.chat"]
|
||||
it "no markdown" do
|
||||
parseMaybeMarkdownList "not a\nmarkdown" `shouldBe` Nothing
|
||||
|
||||
plainText :: Text -> FormattedText
|
||||
plainText = FormattedText Nothing
|
||||
|
||||
redText :: Text -> FormattedText
|
||||
redText = FormattedText $ Just $ colored Red
|
||||
|
||||
plainEdited :: Text -> Bool -> EditedText
|
||||
plainEdited t added = EditedText Nothing t (Just added)
|
||||
|
||||
redEdited :: Text -> Bool -> EditedText
|
||||
redEdited t added = EditedText (Just $ colored Red) t (Just added)
|
||||
|
||||
formattedEditedTextTests :: Spec
|
||||
formattedEditedTextTests = fdescribe "show edits using Wagner-Fisher algorithm" do
|
||||
it "one character change" do
|
||||
formattedEditedText [plainText "Hrllo"] [plainText "Hello"]
|
||||
`shouldBe` []
|
||||
-- `shouldBe` [plainText "H", plainEdited "r" False, plainEdited "e" True, plainText "llo"]
|
||||
|
||||
@@ -13,16 +13,16 @@
|
||||
"simplex-private-4-title": "可选择通过 Tor 访问",
|
||||
"simplex-private-5-title": "多层级的内容填充",
|
||||
"simplex-private-6-title": "带外密钥交换",
|
||||
"simplex-private-7-title": "消息完整性验证",
|
||||
"simplex-private-7-title": "消息完整性认证",
|
||||
"simplex-private-9-title": "单向消息队列",
|
||||
"simplex-private-card-1-point-1": "双棘轮协议——具有完美前向保密和入侵恢复功能的 OTR(不留记录即时通讯) 消息传递。",
|
||||
"simplex-private-card-3-point-2": "服务器指纹和通道绑定可防止中间人与重放攻击。",
|
||||
"simplex-private-card-1-point-1": "双棘轮协议——具有完美前向保密和入侵恢复功能的 OTR 消息传递。",
|
||||
"simplex-private-card-3-point-2": "服务器指纹和通道绑定可防止 MITM 和重放攻击。",
|
||||
"simplex-private-card-3-point-3": "禁用连接恢复以防止会话攻击。",
|
||||
"simplex-private-card-4-point-1": "为了保护您的 IP 地址,您可以通过 Tor 或其他传输覆盖网络访问服务器。",
|
||||
"simplex-private-card-4-point-1": "为了保护您的 IP 地址,您可以通过 Tor 或其他一些传输覆盖网络访问服务器。",
|
||||
"simplex-private-card-5-point-2": "它使不同大小的消息在服务器和网络监视者看来是一样的。",
|
||||
"simplex-private-card-9-point-1": "每个消息队列都有不同的发送和接收地址,单向地传递消息。",
|
||||
"simplex-network-overlay-card-1-li-2": "与大多数 P2P 网络不同,SimpleX 在设计上没有任何类型的全局用户标识符,甚至临时的也没有。SimpleX 仅使用临时的成对标识符,提供更好的匿名性和元数据保护。",
|
||||
"privacy-matters-overlay-card-1-p-1": "许多大型公司会使用您的人际关系来估算您的收入,决定商品的价格,并向您兜售您并不真正需要的产品。",
|
||||
"simplex-private-card-9-point-1": "每个消息队列在一个方向上传递消息,具有不同的发送和接收地址。",
|
||||
"simplex-network-overlay-card-1-li-2": "与大多数 P2P 网络不同,SimpleX 设计没有任何类型的全局用户标识符,甚至临时的也没有,并且仅使用临时的成对标识符,提供更好的匿名性和元数据保护。",
|
||||
"privacy-matters-overlay-card-1-p-1": "许多大公司使用您联系人的信息来估算您的收入,向您兜售您并不真正需要的产品,并确定推荐商品的价格区间。",
|
||||
"simplex-unique-overlay-card-3-p-4": "发送和接收的服务器流量之间没有共同的标识符或密文—— 如果有人在观察它,他们也无法轻易确定谁与谁通信,即使 TLS 受到威胁。",
|
||||
"simplex-unique-card-4-p-1": "SimpleX 网络是完全去中心化的,并且独立于任何加密货币或除互联网以外的任何其他平台。",
|
||||
"join": "加入",
|
||||
@@ -40,9 +40,9 @@
|
||||
"home": "主页",
|
||||
"developers": "开发者",
|
||||
"blog": "博客",
|
||||
"simplex-explained-tab-1-text": "1. 用户会体验到什么",
|
||||
"reference": "参考资料",
|
||||
"simplex-explained-tab-2-text": "2. 背后的运作原理",
|
||||
"simplex-explained-tab-1-text": "1. 用户可体验什么",
|
||||
"reference": "参考",
|
||||
"simplex-explained-tab-2-text": "2. SimpleX 的运作原理",
|
||||
"features": "功能",
|
||||
"why-simplex": "为何选择 SimpleX",
|
||||
"simplex-network": "SimpleX 网络",
|
||||
@@ -50,80 +50,80 @@
|
||||
"simplex-explained-tab-3-text": "3. 服务器能看到什么",
|
||||
"hero-header": "重新定义隐私",
|
||||
"simplex-explained-tab-1-p-2": "它是如何在单向消息队列与没有用户识别符的情况下工作的?",
|
||||
"simplex-explained-tab-2-p-2": "服务器只会单向传输消息,而无法掌握用户的会话或连接的全貌。",
|
||||
"simplex-explained-tab-3-p-1": "服务器对每个队列都有单独的匿名凭证,并且不知道这些凭证属于哪些用户。",
|
||||
"simplex-explained-tab-2-p-2": "服务器只传送单向消息,而无法全面了解用户的会话或连接。",
|
||||
"simplex-explained-tab-3-p-1": "服务器对每个队列都有单独的匿名凭证,并且不知道它们属于哪些用户。",
|
||||
"donate": "捐赠",
|
||||
"simplex-explained-tab-2-p-1": "对于每个连接,您都会使用两个单独的消息队列,通过不同的服务器发送和接收消息。",
|
||||
"simplex-explained-tab-2-p-1": "对于每个连接,您会使用两个单独的消息队列通过不同的服务器发送和接收消息。",
|
||||
"simplex-chat-protocol": "SimpleX 聊天协议",
|
||||
"smp-protocol": "SMP协议",
|
||||
"chat-protocol": "聊天协议",
|
||||
"copyright-label": "© 2020-2023 SimpleX | 开源项目",
|
||||
"terminal-cli": "命令行程式",
|
||||
"simplex-explained-tab-1-p-1": "您可以创建联系人和群组,并进行双向对话,就像是任何其他即时通讯软件一样。",
|
||||
"hero-p-1": "其他应用——如Signal、Matrix、Session、Briar、Jami、Cwtch 等——都需要用户 ID。<br>而SimpleX 不需要用户ID,连<strong>随机生成</strong>的也不需要。<br>这从根本上改善了您的隐私。",
|
||||
"simplex-explained-tab-1-p-1": "您可以像在任何其他即时通讯软件中一样创建联系人和群组,并进行双向对话。",
|
||||
"hero-p-1": "其他应用需要用户 ID:Signal、Matrix、Session、Briar、Jami、Cwtch 等。<br>SimpleX 不需要,<strong>甚至不需要随机数</strong>。<br>这从根本上改善了您的隐私。",
|
||||
"hero-subheader": "首个不使用用户ID的即时通讯软件",
|
||||
"hero-overlay-2-textlink": "SimpleX 是如何工作的?",
|
||||
"hero-2-header-desc": "右侧的视频向您展示了如何通过一次性二维码、面对面交流或通过视频交换链接来连接到您的朋友。您同样可以通过共享邀请链接来进行连接。",
|
||||
"hero-2-header-desc": "该视频向您展示如何通过一次性二维码、当面或通过视频链接来连接到您的朋友。您还可以通过共享邀请链接来建立连接。",
|
||||
"hero-overlay-1-title": "SimpleX 是如何工作的?",
|
||||
"feature-2-title": "端到端加密的图像和文件",
|
||||
"feature-1-title": "端到端加密的文字消息,支持markdown和编辑",
|
||||
"feature-4-title": "端到端加密的语音消息",
|
||||
"feature-3-title": "去中心化的秘密群组——只有用户知道它们的存在",
|
||||
"feature-5-title": "支持消息自动销毁",
|
||||
"simplex-network-overlay-1-title": "与 P2P 通讯协议的比较",
|
||||
"simplex-network-overlay-1-title": "与 P2P 消息传递协议的比较",
|
||||
"feature-7-title": "随身的加密数据库——可将您的个人资料移至另一台设备",
|
||||
"simplex-private-10-title": "临时匿名成对标识符",
|
||||
"simplex-private-8-title": "通过消息混合减少相关性",
|
||||
"simplex-private-8-title": "消息混合以减少相关性",
|
||||
"simplex-private-card-1-point-2": "每个队列中的网络与密码学库加密盒(NaCL cryptobox)可防止 TLS 受到威胁时消息队列之间的流量关联。",
|
||||
"simplex-private-card-10-point-2": "它让在没有用户标识符的情况下传递消息成为可能,并提供比替代方案更好的元数据隐私。",
|
||||
"simplex-private-card-10-point-2": "它允许在没有用户配置文件标识符的情况下传递消息,提供比替代方案更好的元数据隐私。",
|
||||
"privacy-matters-1-title": "广告和价格歧视",
|
||||
"privacy-matters-1-overlay-1-linkText": "隐私为您省钱",
|
||||
"privacy-matters-2-title": "对选举的操纵",
|
||||
"privacy-matters-1-overlay-1-linkText": "隐私能为您省钱",
|
||||
"privacy-matters-2-title": "操纵选举",
|
||||
"privacy-matters-2-overlay-1-linkText": "隐私给您力量",
|
||||
"simplex-private-card-2-point-1": "为来信附加服务器加密层,以防止在 TLS 受破坏时接收和发送的服务器流量之间发生关联。",
|
||||
"privacy-matters-1-overlay-1-title": "隐私为您省钱",
|
||||
"simplex-private-card-3-point-1": "客户端与服务器之间的连接只使用强加密的 TLS 1.2/1.3。",
|
||||
"privacy-matters-3-overlay-1-title": "隐私保您自由",
|
||||
"privacy-matters-3-overlay-1-linkText": "隐私保您自由",
|
||||
"simplex-private-card-4-point-2": "要通过 Tor 使用 SimpleX,请安装 <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot</a> 应用并启用 SOCKS5 代理(在 <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">iOS</a>上需要使用 VPN 模式)。",
|
||||
"simplex-unique-1-overlay-1-title": "您的身份、个人资料、联系人和元数据的完整隐私",
|
||||
"simplex-unique-2-title": "您可以免受垃圾消息和平台滥用的侵害",
|
||||
"simplex-unique-1-title": "您有完整的隐私",
|
||||
"simplex-unique-2-overlay-1-title": "防止垃圾消息和平台滥用的最佳保护",
|
||||
"privacy-matters-1-overlay-1-title": "隐私能为您省钱",
|
||||
"simplex-private-card-3-point-1": "客户端-服务器之间的连接只使用具有强加密算法的 TLS 1.2/1.3。",
|
||||
"privacy-matters-3-overlay-1-title": "隐私保护您的自由",
|
||||
"privacy-matters-3-overlay-1-linkText": "隐私保护您的自由",
|
||||
"simplex-private-card-4-point-2": "要通过 Tor 使用 SimpleX,请安装 <a href=\"https://guardianproject.info/apps/org.torproject.android/\" target=\"_blank\">Orbot app</a> 并启用 SOCKS5 代理(或在 iOS 上的 <a href=\"https://apps.apple.com/us/app/orbot/id1609461599?platform=iphone\" target=\"_blank\">VPN</a>)。",
|
||||
"simplex-unique-1-overlay-1-title": "您的身份、个人资料、联系人和元数据的完全隐私",
|
||||
"simplex-unique-2-title": "您可以免受垃圾邮件和滥用的侵害",
|
||||
"simplex-unique-1-title": "您有完全的隐私",
|
||||
"simplex-unique-2-overlay-1-title": "防止垃圾邮件和滥用的最佳保护",
|
||||
"simplex-unique-3-overlay-1-title": "您数据的所有权、控制权和安全性",
|
||||
"simplex-unique-4-overlay-1-title": "完全去中心化 —— 用户拥有 SimpleX 网络",
|
||||
"simplex-unique-4-overlay-1-title": "完全去中心化—— 用户拥有 SimpleX 网络",
|
||||
"simplex-unique-3-title": "您控制您的数据",
|
||||
"simplex-unique-4-title": "您拥有 SimpleX 网络",
|
||||
"simplex-private-card-7-point-1": "为了保证消息完整性,消息按顺序编号并会包含前一条消息的哈希值。",
|
||||
"simplex-private-card-6-point-1": "许多通信平台容易受到服务器或网络提供商的中间人攻击。",
|
||||
"simplex-private-card-6-point-2": "为防止这种情况,当您将通讯地址作为链接或二维码共享时,SimpleX 应用程序会在带外传递一次性密钥。",
|
||||
"simplex-private-card-9-point-2": "与传统的消息代理相比,它减少了攻击媒介和可用的元数据。",
|
||||
"simplex-private-card-7-point-1": "为了保证完整性,消息按顺序编号并包含前一条消息的哈希值。",
|
||||
"simplex-private-card-6-point-1": "许多通信平台容易受到服务器或网络提供商的 MITM 攻击。",
|
||||
"simplex-private-card-6-point-2": "为防止这种情况,当您将地址作为链接或二维码共享时,SimpleX 应用程序会在带外传递一次性密钥。",
|
||||
"simplex-private-card-9-point-2": "与传统的消息代理相比,它减少了攻击向量和可用的元数据。",
|
||||
"hero-overlay-card-1-p-1": "许多用户问:<em>如果 SimpleX 没有用户标识符,它怎么知道将消息传递到哪里?</em>",
|
||||
"simplex-private-card-7-point-2": "如果添加、删除或更改任何消息,收件人都会收到警告。",
|
||||
"simplex-private-card-8-point-1": "SimpleX 服务器会充当低延迟混合节点,打乱传入和传出消息的顺序。",
|
||||
"simplex-private-card-10-point-1": "SimpleX 为每个用户的联系人或群组成员均使用临时匿名成对地址和凭据。",
|
||||
"simplex-private-card-7-point-2": "如果添加、删除或更改任何消息,收件人将收到警报。",
|
||||
"simplex-private-card-8-point-1": "SimpleX 服务器充当低延迟混合节点—— 传入和传出消息的顺序不同。",
|
||||
"simplex-private-card-10-point-1": "SimpleX 为每个用户联系人或组成员使用临时匿名成对地址和凭据。",
|
||||
"privacy-matters-3-title": "因无辜结社而被起诉",
|
||||
"privacy-matters-2-overlay-1-title": "隐私给您力量",
|
||||
"hero-overlay-card-1-p-2": "为了传递消息,SimpleX 使用消息队列的临时匿名成对标识符(而非其他平台使用的用户 ID)来区分您的每个连接——没有长期标识符这种东西。",
|
||||
"hero-overlay-card-1-p-2": "为了传递消息,与所有其他平台使用用户 ID不同,SimpleX 使用消息队列的临时匿名成对标识符,来区分开您的每个连接——没有长期标识符。",
|
||||
"hero-overlay-card-1-p-3": "您定义用于接收消息的服务器、您的联系人—— 您用来向他们发送消息的服务器。 每个会话都很可能会使用两个不同的服务器。",
|
||||
"hero-overlay-card-1-p-4": "此设计可防止在应用程序级别泄漏任何用户元数据。 为了进一步改善隐私并保护您的 IP 地址,您可以通过 Tor 连接到消息服务器。",
|
||||
"hero-overlay-card-1-p-5": "只有客户端设备存储用户配置文件、联系人和群组; 消息均使用两层端到端加密发送。",
|
||||
"simplex-network-overlay-card-1-p-1": "<a href='https://zh.wikipedia.org/wiki/%E5%B0%8D%E7%AD%89%E7%B6%B2%E8%B7%AF'>P2P</a> 消息传递协议和应用程序存在各种问题,使得它们不如 SimpleX 可靠,分析起来更复杂,并且 容易受到多种类型的攻击。",
|
||||
"simplex-network-overlay-card-1-p-1": "<a href='https://en.wikipedia.org/wiki/Peer-to-peer'>P2P</a> 消息传递协议和应用程序存在各种问题,使得它们不如 SimpleX 可靠,分析起来更复杂,并且 容易受到多种类型的攻击。",
|
||||
"hero-overlay-card-1-p-6": "在 <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>SimpleX 白皮书</a> 中阅读更多内容。",
|
||||
"hero-overlay-card-2-p-2": "然后他们可以将这些信息与现有的公共社交网络相关联,并确定一些真实身份。",
|
||||
"hero-overlay-card-2-p-1": "当用户具有持久身份时,即使这只是一个随机数(如会话 ID),提供商或攻击者也有可能观察到用户如何连接以及他们发送了多少消息。",
|
||||
"hero-overlay-card-2-p-3": "即使是使用 Tor v3 服务的最私密的应用程序,如果您通过同一个人资料与两个不同的联系人交谈,他们也可以证明他们与同一个人有联系。",
|
||||
"hero-overlay-card-2-p-4": "为防止这些攻击,SimpleX 在其设计中不包含任何用户 ID 。 而且,如果您使用隐身模式,您将为每个联系人显示不同的名称,避免他们之间共享任何数据。",
|
||||
"simplex-network-overlay-card-1-li-1": "P2P 网络依赖于<a href='https://zh.wikipedia.org/wiki/%E5%88%86%E6%95%A3%E5%BC%8F%E9%9B%9C%E6%B9%8A%E8%A1%A8'> 分布式散列表(DHT) </a> 的某些变体来路由消息。 DHT 在设计上必须平衡可达性和延迟。 SimpleX 比 P2P 具有更好的可达性和更低的延迟,因为消息可以通过通讯双方选择的多个服务器并行地冗余传递。若是在 P2P 网络中,消息则需要使用算法选择,并依次通过 <em>O(log N)</em> 个节点。",
|
||||
"simplex-network-overlay-card-1-li-4": "P2P 实现(如 <a href='https://zh.wikipedia.org/wiki/BitTorrent_(%E5%8D%8F%E8%AE%AE)'>BitTorrent</a>)可能会被某些互联网提供商阻止。 SimpleX 与传输协议无关——它可以在标准网络协议上工作,例如 WebSockets。",
|
||||
"simplex-network-overlay-card-1-li-3": "P2P 并未解决<a href='https://zh.wikipedia.org/wiki/%E4%B8%AD%E9%97%B4%E4%BA%BA%E6%94%BB%E5%87%BB'>中间人攻击(MITM Attack)</a> 问题。大多数现有的 P2P 实现没有使用带外通讯来进行初始密钥的交换,而 SimpleX 使用带外通讯,或者在某些情况下,使用预先存在的安全和可信连接来进行初始密钥交换。",
|
||||
"simplex-network-overlay-card-1-li-5": "所有已知的 P2P 网络都可能受到 <a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil 攻击</a>,因为每个节点都是可发现的,并且网络作为一个整体运行。 已知的缓解措施不是需要一个中心化的组件就是需要昂贵的<a href='https://en.wikipedia.org/wiki/Proof_of_work'>工作量证明</a>。而 SimpleX 网络没有服务器可发现性,它是碎片化的并且作为多个隔离的子网运行,这样全网络范围的攻击便无从实现。",
|
||||
"simplex-network-overlay-card-1-li-6": "P2P 网络可能受到 <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS 攻击</a> 。客户端有能力重新广播和放大流量,从而导致整个网络范围内的服务中断。 SimpleX 客户端仅中继来自已知连接的流量,因此不能被攻击者用来放大整个网络的流量。",
|
||||
"privacy-matters-overlay-card-1-p-2": "在线零售商知道收入较低的人更有可能在紧急情况下购买商品,因此他们可能会收取更高的价格或取消折扣。",
|
||||
"privacy-matters-overlay-card-1-p-3": "一些金融和保险公司使用社交图谱来确定利率和保费。 它通常会让收入较低的人支付更多—— 它被称为<a href='https://fairbydesign.com/povertypremium/' target='_blank'>“贫困溢价”</a>。",
|
||||
"privacy-matters-overlay-card-1-p-4": "SimpleX 平台比任何替代方案都能更好地保护您人际关系层面的隐私,防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 提供的服务器,我们也不知道用户数量或他们的连接数。",
|
||||
"simplex-network-overlay-card-1-li-1": "P2P 网络依赖于 <a href='https://en.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> 的某些变体来路由消息。 DHT 设计必须平衡交付保证和延迟。 SimpleX 比 P2P 具有更好的传递保证和更低的延迟,因为消息可以通过多个服务器并行地冗余传递,使用接收者选择的服务器。 在 P2P 网络中,消息使用算法选择的节点按顺序通过 <em>O(log N)</em> 个节点。",
|
||||
"simplex-network-overlay-card-1-li-4": "P2P 实施可能会被某些互联网提供商(如 <a href='https://en.wikipedia.org/wiki/BitTorrent'>BitTorrent</a>)阻止。 SimpleX 与传输无关——它可以在标准网络协议上工作,例如 WebSockets。",
|
||||
"simplex-network-overlay-card-1-li-3": "P2P 没有解决<a href='https://en.wikipedia.org/wiki/Man-in-the-middle_attack'>MITM attack</a> 问题,大多数现有的实现不使用带外 用于初始密钥交换的消息。 SimpleX 使用带外消息,或者在某些情况下,使用预先存在的安全和可信连接来进行初始密钥交换。",
|
||||
"simplex-network-overlay-card-1-li-5": "所有已知的 P2P 网络都可能容易受到 <a href='https://en.wikipedia.org/wiki/Sybil_attack'>Sybil 攻击</a>,因为每个节点都是可发现的,并且网络作为一个整体运行。 缓解它的已知措施需要集中式组件或昂贵的<a href='https://en.wikipedia.org/wiki/Proof_of_work'>工作量证明</a>。 SimpleX 网络没有服务器可发现性,它是分散的并且作为多个隔离的子网运行,使得网络范围的攻击不可能。",
|
||||
"simplex-network-overlay-card-1-li-6": "P2P 网络可能容易受到 <a href='https://www.usenix.org/conference/woot15/workshop-program/presentation/p2p-file-sharing-hell-exploiting-bittorrent'>DRDoS 攻击</a> ,当客户端可以重新广播和放大流量时,会导致网络范围内的拒绝服务。 SimpleX 客户端仅中继来自已知连接的流量,不能被攻击者用来放大整个网络的流量。",
|
||||
"privacy-matters-overlay-card-1-p-2": "在线零售商知道收入较低的人更有可能进行紧急购买,因此他们可能会收取更高的价格或取消折扣。",
|
||||
"privacy-matters-overlay-card-1-p-3": "一些金融和保险公司使用社交图谱来确定利率和保费。 它通常会让收入较低的人支付更多—— 它被称为<a href='https://fairbydesign.com/povertypremium/' target='_blank'>'贫困溢价'</a>。",
|
||||
"privacy-matters-overlay-card-1-p-4": "SimpleX 平台比任何替代方案都更好地保护您的连接隐私,完全防止您的社交图谱被任何公司或组织使用。 即使人们使用 SimpleX Chat 提供的服务器,我们也不知道用户数量或他们的连接数。",
|
||||
"privacy-matters-overlay-card-2-p-1": "不久前,我们观察到几次大选被<a href='https://en.wikipedia.org/wiki/Facebook–Cambridge_Analytica_data_scandal' target='_blank'>一家知名咨询公司</a>操纵,该公司使用我们的社交图谱扭曲我们对现实世界的看法并操纵我们的选票。",
|
||||
"privacy-matters-overlay-card-2-p-2": "为了客观并做出独立的决定,您需要控制您的信息空间。 而这只有当您使用没有能力访问您的社交图谱的,注重隐私的通信平台时,这才有可能。",
|
||||
"privacy-matters-overlay-card-2-p-2": "为了客观并做出独立的决定,您需要控制您的信息空间。 只有当您使用无法访问您的社交图谱的私人通信平台时这才有可能。",
|
||||
"privacy-matters-overlay-card-2-p-3": "SimpleX 是第一个没有设计任何用户标识符的平台,这样能比任何已知的替代方案都更好地保护您的连接图谱。",
|
||||
"privacy-matters-overlay-card-3-p-2": "最令人震惊的故事之一是 <a href='https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi' target='_blank'>Mohamedou Ould Salahi</a> 在他的回忆录中描述并在毛里塔尼亚电影中展示的经历。 他在未经审判的情况下被关进关塔那摩集中营,并在打电话给他在阿富汗的亲戚后在那里遭受了 15 年的折磨,他被怀疑参与了 9/11 袭击,尽管他在过去 10 年住在德国。",
|
||||
"privacy-matters-overlay-card-3-p-1": "每个人都应该关心他们通信的隐私和安全——无害的谈话会让您处于危险之中,即使您没有什么可隐瞒的。",
|
||||
@@ -197,10 +197,10 @@
|
||||
"comparison-section-list-point-4": "如果运营商的服务器受到威胁",
|
||||
"comparison-section-list-point-1": "通常基于电话号码,在某些情况下基于用户名",
|
||||
"comparison-section-list-point-2": "基于 DNS 的地址",
|
||||
"comparison-section-list-point-6": "P2P 是分布式的,而非联邦式的 - 它们作为单个网络运行",
|
||||
"comparison-section-list-point-6": "P2P 是分布式的,它们不是联合的 - 它们作为单个网络运行",
|
||||
"see-here": "参见此处",
|
||||
"comparison-section-list-point-7": "P2P 网络要么拥有中央管理机制,要么整个网络都可能受到损害",
|
||||
"simplex-private-card-5-point-1": "SimpleX 为每个加密层进行内容填充来对抗长度扩展攻击。",
|
||||
"simplex-private-card-5-point-1": "SimpleX 为每个加密层使用内容填充来阻断长度扩展攻击。",
|
||||
"simplex-unique-overlay-card-4-p-2": "SimpleX 平台使用<a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>开放协议</a>并提供<a href='https://github.com/simplex-chat/simplex-chat/tree/stable/packages/simplex-chat-client/typescript' target='_blank'>用于创建聊天机器人的 SDK</a>, 允许用户实现通过 SimpleX Chat 应用程序与之交互的服务—— 我们真的很期待看到您可以依托SimpleX构建哪些服务。",
|
||||
"simplex-unique-overlay-card-2-p-2": "即使使用可选的用户地址,当它被用于发送垃圾邮件联系请求,您可以更改或完全删除它而不会丢失任何连接。",
|
||||
"simplex-unique-overlay-card-3-p-1": "SimpleX Chat 使用<strong>便携式加密数据库格式</strong>仅将所有用户数据存储在客户端设备上,该格式可以导出并传输到任何支持的设备。",
|
||||
@@ -226,10 +226,10 @@
|
||||
"guide-dropdown-1": "快速指南",
|
||||
"docs-dropdown-1": "SimpleX 平台",
|
||||
"docs-dropdown-5": "托管 XFTP 服务器",
|
||||
"newer-version-of-eng-msg": "本页面有较新的英语版本。",
|
||||
"guide-dropdown-4": "聊天档案",
|
||||
"guide-dropdown-6": "音视频通话",
|
||||
"newer-version-of-eng-msg": "本页面的英文版本有更新。",
|
||||
"guide-dropdown-4": "聊天资料",
|
||||
"guide-dropdown-6": "语音和视频通话",
|
||||
"guide-dropdown-9": "建立连接",
|
||||
"back-to-top": "回到页首",
|
||||
"back-to-top": "回到顶部",
|
||||
"on-this-page": "在此页面上"
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
<li>SMP server monitoring: status bot and page</li>
|
||||
</ul>
|
||||
|
||||
<p>Also, the app interface is now available in Chinese and Spanish - thanks to our users!</p>
|
||||
<p>Also, the app interface is now available in Chinese and Spanish - thanks our users!</p>
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
<li>networking improvements</li>
|
||||
</ul>
|
||||
|
||||
<p>Also, the app interface is now available in Polish - thanks to our users.</p>
|
||||
<p>Also, the app interface is now available in Polish - thanks our users.</p>
|
||||
|
||||
<p>SimpleX Chat apps are now available in 10 languages!</p>
|
||||
@@ -1,18 +0,0 @@
|
||||
<p>v5.1 is released:</p>
|
||||
|
||||
<ul class="mb-[12px]">
|
||||
<li>message reactions 🚀</li>
|
||||
<li><p>improved messages:</p>
|
||||
<ul>
|
||||
<li>voice messages up to 5 minutes</li>
|
||||
<li>custom time to disappear</li>
|
||||
<li>message editing history</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>new design and customizable themes you can share*!</li>
|
||||
<li>self-destruct passcode</li>
|
||||
</ul>
|
||||
|
||||
<p>Also, the app interface is now available in Japanese and Portuguese (Brazil)* - thanks to our users!</p>
|
||||
|
||||
<p>* Android app only.</p>
|
||||