mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e73f5c40cf | |||
| 4c960bdc44 | |||
| dcb82951ed | |||
| 138cce4436 | |||
| 98417dafc4 | |||
| e8374be19c | |||
| 62a2f61751 | |||
| 2d47175f94 | |||
| a6d7604d21 | |||
| 9e3573fc76 | |||
| 13ebaf587e | |||
| 61e20550bc | |||
| d1cc5c1769 | |||
| 16b041c8c6 | |||
| 810f248c74 | |||
| 813fecddfe | |||
| 5a7d61c964 | |||
| cba24983e6 | |||
| 4dc2a1b72d | |||
| 3d4e4e2ef9 | |||
| 113c67ec95 | |||
| a2e887024f | |||
| 37262b3ed5 | |||
| dca4fe7701 | |||
| 88c9334d18 | |||
| 58f06aa821 | |||
| ae5deab8d3 | |||
| edfece3206 | |||
| c32cf8055d | |||
| 72ec03a822 | |||
| d89e0efedd | |||
| 707e8592d9 | |||
| a1b27e9a99 | |||
| f68d8fd97c | |||
| abff42a264 | |||
| c1ced70836 | |||
| e14966d36e | |||
| 97943fc609 | |||
| 0f143b2e77 | |||
| be10dcbcfc | |||
| 97dbec927c | |||
| b4879ca2a3 | |||
| 768c497025 |
@@ -86,13 +86,15 @@ You can use SimpleX with your own servers and still communicate with people usin
|
|||||||
|
|
||||||
Recent updates:
|
Recent updates:
|
||||||
|
|
||||||
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration.](./blog/20221206-simplex-chat-v4.3-voice-messages.md)
|
[Jan 03, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||||
|
|
||||||
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
|
||||||
|
|
||||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md)
|
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||||
|
|
||||||
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md)
|
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||||
|
|
||||||
|
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md).
|
||||||
|
|
||||||
[All updates](./blog)
|
[All updates](./blog)
|
||||||
|
|
||||||
@@ -191,17 +193,21 @@ If you are considering developing with SimpleX platform please get in touch for
|
|||||||
- ✅ View deleted messages, full message deletion by sender (with recipient opt-in per contact).
|
- ✅ View deleted messages, full message deletion by sender (with recipient opt-in per contact).
|
||||||
- ✅ Block screenshots and view in recent apps.
|
- ✅ Block screenshots and view in recent apps.
|
||||||
- ✅ Advanced server configuration.
|
- ✅ Advanced server configuration.
|
||||||
|
- ✅ Disappearing messages (with recipient opt-in per-contact).
|
||||||
|
- ✅ "Live" messages.
|
||||||
|
- ✅ Contact verification via a separate out-of-band channel.
|
||||||
|
- 🏗 Multiple user profiles in the same chat database.
|
||||||
|
- 🏗 Optionally avoid re-using the same TCP session for multiple connections.
|
||||||
|
- 🏗 File server to optimize for efficient and private sending of large files.
|
||||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||||
- 🏗 Contact verification via a separate out-of-band channel.
|
- 🏗 Reduced battery and traffic usage in large groups.
|
||||||
- 🏗 Ephemeral/disappearing/OTR conversations with the existing contacts.
|
- 🏗 Preserve message drafts.
|
||||||
- Optionally avoid re-using the same TCP session for multiple connections.
|
- 🏗 Support older Android OS and 32-bit CPUs.
|
||||||
|
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||||
- Access password/pin (with optional alternative access password).
|
- Access password/pin (with optional alternative access password).
|
||||||
- Media server to optimize sending large files to groups.
|
|
||||||
- Video messages.
|
- Video messages.
|
||||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||||
- Multiple user profiles in the same chat database.
|
|
||||||
- Feeds/broadcasts.
|
- Feeds/broadcasts.
|
||||||
- Unconfirmed: disappearing messages (with recipient opt-in per-contact).
|
|
||||||
- Web widgets for custom interactivity in the chats.
|
- Web widgets for custom interactivity in the chats.
|
||||||
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
||||||
- Supporting the same profile on multiple devices.
|
- Supporting the same profile on multiple devices.
|
||||||
@@ -213,24 +219,22 @@ If you are considering developing with SimpleX platform please get in touch for
|
|||||||
|
|
||||||
## Join a user group
|
## Join a user group
|
||||||
|
|
||||||
You can join a general group with more than 100 members: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D).
|
You can join a general English speaking group: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FcIS0gu1h0Y8pZpQkDaSz7HZGSHcKpMB9%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAKzzWAJYrVt1zdgRp4pD3FBst6eK7233DJeNElENLJRA%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%228mazMhefXoM5HxWBfZnvwQ%3D%3D%22%7D).
|
||||||
|
|
||||||
You can also join smaller groups by countries/languages: [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FmIorjTDPG24jdLKXwutS6o9hdQQRZwfQ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA9N0BZaECrAw3we3S1Wq4QO7NERBuPt9447immrB50wo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22S8aISlOgkTMytSox9gAM2Q%3D%3D%22%7D) (German), [\#SimpleX-US](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FlTWmQplLEaoJyHnEL1-B3f2PtDsikcTs%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-hMBlsQjNxK2vaVhqW_UyAVtuoYqgYTigK4B9dJ9CGc%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22G0UtRHIn0TmPoo08h_cbTA%3D%3D%22%7D) (US/English), [\#SimpleX-France](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F11r6XyjwVMj0WDIUMbmNDXO996M_EN_1%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAXDmc2Lrj9WQOjEcWa0DeQHF3HcYOp9b68s8M_BJ7gEk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22EZCeSYpeIBkaQwCcpcF00w%3D%3D%22%7D), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FZSYM278L5WoZiApx3925EAjSXcsAVNVu%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA7RJ2wfT8zdfOLyE5OtWLEAPowj-q6F2HB0ExbATw8Gk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22fsVoklNGptt7n-droqJYUQ%3D%3D%22%7D) (Russian), [#SimpleX-NL](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmP0LbswSbfxoVkkxiWE2NYnBCgZ9Snvj%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAVwZuSsw4Mf52EaBNdNI3RebsLm0jg65ZIkcmH9E5uy8%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22M9xIULUNZx51Wsa5Kdb0Sg%3D%3D%22%7D) (Netherlands/Dutch), [#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaZ_wjh6QAYHB-LjyGtp8bllkzoq880u-%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-_Wulzc3j16i7t77XJ5wgwxeW8_Ea8GxetMo7K4MgjI%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22QWmXdrFzIeMd2OoEPMFkBQ%3D%3D%22%7D) (Italian).
|
Groups in languages other than English, that we have app interface translated into: [\#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).
|
||||||
|
|
||||||
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
|
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
|
||||||
|
|
||||||
Let us know if you'd like to add some other countries to the list.
|
|
||||||
|
|
||||||
Join via the app to share what's going on and ask any questions!
|
Join via the app to share what's going on and ask any questions!
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
We would love to have you join the development! You can contribute to SimpleX Chat with:
|
We would love to have you join the development! You can contribute to SimpleX Chat with:
|
||||||
|
|
||||||
- developing features - please connect to us via chat so we can help you get started.
|
- translate UI to your language - we are using [Weblate](https://hosted.weblate.org/projects/simplex-chat/) to translate the interface, please get in touch if you want to contribute!
|
||||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
|
||||||
- translate UI to some language - we are currently setting up the UI to simplify it, please get in touch and let us know if you would be able to support and update the translations.
|
|
||||||
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
|
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
|
||||||
|
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||||
|
- developing features - please connect to us via chat so we can help you get started.
|
||||||
|
|
||||||
## Help us with donations
|
## Help us with donations
|
||||||
|
|
||||||
@@ -250,6 +254,7 @@ It is possible to donate via:
|
|||||||
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||||
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||||
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||||
|
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
|
||||||
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
|
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
|
||||||
|
|
||||||
Thank you,
|
Thank you,
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "chat.simplex.app"
|
applicationId "chat.simplex.app"
|
||||||
minSdk 29
|
minSdk 29
|
||||||
targetSdk 32
|
targetSdk 32
|
||||||
versionCode 84
|
versionCode 88
|
||||||
versionName "4.4"
|
versionName "4.4.2"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
ndk {
|
ndk {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
|||||||
var isAppOnForeground: Boolean = false
|
var isAppOnForeground: Boolean = false
|
||||||
|
|
||||||
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
||||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() ?: ""
|
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
|
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
|
||||||
val res: DBMigrationResult = kotlin.runCatching {
|
val res: DBMigrationResult = kotlin.runCatching {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import chat.simplex.app.views.helpers.*
|
|||||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||||
import chat.simplex.app.views.usersettings.NotificationPreviewMode
|
import chat.simplex.app.views.usersettings.NotificationPreviewMode
|
||||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.datetime.*
|
import kotlinx.datetime.*
|
||||||
import kotlinx.serialization.*
|
import kotlinx.serialization.*
|
||||||
import kotlinx.serialization.descriptors.*
|
import kotlinx.serialization.descriptors.*
|
||||||
@@ -180,7 +181,13 @@ class ChatModel(val controller: ChatController) {
|
|||||||
}
|
}
|
||||||
// add to current chat
|
// add to current chat
|
||||||
if (chatId.value == cInfo.id) {
|
if (chatId.value == cInfo.id) {
|
||||||
chatItems.add(cItem)
|
runBlocking(Dispatchers.Main) {
|
||||||
|
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
|
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||||
|
} else {
|
||||||
|
chatItems.add(cItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +218,9 @@ class ChatModel(val controller: ChatController) {
|
|||||||
chatItems[itemIndex] = cItem
|
chatItems[itemIndex] = cItem
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
chatItems.add(cItem)
|
runBlocking(Dispatchers.Main) {
|
||||||
|
chatItems.add(cItem)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -255,6 +264,20 @@ class ChatModel(val controller: ChatController) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
|
||||||
|
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
||||||
|
runBlocking(Dispatchers.Main) {
|
||||||
|
chatItems.add(cItem)
|
||||||
|
}
|
||||||
|
return cItem
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeLiveDummy() {
|
||||||
|
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
|
chatItems.removeLast()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||||
val markedRead = markItemsReadInCurrentChat(cInfo, range)
|
val markedRead = markItemsReadInCurrentChat(cInfo, range)
|
||||||
// update preview
|
// update preview
|
||||||
@@ -1278,7 +1301,8 @@ data class ChatItem (
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
|
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
|
||||||
|
const val TEMP_LIVE_CHAT_ITEM_ID = -2L
|
||||||
|
|
||||||
val deletedItemDummy: ChatItem
|
val deletedItemDummy: ChatItem
|
||||||
get() = ChatItem(
|
get() = ChatItem(
|
||||||
chatDir = CIDirection.DirectRcv(),
|
chatDir = CIDirection.DirectRcv(),
|
||||||
@@ -1300,6 +1324,26 @@ data class ChatItem (
|
|||||||
file = null
|
file = null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun liveDummy(direct: Boolean): ChatItem = ChatItem(
|
||||||
|
chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(),
|
||||||
|
meta = CIMeta(
|
||||||
|
itemId = TEMP_LIVE_CHAT_ITEM_ID,
|
||||||
|
itemTs = Clock.System.now(),
|
||||||
|
itemText = "",
|
||||||
|
itemStatus = CIStatus.RcvRead(),
|
||||||
|
createdAt = Clock.System.now(),
|
||||||
|
updatedAt = Clock.System.now(),
|
||||||
|
itemDeleted = false,
|
||||||
|
itemEdited = false,
|
||||||
|
itemTimed = null,
|
||||||
|
itemLive = true,
|
||||||
|
editable = false
|
||||||
|
),
|
||||||
|
content = CIContent.SndMsgContent(MsgContent.MCText("")),
|
||||||
|
quotedItem = null,
|
||||||
|
file = null
|
||||||
|
)
|
||||||
|
|
||||||
fun invalidJSON(json: String): ChatItem =
|
fun invalidJSON(json: String): ChatItem =
|
||||||
ChatItem(
|
ChatItem(
|
||||||
chatDir = CIDirection.DirectSnd(),
|
chatDir = CIDirection.DirectSnd(),
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class AppPreferences(val context: Context) {
|
|||||||
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
||||||
val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true)
|
val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true)
|
||||||
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
|
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
|
||||||
val privacyTransferImagesInline = mkBoolPreference(SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE, false)
|
val privacyTransferImagesInline = mkBoolPreference(SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE, true)
|
||||||
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
|
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
|
||||||
private val _simplexLinkMode = mkStrPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default.name)
|
private val _simplexLinkMode = mkStrPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default.name)
|
||||||
val simplexLinkMode: SharedPreference<SimplexLinkMode> = SharedPreference(
|
val simplexLinkMode: SharedPreference<SimplexLinkMode> = SharedPreference(
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ fun TerminalLayout(
|
|||||||
SendMsgView(
|
SendMsgView(
|
||||||
composeState = composeState,
|
composeState = composeState,
|
||||||
showVoiceRecordIcon = false,
|
showVoiceRecordIcon = false,
|
||||||
recState = mutableStateOf(RecordingState.NotStarted),
|
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||||
isDirectChat = false,
|
isDirectChat = false,
|
||||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
@@ -147,8 +147,8 @@ fun TerminalLayout(
|
|||||||
sendMessage = sendCommand,
|
sendMessage = sendCommand,
|
||||||
sendLiveMessage = null,
|
sendLiveMessage = null,
|
||||||
updateLiveMessage = null,
|
updateLiveMessage = null,
|
||||||
::onMessageChange,
|
onMessageChange = ::onMessageChange,
|
||||||
textStyle
|
textStyle = textStyle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -568,7 +568,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
if (composeState.value.editing) {
|
if (composeState.value.editing) {
|
||||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||||
} else {
|
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ sealed class ComposeContextItem {
|
|||||||
data class LiveMessage(
|
data class LiveMessage(
|
||||||
val chatItem: ChatItem,
|
val chatItem: ChatItem,
|
||||||
val typedMsg: String,
|
val typedMsg: String,
|
||||||
val sentMsg: String
|
val sentMsg: String,
|
||||||
|
val sent: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -103,6 +104,9 @@ data class ComposeState(
|
|||||||
}
|
}
|
||||||
hasContent && !inProgress
|
hasContent && !inProgress
|
||||||
}
|
}
|
||||||
|
val endLiveDisabled: Boolean
|
||||||
|
get() = liveMessage != null && message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
|
||||||
|
|
||||||
val linkPreviewAllowed: Boolean
|
val linkPreviewAllowed: Boolean
|
||||||
get() =
|
get() =
|
||||||
when (preview) {
|
when (preview) {
|
||||||
@@ -352,6 +356,7 @@ fun ComposeView(
|
|||||||
chosenContent.value = emptyList()
|
chosenContent.value = emptyList()
|
||||||
chosenAudio.value = null
|
chosenAudio.value = null
|
||||||
chosenFile.value = null
|
chosenFile.value = null
|
||||||
|
chatModel.removeLiveDummy()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
|
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
|
||||||
@@ -430,7 +435,7 @@ fun ComposeView(
|
|||||||
if (cs.contextItem is ComposeContextItem.EditingItem) {
|
if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||||
val ei = cs.contextItem.chatItem
|
val ei = cs.contextItem.chatItem
|
||||||
sent = updateMessage(ei, cInfo, live)
|
sent = updateMessage(ei, cInfo, live)
|
||||||
} else if (liveMessage != null) {
|
} else if (liveMessage != null && liveMessage.sent) {
|
||||||
sent = updateMessage(liveMessage.chatItem, cInfo, live)
|
sent = updateMessage(liveMessage.chatItem, cInfo, live)
|
||||||
} else {
|
} else {
|
||||||
val msgs: ArrayList<MsgContent> = ArrayList()
|
val msgs: ArrayList<MsgContent> = ArrayList()
|
||||||
@@ -569,13 +574,16 @@ fun ComposeView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendLiveMessage() {
|
suspend fun sendLiveMessage() {
|
||||||
val typedMsg = composeState.value.message
|
val cs = composeState.value
|
||||||
val sentMsg = truncateToWords(typedMsg)
|
val typedMsg = cs.message
|
||||||
if (composeState.value.liveMessage == null) {
|
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
|
||||||
val ci = sendMessageAsync(sentMsg, live = true)
|
val ci = sendMessageAsync(typedMsg, live = true)
|
||||||
if (ci != null) {
|
if (ci != null) {
|
||||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg))
|
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||||
}
|
}
|
||||||
|
} else if (cs.liveMessage == null) {
|
||||||
|
val cItem = chatModel.addLiveDummy(chat.chatInfo)
|
||||||
|
composeState.value = composeState.value.copy(liveMessage = LiveMessage(cItem, typedMsg = typedMsg, sentMsg = typedMsg, sent = false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,7 +600,7 @@ fun ComposeView(
|
|||||||
if (sentMsg != null) {
|
if (sentMsg != null) {
|
||||||
val ci = sendMessageAsync(sentMsg, live = true)
|
val ci = sendMessageAsync(sentMsg, live = true)
|
||||||
if (ci != null) {
|
if (ci != null) {
|
||||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg))
|
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||||
}
|
}
|
||||||
} else if (liveMessage.typedMsg != typedMsg) {
|
} else if (liveMessage.typedMsg != typedMsg) {
|
||||||
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
|
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
|
||||||
@@ -701,9 +709,13 @@ fun ComposeView(
|
|||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val orientation = activity.resources.configuration.orientation
|
val orientation = activity.resources.configuration.orientation
|
||||||
onDispose {
|
onDispose {
|
||||||
if (orientation == activity.resources.configuration.orientation && composeState.value.liveMessage != null) {
|
if (orientation == activity.resources.configuration.orientation) {
|
||||||
sendMessage()
|
val cs = composeState.value
|
||||||
resetLinkPreview()
|
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
|
||||||
|
sendMessage()
|
||||||
|
resetLinkPreview()
|
||||||
|
}
|
||||||
|
chatModel.removeLiveDummy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -723,6 +735,10 @@ fun ComposeView(
|
|||||||
},
|
},
|
||||||
sendLiveMessage = ::sendLiveMessage,
|
sendLiveMessage = ::sendLiveMessage,
|
||||||
updateLiveMessage = ::updateLiveMessage,
|
updateLiveMessage = ::updateLiveMessage,
|
||||||
|
cancelLiveMessage = {
|
||||||
|
composeState.value = composeState.value.copy(liveMessage = null)
|
||||||
|
chatModel.removeLiveDummy()
|
||||||
|
},
|
||||||
onMessageChange = ::onMessageChange,
|
onMessageChange = ::onMessageChange,
|
||||||
textStyle = textStyle
|
textStyle = textStyle
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import android.app.Activity
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.os.Build
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.WindowManager
|
||||||
import android.view.inputmethod.*
|
import android.view.inputmethod.*
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
@@ -60,14 +62,15 @@ fun SendMsgView(
|
|||||||
allowedVoiceByPrefs: Boolean,
|
allowedVoiceByPrefs: Boolean,
|
||||||
allowVoiceToContact: () -> Unit,
|
allowVoiceToContact: () -> Unit,
|
||||||
sendMessage: () -> Unit,
|
sendMessage: () -> Unit,
|
||||||
sendLiveMessage: ( suspend () -> Unit)? = null,
|
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||||
updateLiveMessage: (suspend () -> Unit)? = null,
|
updateLiveMessage: (suspend () -> Unit)? = null,
|
||||||
|
cancelLiveMessage: (() -> Unit)? = null,
|
||||||
onMessageChange: (String) -> Unit,
|
onMessageChange: (String) -> Unit,
|
||||||
textStyle: MutableState<TextStyle>
|
textStyle: MutableState<TextStyle>
|
||||||
) {
|
) {
|
||||||
Box(Modifier.padding(vertical = 8.dp)) {
|
Box(Modifier.padding(vertical = 8.dp)) {
|
||||||
val cs = composeState.value
|
val cs = composeState.value
|
||||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
||||||
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||||
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
||||||
NativeKeyboard(composeState, textStyle, onMessageChange)
|
NativeKeyboard(composeState, textStyle, onMessageChange)
|
||||||
@@ -106,7 +109,10 @@ fun SendMsgView(
|
|||||||
else ->
|
else ->
|
||||||
RecordVoiceView(recState, stopRecOnNextClick)
|
RecordVoiceView(recState, stopRecOnNextClick)
|
||||||
}
|
}
|
||||||
if (sendLiveMessage != null && updateLiveMessage != null && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)) {
|
if (sendLiveMessage != null
|
||||||
|
&& updateLiveMessage != null
|
||||||
|
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||||
|
&& cs.contextItem is ComposeContextItem.NoContextItem) {
|
||||||
Spacer(Modifier.width(10.dp))
|
Spacer(Modifier.width(10.dp))
|
||||||
StartLiveMessageButton {
|
StartLiveMessageButton {
|
||||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||||
@@ -116,15 +122,24 @@ fun SendMsgView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cs.liveMessage?.sent == false && cs.message.isEmpty() -> {
|
||||||
|
CancelLiveMessageButton {
|
||||||
|
cancelLiveMessage?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
val cs = composeState.value
|
||||||
val icon = if (cs.editing || cs.liveMessage != null) Icons.Filled.Check else Icons.Outlined.ArrowUpward
|
val icon = if (cs.editing || cs.liveMessage != null) Icons.Filled.Check else Icons.Outlined.ArrowUpward
|
||||||
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
|
val disabled = !cs.sendEnabled() ||
|
||||||
if (composeState.value.liveMessage == null &&
|
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||||
|
cs.endLiveDisabled
|
||||||
|
if (cs.liveMessage == null &&
|
||||||
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
|
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
|
||||||
|
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||||
sendLiveMessage != null && updateLiveMessage != null
|
sendLiveMessage != null && updateLiveMessage != null
|
||||||
) {
|
) {
|
||||||
var showDropdown by rememberSaveable { mutableStateOf(false) }
|
var showDropdown by rememberSaveable { mutableStateOf(false) }
|
||||||
SendTextButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled(), sendMessage) { showDropdown = true }
|
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showDropdown,
|
expanded = showDropdown,
|
||||||
@@ -141,7 +156,7 @@ fun SendMsgView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SendTextButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled(), sendMessage)
|
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,7 +178,6 @@ private fun NativeKeyboard(
|
|||||||
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
|
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||||
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
|
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
|
||||||
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
|
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||||
|
|
||||||
var showKeyboard by remember { mutableStateOf(false) }
|
var showKeyboard by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(cs.contextItem) {
|
LaunchedEffect(cs.contextItem) {
|
||||||
if (cs.contextItem is ComposeContextItem.QuotedItem) {
|
if (cs.contextItem is ComposeContextItem.QuotedItem) {
|
||||||
@@ -184,6 +198,7 @@ private fun NativeKeyboard(
|
|||||||
) {
|
) {
|
||||||
super.setOnReceiveContentListener(mimeTypes, listener)
|
super.setOnReceiveContentListener(mimeTypes, listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||||
val connection = super.onCreateInputConnection(editorInfo)
|
val connection = super.onCreateInputConnection(editorInfo)
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||||
@@ -323,7 +338,9 @@ private fun LockToCurrentOrientationUntilDispose() {
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
val activity = context as Activity
|
val activity = context as Activity
|
||||||
activity.requestedOrientation = when (activity.display?.rotation) {
|
val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager
|
||||||
|
val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation
|
||||||
|
activity.requestedOrientation = when (rotation) {
|
||||||
android.view.Surface.ROTATION_90 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
android.view.Surface.ROTATION_90 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
android.view.Surface.ROTATION_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
android.view.Surface.ROTATION_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
android.view.Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
android.view.Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
@@ -334,7 +351,6 @@ private fun LockToCurrentOrientationUntilDispose() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StopRecordButton(onClick: () -> Unit) {
|
private fun StopRecordButton(onClick: () -> Unit) {
|
||||||
IconButton(onClick, Modifier.size(36.dp)) {
|
IconButton(onClick, Modifier.size(36.dp)) {
|
||||||
@@ -369,9 +385,24 @@ private fun ProgressIndicator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SendTextButton(
|
private fun CancelLiveMessageButton(
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
IconButton(onClick, Modifier.size(36.dp)) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Close,
|
||||||
|
stringResource(R.string.icon_descr_cancel_live_message),
|
||||||
|
tint = MaterialTheme.colors.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SendMsgButton(
|
||||||
icon: ImageVector,
|
icon: ImageVector,
|
||||||
backgroundColor: Color,
|
|
||||||
sizeDp: Animatable<Float, AnimationVector1D>,
|
sizeDp: Animatable<Float, AnimationVector1D>,
|
||||||
alpha: Animatable<Float, AnimationVector1D>,
|
alpha: Animatable<Float, AnimationVector1D>,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
@@ -400,7 +431,7 @@ private fun SendTextButton(
|
|||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.alpha(alpha.value)
|
.alpha(alpha.value)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(backgroundColor)
|
.background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight)
|
||||||
.padding(3.dp)
|
.padding(3.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -519,7 +550,7 @@ fun PreviewSendMsgView() {
|
|||||||
SendMsgView(
|
SendMsgView(
|
||||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||||
showVoiceRecordIcon = false,
|
showVoiceRecordIcon = false,
|
||||||
recState = mutableStateOf(RecordingState.NotStarted),
|
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||||
isDirectChat = true,
|
isDirectChat = true,
|
||||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
@@ -547,7 +578,7 @@ fun PreviewSendMsgViewEditing() {
|
|||||||
SendMsgView(
|
SendMsgView(
|
||||||
composeState = remember { mutableStateOf(composeStateEditing) },
|
composeState = remember { mutableStateOf(composeStateEditing) },
|
||||||
showVoiceRecordIcon = false,
|
showVoiceRecordIcon = false,
|
||||||
recState = mutableStateOf(RecordingState.NotStarted),
|
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||||
isDirectChat = true,
|
isDirectChat = true,
|
||||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
@@ -575,7 +606,7 @@ fun PreviewSendMsgViewInProgress() {
|
|||||||
SendMsgView(
|
SendMsgView(
|
||||||
composeState = remember { mutableStateOf(composeStateInProgress) },
|
composeState = remember { mutableStateOf(composeStateInProgress) },
|
||||||
showVoiceRecordIcon = false,
|
showVoiceRecordIcon = false,
|
||||||
recState = mutableStateOf(RecordingState.NotStarted),
|
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||||
isDirectChat = true,
|
isDirectChat = true,
|
||||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||||
needToAllowVoiceToContact = false,
|
needToAllowVoiceToContact = false,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
|
|||||||
if (meta.itemEdited) res += iconSpace
|
if (meta.itemEdited) res += iconSpace
|
||||||
if (meta.itemTimed != null) {
|
if (meta.itemTimed != null) {
|
||||||
res += iconSpace
|
res += iconSpace
|
||||||
val ttl = meta.itemTimed?.ttl
|
val ttl = meta.itemTimed.ttl
|
||||||
if (ttl != chatTTL) {
|
if (ttl != chatTTL) {
|
||||||
res += TimedMessagesPreference.shortTtlText(ttl)
|
res += TimedMessagesPreference.shortTtlText(ttl)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ fun ChatItemView(
|
|||||||
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
|
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
|
||||||
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
|
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
|
||||||
val onLinkLongClick = { _: String -> showMenu.value = true }
|
val onLinkLongClick = { _: String -> showMenu.value = true }
|
||||||
|
val live = composeState.value.liveMessage != null
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -97,7 +98,7 @@ fun ChatItemView(
|
|||||||
onDismissRequest = { showMenu.value = false },
|
onDismissRequest = { showMenu.value = false },
|
||||||
Modifier.width(220.dp)
|
Modifier.width(220.dp)
|
||||||
) {
|
) {
|
||||||
if (!cItem.meta.itemDeleted) {
|
if (!cItem.meta.itemDeleted && !live) {
|
||||||
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
|
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
|
||||||
if (composeState.value.editing) {
|
if (composeState.value.editing) {
|
||||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||||
@@ -133,7 +134,7 @@ fun ChatItemView(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice) {
|
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||||
ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = {
|
ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = {
|
||||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||||
showMenu.value = false
|
showMenu.value = false
|
||||||
@@ -149,7 +150,9 @@ fun ChatItemView(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
if (!(live && cItem.meta.isLive)) {
|
||||||
|
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
|||||||
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
||||||
stopped
|
stopped
|
||||||
)
|
)
|
||||||
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection -> {}
|
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
<string name="simplex_link_mode_browser_warning">Das Öffnen des Links über den Browser kann die Privatsphäre und Sicherheit der Verbindung reduzieren. SimpleX-Links, denen nicht vertraut wird, werden Rot sein.</string>
|
<string name="simplex_link_mode_browser_warning">Das Öffnen des Links über den Browser kann die Privatsphäre und Sicherheit der Verbindung reduzieren. SimpleX-Links, denen nicht vertraut wird, werden Rot sein.</string>
|
||||||
<!-- SimpleXAPI.kt -->
|
<!-- SimpleXAPI.kt -->
|
||||||
<string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string>
|
<string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string>
|
||||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</string>
|
<string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string>
|
||||||
<string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerk-Konfiguration.</string>
|
<string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerk-Konfiguration.</string>
|
||||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||||
<string name="connection_timeout">Verbindungszeitüberschreitung</string>
|
<string name="connection_timeout">Verbindungszeitüberschreitung</string>
|
||||||
@@ -230,8 +230,8 @@
|
|||||||
<string name="icon_descr_send_message">Nachricht senden</string>
|
<string name="icon_descr_send_message">Nachricht senden</string>
|
||||||
<string name="icon_descr_record_voice_message">Nehme Sprachnachricht auf</string>
|
<string name="icon_descr_record_voice_message">Nehme Sprachnachricht auf</string>
|
||||||
<string name="allow_voice_messages_question">Sprachnachrichten erlauben?</string>
|
<string name="allow_voice_messages_question">Sprachnachrichten erlauben?</string>
|
||||||
<string name="you_need_to_allow_to_send_voice">Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, damit Sie sie senden können.</string>
|
<string name="you_need_to_allow_to_send_voice">Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, damit Sie sie versenden können.</string>
|
||||||
<string name="voice_messages_prohibited">Sprachnachrichten unzulässig!</string>
|
<string name="voice_messages_prohibited">Sprachnachrichten nicht erlaubt!</string>
|
||||||
<string name="ask_your_contact_to_enable_voice">Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren.</string>
|
<string name="ask_your_contact_to_enable_voice">Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren.</string>
|
||||||
<string name="only_group_owners_can_enable_voice">Sprachnachrichten können nur von Gruppen-Eigentümern aktiviert werden.</string>
|
<string name="only_group_owners_can_enable_voice">Sprachnachrichten können nur von Gruppen-Eigentümern aktiviert werden.</string>
|
||||||
<!-- General Actions / Responses -->
|
<!-- General Actions / Responses -->
|
||||||
@@ -361,7 +361,7 @@
|
|||||||
<string name="smp_servers_add">Füge Server hinzu…</string>
|
<string name="smp_servers_add">Füge Server hinzu…</string>
|
||||||
<string name="smp_servers_test_server">Teste Server</string>
|
<string name="smp_servers_test_server">Teste Server</string>
|
||||||
<string name="smp_servers_test_servers">Teste alle Server</string>
|
<string name="smp_servers_test_servers">Teste alle Server</string>
|
||||||
<string name="smp_servers_save">Sichere alle Server</string>
|
<string name="smp_servers_save">Alle Server speichern</string>
|
||||||
<string name="smp_servers_test_failed">Server Test ist fehlgeschlagen!</string>
|
<string name="smp_servers_test_failed">Server Test ist fehlgeschlagen!</string>
|
||||||
<string name="smp_servers_test_some_failed">Einige Server haben den Test nicht bestanden:</string>
|
<string name="smp_servers_test_some_failed">Einige Server haben den Test nicht bestanden:</string>
|
||||||
<string name="smp_servers_scan_qr">Scannen Sie den QR-Code des Servers</string>
|
<string name="smp_servers_scan_qr">Scannen Sie den QR-Code des Servers</string>
|
||||||
@@ -386,10 +386,10 @@
|
|||||||
<string name="how_to_use_your_servers">Wie Sie Ihre Server nutzen</string>
|
<string name="how_to_use_your_servers">Wie Sie Ihre Server nutzen</string>
|
||||||
<string name="saved_ICE_servers_will_be_removed">Gespeicherte WebRTC ICE-Server werden entfernt.</string>
|
<string name="saved_ICE_servers_will_be_removed">Gespeicherte WebRTC ICE-Server werden entfernt.</string>
|
||||||
<string name="your_ICE_servers">Ihre ICE-Server</string>
|
<string name="your_ICE_servers">Ihre ICE-Server</string>
|
||||||
<string name="configure_ICE_servers">Konfigurieren Sie ICE-Server</string>
|
<string name="configure_ICE_servers">ICE-Server konfigurieren</string>
|
||||||
<string name="enter_one_ICE_server_per_line">ICE-Server (einer pro Zeile)</string>
|
<string name="enter_one_ICE_server_per_line">ICE-Server (einer pro Zeile)</string>
|
||||||
<string name="error_saving_ICE_servers">Fehler beim Speichern der ICE-Server</string>
|
<string name="error_saving_ICE_servers">Fehler beim Speichern der ICE-Server</string>
|
||||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</string>
|
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string>
|
||||||
<string name="save_servers_button">Speichern</string>
|
<string name="save_servers_button">Speichern</string>
|
||||||
<string name="network_and_servers">Netzwerk & Server</string>
|
<string name="network_and_servers">Netzwerk & Server</string>
|
||||||
<string name="network_settings">Erweiterte Netzwerkeinstellungen</string>
|
<string name="network_settings">Erweiterte Netzwerkeinstellungen</string>
|
||||||
@@ -882,29 +882,29 @@
|
|||||||
<string name="allow_your_contacts_irreversibly_delete">Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen.</string>
|
<string name="allow_your_contacts_irreversibly_delete">Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen.</string>
|
||||||
<string name="allow_irreversible_message_deletion_only_if">Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</string>
|
<string name="allow_irreversible_message_deletion_only_if">Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</string>
|
||||||
<string name="contacts_can_mark_messages_for_deletion">Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen.</string>
|
<string name="contacts_can_mark_messages_for_deletion">Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen.</string>
|
||||||
<string name="allow_your_contacts_to_send_voice_messages">Erlauben Sie Ihre Kontakten Sprachnachrichten zu senden.</string>
|
<string name="allow_your_contacts_to_send_voice_messages">Erlauben Sie Ihre Kontakten Sprachnachrichten zu versenden.</string>
|
||||||
<string name="allow_voice_messages_only_if">Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt.</string>
|
<string name="allow_voice_messages_only_if">Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt.</string>
|
||||||
<string name="prohibit_sending_voice_messages">Das Senden von Sprachnachrichten verbieten.</string>
|
<string name="prohibit_sending_voice_messages">Das Senden von Sprachnachrichten nicht erlauben.</string>
|
||||||
<string name="both_you_and_your_contacts_can_delete">Sowohl Ihr Kontakt, als auch Sie können Nachrichten unwiederbringlich löschen.</string>
|
<string name="both_you_and_your_contacts_can_delete">Sowohl Ihr Kontakt, als auch Sie können Nachrichten unwiederbringlich löschen.</string>
|
||||||
<string name="only_you_can_delete_messages">Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren).</string>
|
<string name="only_you_can_delete_messages">Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren).</string>
|
||||||
<string name="only_your_contact_can_delete">Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).</string>
|
<string name="only_your_contact_can_delete">Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).</string>
|
||||||
<string name="message_deletion_prohibited">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string>
|
<string name="message_deletion_prohibited">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string>
|
||||||
<string name="both_you_and_your_contact_can_send_voice">Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden.</string>
|
<string name="both_you_and_your_contact_can_send_voice">Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten versenden.</string>
|
||||||
<string name="only_you_can_send_voice">Nur Sie können Sprachnachrichten senden.</string>
|
<string name="only_you_can_send_voice">Nur Sie können Sprachnachrichten versenden.</string>
|
||||||
<string name="only_your_contact_can_send_voice">Nur Ihr Kontakt kann Sprachnachrichten senden.</string>
|
<string name="only_your_contact_can_send_voice">Nur Ihr Kontakt kann Sprachnachrichten versenden.</string>
|
||||||
<string name="voice_prohibited_in_this_chat">In diesem Chat sind Sprachnachrichten untersagt.</string>
|
<string name="voice_prohibited_in_this_chat">In diesem Chat sind Sprachnachrichten nicht erlaubt.</string>
|
||||||
<string name="allow_direct_messages">Das Senden von Direktnachrichten an Mitglieder erlauben.</string>
|
<string name="allow_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder erlauben.</string>
|
||||||
<string name="prohibit_direct_messages">Das Senden von Direktnachrichten an Mitglieder verbieten.</string>
|
<string name="prohibit_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben.</string>
|
||||||
<string name="allow_to_delete_messages">Unwiederbringliches Löschen von gesendeten Nachrichten erlauben.</string>
|
<string name="allow_to_delete_messages">Unwiederbringliches löschen von gesendeten Nachrichten erlauben.</string>
|
||||||
<string name="prohibit_message_deletion">Unwiederbringliches Löschen von Nachrichten verbieten.</string>
|
<string name="prohibit_message_deletion">Unwiederbringliches löschen von Nachrichten nicht erlauben.</string>
|
||||||
<string name="allow_to_send_voice">Senden von Sprachnachrichten erlauben.</string>
|
<string name="allow_to_send_voice">Das Senden von Sprachnachrichten erlauben.</string>
|
||||||
<string name="prohibit_sending_voice">Senden von Sprachnachrichten untersagen.</string>
|
<string name="prohibit_sending_voice">Das Senden von Sprachnachrichten nicht erlauben.</string>
|
||||||
<string name="group_members_can_send_dms">Gruppenmitglieder können Direktnachrichten versenden.</string>
|
<string name="group_members_can_send_dms">Gruppenmitglieder können Direktnachrichten versenden.</string>
|
||||||
<string name="direct_messages_are_prohibited_in_chat">In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht möglich.</string>
|
<string name="direct_messages_are_prohibited_in_chat">In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</string>
|
||||||
<string name="group_members_can_delete">Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen.</string>
|
<string name="group_members_can_delete">Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen.</string>
|
||||||
<string name="message_deletion_prohibited_in_chat">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten verboten.</string>
|
<string name="message_deletion_prohibited_in_chat">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string>
|
||||||
<string name="group_members_can_send_voice">Gruppenmitglieder können Sprachnachrichten senden.</string>
|
<string name="group_members_can_send_voice">Gruppenmitglieder können Sprachnachrichten versenden.</string>
|
||||||
<string name="voice_messages_are_prohibited">In dieser Gruppe sind Sprachnachrichten untersagt.</string>
|
<string name="voice_messages_are_prohibited">In dieser Gruppe sind Sprachnachrichten nicht erlaubt.</string>
|
||||||
<string name="live">LIVE</string>
|
<string name="live">LIVE</string>
|
||||||
<string name="view_security_code">Schauen Sie sich den Sicherheitscode an</string>
|
<string name="view_security_code">Schauen Sie sich den Sicherheitscode an</string>
|
||||||
<string name="onboarding_notifications_mode_service">Sofort</string>
|
<string name="onboarding_notifications_mode_service">Sofort</string>
|
||||||
@@ -912,7 +912,7 @@
|
|||||||
<string name="onboarding_notifications_mode_off_desc"><b>Beste Option für die Batterieausdauer</b>. Sie empfangen Benachrichtigungen nur solange die App abläuft. Der Hintergrundservice wird nicht genutzt!</string>
|
<string name="onboarding_notifications_mode_off_desc"><b>Beste Option für die Batterieausdauer</b>. Sie empfangen Benachrichtigungen nur solange die App abläuft. Der Hintergrundservice wird nicht genutzt!</string>
|
||||||
<string name="send_verb">Senden</string>
|
<string name="send_verb">Senden</string>
|
||||||
<string name="is_verified">%s wurde überprüft</string>
|
<string name="is_verified">%s wurde überprüft</string>
|
||||||
<string name="clear_verification">Überprüfung erfolgt</string>
|
<string name="clear_verification">Überprüfung zurücknehmen</string>
|
||||||
<string name="onboarding_notifications_mode_off">Solange die App abläuft</string>
|
<string name="onboarding_notifications_mode_off">Solange die App abläuft</string>
|
||||||
<string name="onboarding_notifications_mode_subtitle">Kann später über die Einstellungen geändert werden.</string>
|
<string name="onboarding_notifications_mode_subtitle">Kann später über die Einstellungen geändert werden.</string>
|
||||||
<string name="delete_after">Löschen nach</string>
|
<string name="delete_after">Löschen nach</string>
|
||||||
@@ -978,7 +978,7 @@
|
|||||||
<string name="v4_3_improved_privacy_and_security_desc">App-Bildschirm in aktuellen Anwendungen verbergen.</string>
|
<string name="v4_3_improved_privacy_and_security_desc">App-Bildschirm in aktuellen Anwendungen verbergen.</string>
|
||||||
<string name="v4_3_improved_privacy_and_security">Verbesserte Privatsphäre und Sicherheit</string>
|
<string name="v4_3_improved_privacy_and_security">Verbesserte Privatsphäre und Sicherheit</string>
|
||||||
<string name="v4_3_improved_server_configuration">Verbesserte Serverkonfiguration</string>
|
<string name="v4_3_improved_server_configuration">Verbesserte Serverkonfiguration</string>
|
||||||
<string name="v4_3_irreversible_message_deletion">Unwiederbringliches Löschen einer Nachricht</string>
|
<string name="v4_3_irreversible_message_deletion">Unwiederbringliches löschen einer Nachricht</string>
|
||||||
<string name="v4_4_live_messages">Live Nachrichten</string>
|
<string name="v4_4_live_messages">Live Nachrichten</string>
|
||||||
<string name="v4_3_voice_messages_desc">Max. 40 Sekunden, sofort erhalten.</string>
|
<string name="v4_3_voice_messages_desc">Max. 40 Sekunden, sofort erhalten.</string>
|
||||||
<string name="v4_4_live_messages_desc">Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</string>
|
<string name="v4_4_live_messages_desc">Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</string>
|
||||||
|
|||||||
@@ -296,7 +296,7 @@
|
|||||||
<string name="toast_permission_denied">Autorisation refusée !</string>
|
<string name="toast_permission_denied">Autorisation refusée !</string>
|
||||||
<string name="use_camera_button">Utiliser l\'Appareil photo</string>
|
<string name="use_camera_button">Utiliser l\'Appareil photo</string>
|
||||||
<string name="thank_you_for_installing_simplex">Merci d\'avoir installé <xliff:g id="appNameFull">SimpleX Chat</xliff:g> !</string>
|
<string name="thank_you_for_installing_simplex">Merci d\'avoir installé <xliff:g id="appNameFull">SimpleX Chat</xliff:g> !</string>
|
||||||
<string name="you_can_connect_to_simplex_chat_founder">Vous pouvez <font color="#0088ff">vous connecter aux développeurs de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour leur poser toutes vos questions et pour recevoir des informations sur les mises à jour</font>.</string>
|
<string name="you_can_connect_to_simplex_chat_founder">Vous pouvez <font color="#0088ff">vous connecter aux développeurs de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour leur poser des questions et recevoir des réponses :</font>.</string>
|
||||||
<string name="above_then_preposition_continuation">ci-dessus, puis :</string>
|
<string name="above_then_preposition_continuation">ci-dessus, puis :</string>
|
||||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Ajouter un nouveau contact</b> : afin de créer un code QR à usage unique pour votre contact.</string>
|
<string name="add_new_contact_to_create_one_time_QR_code"><b>Ajouter un nouveau contact</b> : afin de créer un code QR à usage unique pour votre contact.</string>
|
||||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si vous choisissez de la rejeter, l\'expéditeur·rice NE sera PAS notifié·e.</string>
|
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si vous choisissez de la rejeter, l\'expéditeur·rice NE sera PAS notifié·e.</string>
|
||||||
@@ -328,7 +328,7 @@
|
|||||||
<string name="network_use_onion_hosts">Utiliser les hôtes .onions</string>
|
<string name="network_use_onion_hosts">Utiliser les hôtes .onions</string>
|
||||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
|
<string name="network_use_onion_hosts_prefer_desc_in_alert">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
|
||||||
<string name="network_use_onion_hosts_required_desc_in_alert">Les hôtes .onion seront nécessaires pour la connexion.</string>
|
<string name="network_use_onion_hosts_required_desc_in_alert">Les hôtes .onion seront nécessaires pour la connexion.</string>
|
||||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Vous contrôlez par quel·s serveur·s vous pouvez <b>transmettre</b> ainsi que par quel·s serveur·s vous pouvez <b>recevoir</b> des messages de vos contacts.</string>
|
<string name="you_control_servers_to_receive_your_contacts_to_send">Vous contrôlez par quel·s serveur·s vous pouvez <b>transmettre</b> ainsi que par quel·s serveur·s vous pouvez <b>recevoir</b> les messages de vos contacts.</string>
|
||||||
<string name="your_settings">Vos paramètres</string>
|
<string name="your_settings">Vos paramètres</string>
|
||||||
<string name="chat_lock">SimpleX Lock</string>
|
<string name="chat_lock">SimpleX Lock</string>
|
||||||
<string name="chat_console">Console du chat</string>
|
<string name="chat_console">Console du chat</string>
|
||||||
@@ -381,7 +381,7 @@
|
|||||||
<string name="callstate_received_confirmation">confimation reçu…</string>
|
<string name="callstate_received_confirmation">confimation reçu…</string>
|
||||||
<string name="callstate_connecting">connexion…</string>
|
<string name="callstate_connecting">connexion…</string>
|
||||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocole et code open-source – n\'importe qui peut heberger un serveur.</string>
|
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocole et code open-source – n\'importe qui peut heberger un serveur.</string>
|
||||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Pour protéger la vie privée, au lieu d\'ID d\'utilisateur utilisés par toutes les autres plateformes, <xliff:g id="appName">SimpleX</xliff:g> possède des identifiants pour les files d\'attente de messages, distincts pour chacun de vos contacts.</string>
|
<string name="to_protect_privacy_simplex_has_ids_for_queues">Pour protéger votre vie privée, au lieu d\'IDs utilisés par toutes les autres plateformes, <xliff:g id="appName">SimpleX</xliff:g> possède des IDs pour les queues de messages, distinctes pour chacun de vos contacts.</string>
|
||||||
<string name="read_more_in_github">Plus d\'informations sur notre GitHub.</string>
|
<string name="read_more_in_github">Plus d\'informations sur notre GitHub.</string>
|
||||||
<string name="paste_the_link_you_received">Coller le lien reçu</string>
|
<string name="paste_the_link_you_received">Coller le lien reçu</string>
|
||||||
<string name="use_chat">Utiliser le chat</string>
|
<string name="use_chat">Utiliser le chat</string>
|
||||||
@@ -467,7 +467,7 @@
|
|||||||
<string name="make_private_connection">Établir une connexion privée</string>
|
<string name="make_private_connection">Établir une connexion privée</string>
|
||||||
<string name="how_it_works">Comment ça fonctionne</string>
|
<string name="how_it_works">Comment ça fonctionne</string>
|
||||||
<string name="how_simplex_works">Comment <xliff:g id="appName">SimpleX</xliff:g> fonctionne</string>
|
<string name="how_simplex_works">Comment <xliff:g id="appName">SimpleX</xliff:g> fonctionne</string>
|
||||||
<string name="many_people_asked_how_can_it_deliver">Beaucoup se demande : <i>si <xliff:g id="appName">SimpleX</xliff:g> n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?</i></string>
|
<string name="many_people_asked_how_can_it_deliver">Beaucoup se demandent : <i>si <xliff:g id="appName">SimpleX</xliff:g> n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?</i></string>
|
||||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un <b>chiffrement de bout en bout à deux couches</b>.</string>
|
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un <b>chiffrement de bout en bout à deux couches</b>.</string>
|
||||||
<string name="read_more_in_github_with_link">Pour en savoir plus, consultez notre <font color="#0088ff">GitHub repository</font>.</string>
|
<string name="read_more_in_github_with_link">Pour en savoir plus, consultez notre <font color="#0088ff">GitHub repository</font>.</string>
|
||||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Batterie peu utilisée</b>. Le service de fond vérifie les nouveaux messages toutes les 10 minutes. Vous risquez de manquer des appels et des messages urgents.</string>
|
<string name="onboarding_notifications_mode_periodic_desc"><b>Batterie peu utilisée</b>. Le service de fond vérifie les nouveaux messages toutes les 10 minutes. Vous risquez de manquer des appels et des messages urgents.</string>
|
||||||
@@ -895,7 +895,7 @@
|
|||||||
<string name="v4_4_live_messages_desc">Les destinataires voient les mises à jour au fur et à mesure que vous les tapez.</string>
|
<string name="v4_4_live_messages_desc">Les destinataires voient les mises à jour au fur et à mesure que vous les tapez.</string>
|
||||||
<string name="v4_4_verify_connection_security">Vérifier la sécurité de la connexion</string>
|
<string name="v4_4_verify_connection_security">Vérifier la sécurité de la connexion</string>
|
||||||
<string name="v4_4_verify_connection_security_desc">Comparez les codes de sécurité avec vos contacts.</string>
|
<string name="v4_4_verify_connection_security_desc">Comparez les codes de sécurité avec vos contacts.</string>
|
||||||
<string name="new_in_version">Nouveau en %s</string>
|
<string name="new_in_version">Nouveautés de la %s</string>
|
||||||
<string name="v4_2_security_assessment">Évaluation de sécurité</string>
|
<string name="v4_2_security_assessment">Évaluation de sécurité</string>
|
||||||
<string name="v4_2_group_links">Liens de groupe</string>
|
<string name="v4_2_group_links">Liens de groupe</string>
|
||||||
<string name="v4_2_auto_accept_contact_requests_desc">Avec message de bienvenue facultatif.</string>
|
<string name="v4_2_auto_accept_contact_requests_desc">Avec message de bienvenue facultatif.</string>
|
||||||
|
|||||||
@@ -271,6 +271,7 @@
|
|||||||
<string name="live_message">Live message!</string>
|
<string name="live_message">Live message!</string>
|
||||||
<string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
|
<string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
|
||||||
<string name="send_verb">Send</string>
|
<string name="send_verb">Send</string>
|
||||||
|
<string name="icon_descr_cancel_live_message">Cancel live message</string>
|
||||||
|
|
||||||
<!-- General Actions / Responses -->
|
<!-- General Actions / Responses -->
|
||||||
<string name="back">Back</string>
|
<string name="back">Back</string>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ struct ContentView: View {
|
|||||||
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
||||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = true
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
||||||
@State private var showWhatsNew = false
|
@State private var showWhatsNew = false
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ final class ChatModel: ObservableObject {
|
|||||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||||
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||||
let ci = reversedChatItems[i]
|
let ci = reversedChatItems[i]
|
||||||
withAnimation(.default) {
|
withAnimation {
|
||||||
self.reversedChatItems[i] = cItem
|
self.reversedChatItems[i] = cItem
|
||||||
self.reversedChatItems[i].viewTimestamp = .now
|
self.reversedChatItems[i].viewTimestamp = .now
|
||||||
// on some occasions the confirmation of message being accepted by the server (tick)
|
// on some occasions the confirmation of message being accepted by the server (tick)
|
||||||
@@ -230,9 +230,18 @@ final class ChatModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
withAnimation { reversedChatItems.insert(cItem, at: 0) }
|
withAnimation(itemAnimation()) {
|
||||||
|
reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func itemAnimation() -> Animation? {
|
||||||
|
switch cItem.chatDir {
|
||||||
|
case .directSnd, .groupSnd: return cItem.meta.isLive ? nil : .default
|
||||||
|
default: return .default
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||||
@@ -274,6 +283,28 @@ final class ChatModel: ObservableObject {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
|
||||||
|
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||||
|
withAnimation {
|
||||||
|
reversedChatItems.insert(cItem, at: 0)
|
||||||
|
}
|
||||||
|
return cItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeLiveDummy(animated: Bool = true) {
|
||||||
|
if hasLiveDummy {
|
||||||
|
if animated {
|
||||||
|
withAnimation { _ = reversedChatItems.removeFirst() }
|
||||||
|
} else {
|
||||||
|
_ = reversedChatItems.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasLiveDummy: Bool {
|
||||||
|
reversedChatItems.first?.isLiveDummy == true
|
||||||
|
}
|
||||||
|
|
||||||
func markChatItemsRead(_ cInfo: ChatInfo) {
|
func markChatItemsRead(_ cInfo: ChatInfo) {
|
||||||
// update preview
|
// update preview
|
||||||
_updateChat(cInfo.id) { chat in
|
_updateChat(cInfo.id) { chat in
|
||||||
|
|||||||
@@ -49,8 +49,9 @@ func saveAnimImage(_ image: UIImage) -> String? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveImage(_ uiImage: UIImage) -> String? {
|
func saveImage(_ uiImage: UIImage) -> String? {
|
||||||
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE) {
|
let hasAlpha = imageHasAlpha(uiImage)
|
||||||
let ext = imageHasAlpha(uiImage) ? "png" : "jpg"
|
let ext = hasAlpha ? "png" : "jpg"
|
||||||
|
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE, hasAlpha: hasAlpha) {
|
||||||
let fileName = generateNewFileName("IMG", ext)
|
let fileName = generateNewFileName("IMG", ext)
|
||||||
return saveFile(imageDataResized, fileName)
|
return saveFile(imageDataResized, fileName)
|
||||||
}
|
}
|
||||||
@@ -67,19 +68,18 @@ func cropToSquare(_ image: UIImage) -> UIImage {
|
|||||||
} else if size.height > side {
|
} else if size.height > side {
|
||||||
origin.y -= (size.height - side) / 2
|
origin.y -= (size.height - side) / 2
|
||||||
}
|
}
|
||||||
return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size))
|
return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size), hasAlpha: imageHasAlpha(image))
|
||||||
}
|
}
|
||||||
|
|
||||||
func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? {
|
func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? {
|
||||||
var img = image
|
var img = image
|
||||||
let usePng = imageHasAlpha(image)
|
var data = hasAlpha ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
||||||
var data = usePng ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
|
||||||
var dataSize = data?.count ?? 0
|
var dataSize = data?.count ?? 0
|
||||||
while dataSize != 0 && dataSize > maxDataSize {
|
while dataSize != 0 && dataSize > maxDataSize {
|
||||||
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
||||||
let clippedRatio = min(ratio, 2.0)
|
let clippedRatio = min(ratio, 2.0)
|
||||||
img = reduceSize(img, ratio: clippedRatio)
|
img = reduceSize(img, ratio: clippedRatio, hasAlpha: hasAlpha)
|
||||||
data = usePng ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
data = hasAlpha ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
||||||
dataSize = data?.count ?? 0
|
dataSize = data?.count ?? 0
|
||||||
}
|
}
|
||||||
logger.debug("resizeImageToDataSize final \(dataSize)")
|
logger.debug("resizeImageToDataSize final \(dataSize)")
|
||||||
@@ -88,45 +88,61 @@ func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? {
|
|||||||
|
|
||||||
func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
|
func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
|
||||||
var img = image
|
var img = image
|
||||||
var str = compressImageStr(img)
|
let hasAlpha = imageHasAlpha(image)
|
||||||
|
var str = compressImageStr(img, hasAlpha: hasAlpha)
|
||||||
var dataSize = str?.count ?? 0
|
var dataSize = str?.count ?? 0
|
||||||
while dataSize != 0 && dataSize > maxDataSize {
|
while dataSize != 0 && dataSize > maxDataSize {
|
||||||
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
||||||
let clippedRatio = min(ratio, 2.0)
|
let clippedRatio = min(ratio, 2.0)
|
||||||
img = reduceSize(img, ratio: clippedRatio)
|
img = reduceSize(img, ratio: clippedRatio, hasAlpha: hasAlpha)
|
||||||
str = compressImageStr(img)
|
str = compressImageStr(img, hasAlpha: hasAlpha)
|
||||||
dataSize = str?.count ?? 0
|
dataSize = str?.count ?? 0
|
||||||
}
|
}
|
||||||
logger.debug("resizeImageToStrSize final \(dataSize)")
|
logger.debug("resizeImageToStrSize final \(dataSize)")
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85) -> String? {
|
func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? {
|
||||||
let ext = imageHasAlpha(image) ? "png" : "jpg"
|
let ext = hasAlpha ? "png" : "jpg"
|
||||||
if let data = imageHasAlpha(image) ? image.pngData() : image.jpegData(compressionQuality: compressionQuality) {
|
if let data = hasAlpha ? image.pngData() : image.jpegData(compressionQuality: compressionQuality) {
|
||||||
return "data:image/\(ext);base64,\(data.base64EncodedString())"
|
return "data:image/\(ext);base64,\(data.base64EncodedString())"
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reduceSize(_ image: UIImage, ratio: CGFloat) -> UIImage {
|
private func reduceSize(_ image: UIImage, ratio: CGFloat, hasAlpha: Bool) -> UIImage {
|
||||||
let newSize = CGSize(width: floor(image.size.width / ratio), height: floor(image.size.height / ratio))
|
let newSize = CGSize(width: floor(image.size.width / ratio), height: floor(image.size.height / ratio))
|
||||||
let bounds = CGRect(origin: .zero, size: newSize)
|
let bounds = CGRect(origin: .zero, size: newSize)
|
||||||
return resizeImage(image, newBounds: bounds, drawIn: bounds)
|
return resizeImage(image, newBounds: bounds, drawIn: bounds, hasAlpha: hasAlpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect) -> UIImage {
|
private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, hasAlpha: Bool) -> UIImage {
|
||||||
let format = UIGraphicsImageRendererFormat()
|
let format = UIGraphicsImageRendererFormat()
|
||||||
format.scale = 1.0
|
format.scale = 1.0
|
||||||
format.opaque = !imageHasAlpha(image)
|
format.opaque = !hasAlpha
|
||||||
return UIGraphicsImageRenderer(bounds: newBounds, format: format).image { _ in
|
return UIGraphicsImageRenderer(bounds: newBounds, format: format).image { _ in
|
||||||
image.draw(in: drawIn)
|
image.draw(in: drawIn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageHasAlpha(_ img: UIImage) -> Bool {
|
func imageHasAlpha(_ img: UIImage) -> Bool {
|
||||||
let alpha = img.cgImage?.alphaInfo
|
if let cgImage = img.cgImage {
|
||||||
return alpha == .first || alpha == .last || alpha == .premultipliedFirst || alpha == .premultipliedLast || alpha == .alphaOnly
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
|
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
|
||||||
|
if let context = CGContext(data: nil, width: cgImage.width, height: cgImage.height, bitsPerComponent: 8, bytesPerRow: cgImage.width * 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) {
|
||||||
|
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
|
||||||
|
if let data = context.data {
|
||||||
|
let data = data.assumingMemoryBound(to: UInt8.self)
|
||||||
|
let size = cgImage.width * cgImage.height
|
||||||
|
var i = 0
|
||||||
|
while i < size {
|
||||||
|
if data[i] < 255 { return true }
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFileFromURL(_ url: URL) -> String? {
|
func saveFileFromURL(_ url: URL) -> String? {
|
||||||
|
|||||||
@@ -442,9 +442,13 @@ struct ChatView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||||
|
let uiMenu: Binding<UIMenu> = Binding(
|
||||||
|
get: { UIMenu(title: "", children: menu(live: composeState.liveMessage != nil)) },
|
||||||
|
set: { _ in }
|
||||||
|
)
|
||||||
|
|
||||||
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed)
|
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed)
|
||||||
.uiKitContextMenu(actions: menu())
|
.uiKitContextMenu(menu: uiMenu)
|
||||||
.confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) {
|
.confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) {
|
||||||
Button("Delete for me", role: .destructive) {
|
Button("Delete for me", role: .destructive) {
|
||||||
deleteMessage(.cidmInternal)
|
deleteMessage(.cidmInternal)
|
||||||
@@ -459,10 +463,10 @@ struct ChatView: View {
|
|||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func menu() -> [UIAction] {
|
private func menu(live: Bool) -> [UIAction] {
|
||||||
var menu: [UIAction] = []
|
var menu: [UIAction] = []
|
||||||
if let mc = ci.content.msgContent, !ci.meta.itemDeleted || revealed {
|
if let mc = ci.content.msgContent, !ci.meta.itemDeleted || revealed {
|
||||||
if !ci.meta.itemDeleted {
|
if !ci.meta.itemDeleted && !ci.isLiveDummy && !live {
|
||||||
menu.append(replyUIAction())
|
menu.append(replyUIAction())
|
||||||
}
|
}
|
||||||
menu.append(shareUIAction())
|
menu.append(shareUIAction())
|
||||||
@@ -478,13 +482,15 @@ struct ChatView: View {
|
|||||||
menu.append(saveFileAction(filePath))
|
menu.append(saveFileAction(filePath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ci.meta.editable && !mc.isVoice {
|
if ci.meta.editable && !mc.isVoice && !live {
|
||||||
menu.append(editAction())
|
menu.append(editAction())
|
||||||
}
|
}
|
||||||
if revealed {
|
if revealed {
|
||||||
menu.append(hideUIAction())
|
menu.append(hideUIAction())
|
||||||
}
|
}
|
||||||
menu.append(deleteUIAction())
|
if !live || !ci.meta.isLive {
|
||||||
|
menu.append(deleteUIAction())
|
||||||
|
}
|
||||||
} else if ci.meta.itemDeleted {
|
} else if ci.meta.itemDeleted {
|
||||||
menu.append(revealUIAction())
|
menu.append(revealUIAction())
|
||||||
menu.append(deleteUIAction())
|
menu.append(deleteUIAction())
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ enum VoiceMessageRecordingState {
|
|||||||
struct LiveMessage {
|
struct LiveMessage {
|
||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
var typedMsg: String
|
var typedMsg: String
|
||||||
var sentMsg: String
|
var sentMsg: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ComposeState {
|
struct ComposeState {
|
||||||
@@ -96,6 +96,13 @@ struct ComposeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var quoting: Bool {
|
||||||
|
switch contextItem {
|
||||||
|
case .quotedItem: return true
|
||||||
|
default: return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var sendEnabled: Bool {
|
var sendEnabled: Bool {
|
||||||
switch preview {
|
switch preview {
|
||||||
case .imagePreviews: return true
|
case .imagePreviews: return true
|
||||||
@@ -105,6 +112,10 @@ struct ComposeState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var endLiveDisabled: Bool {
|
||||||
|
liveMessage != nil && message.isEmpty && noPreview && !quoting
|
||||||
|
}
|
||||||
|
|
||||||
var linkPreviewAllowed: Bool {
|
var linkPreviewAllowed: Bool {
|
||||||
switch preview {
|
switch preview {
|
||||||
case .imagePreviews: return false
|
case .imagePreviews: return false
|
||||||
@@ -232,9 +243,9 @@ struct ComposeView: View {
|
|||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
contextItemView()
|
contextItemView()
|
||||||
switch (composeState.editing, composeState.preview) {
|
switch (composeState.editing, composeState.preview) {
|
||||||
case (true, .filePreview): EmptyView()
|
case (true, .filePreview): EmptyView()
|
||||||
case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed
|
case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed
|
||||||
default: previewView()
|
default: previewView()
|
||||||
}
|
}
|
||||||
HStack (alignment: .bottom) {
|
HStack (alignment: .bottom) {
|
||||||
Button {
|
Button {
|
||||||
@@ -255,6 +266,10 @@ struct ComposeView: View {
|
|||||||
},
|
},
|
||||||
sendLiveMessage: sendLiveMessage,
|
sendLiveMessage: sendLiveMessage,
|
||||||
updateLiveMessage: updateLiveMessage,
|
updateLiveMessage: updateLiveMessage,
|
||||||
|
cancelLiveMessage: {
|
||||||
|
composeState.liveMessage = nil
|
||||||
|
chatModel.removeLiveDummy()
|
||||||
|
},
|
||||||
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
||||||
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
||||||
startVoiceMessageRecording: {
|
startVoiceMessageRecording: {
|
||||||
@@ -371,10 +386,11 @@ struct ComposeView: View {
|
|||||||
if let fileName = composeState.voiceMessageRecordingFileName {
|
if let fileName = composeState.voiceMessageRecordingFileName {
|
||||||
cancelVoiceMessageRecording(fileName)
|
cancelVoiceMessageRecording(fileName)
|
||||||
}
|
}
|
||||||
if composeState.liveMessage != nil {
|
if composeState.liveMessage != nil && (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
resetLinkPreview()
|
resetLinkPreview()
|
||||||
}
|
}
|
||||||
|
chatModel.removeLiveDummy(animated: false)
|
||||||
}
|
}
|
||||||
.onChange(of: chatModel.stopPreviousRecPlay) { _ in
|
.onChange(of: chatModel.stopPreviousRecPlay) { _ in
|
||||||
if !startingRecording {
|
if !startingRecording {
|
||||||
@@ -395,11 +411,17 @@ struct ComposeView: View {
|
|||||||
|
|
||||||
private func sendLiveMessage() async {
|
private func sendLiveMessage() async {
|
||||||
let typedMsg = composeState.message
|
let typedMsg = composeState.message
|
||||||
let sentMsg = truncateToWords(typedMsg)
|
let lm = composeState.liveMessage
|
||||||
if composeState.liveMessage == nil,
|
if (composeState.sendEnabled || composeState.quoting)
|
||||||
let ci = await sendMessageAsync(sentMsg, live: true) {
|
&& (lm == nil || lm?.sentMsg == nil),
|
||||||
|
let ci = await sendMessageAsync(typedMsg, live: true) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: sentMsg))
|
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: typedMsg))
|
||||||
|
}
|
||||||
|
} else if lm == nil {
|
||||||
|
let cItem = chatModel.addLiveDummy(chat.chatInfo)
|
||||||
|
await MainActor.run {
|
||||||
|
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: cItem, typedMsg: typedMsg, sentMsg: nil))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,7 +446,7 @@ struct ComposeView: View {
|
|||||||
|
|
||||||
private func liveMessageToSend(_ lm: LiveMessage, _ t: String) -> String? {
|
private func liveMessageToSend(_ lm: LiveMessage, _ t: String) -> String? {
|
||||||
let s = t != lm.typedMsg ? truncateToWords(t) : t
|
let s = t != lm.typedMsg ? truncateToWords(t) : t
|
||||||
return s != lm.sentMsg ? s : nil
|
return s != lm.sentMsg && (lm.sentMsg != nil || !s.isEmpty) ? s : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func truncateToWords(_ s: String) -> String {
|
private func truncateToWords(_ s: String) -> String {
|
||||||
@@ -512,7 +534,7 @@ struct ComposeView: View {
|
|||||||
}
|
}
|
||||||
if case let .editingItem(ci) = composeState.contextItem {
|
if case let .editingItem(ci) = composeState.contextItem {
|
||||||
sent = await updateMessage(ci, live: live)
|
sent = await updateMessage(ci, live: live)
|
||||||
} else if let liveMessage = liveMessage {
|
} else if let liveMessage = liveMessage, liveMessage.sentMsg != nil {
|
||||||
sent = await updateMessage(liveMessage.chatItem, live: live)
|
sent = await updateMessage(liveMessage.chatItem, live: live)
|
||||||
} else {
|
} else {
|
||||||
var quoted: Int64? = nil
|
var quoted: Int64? = nil
|
||||||
@@ -609,6 +631,7 @@ struct ComposeView: View {
|
|||||||
live: live
|
live: live
|
||||||
) {
|
) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
chatModel.removeLiveDummy(animated: false)
|
||||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||||
}
|
}
|
||||||
return chatItem
|
return chatItem
|
||||||
|
|||||||
@@ -9,11 +9,14 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
|
||||||
|
private let liveMsgInterval: UInt64 = 3000_000000
|
||||||
|
|
||||||
struct SendMessageView: View {
|
struct SendMessageView: View {
|
||||||
@Binding var composeState: ComposeState
|
@Binding var composeState: ComposeState
|
||||||
var sendMessage: () -> Void
|
var sendMessage: () -> Void
|
||||||
var sendLiveMessage: (() async -> Void)? = nil
|
var sendLiveMessage: (() async -> Void)? = nil
|
||||||
var updateLiveMessage: (() async -> Void)? = nil
|
var updateLiveMessage: (() async -> Void)? = nil
|
||||||
|
var cancelLiveMessage: (() -> Void)? = nil
|
||||||
var showVoiceMessageButton: Bool = true
|
var showVoiceMessageButton: Bool = true
|
||||||
var voiceMessageAllowed: Bool = true
|
var voiceMessageAllowed: Bool = true
|
||||||
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
|
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
|
||||||
@@ -97,12 +100,18 @@ struct SendMessageView: View {
|
|||||||
} else {
|
} else {
|
||||||
voiceMessageNotAllowedButton()
|
voiceMessageNotAllowedButton()
|
||||||
}
|
}
|
||||||
if let send = sendLiveMessage, let update = updateLiveMessage {
|
if let send = sendLiveMessage,
|
||||||
|
let update = updateLiveMessage,
|
||||||
|
case .noContextItem = composeState.contextItem {
|
||||||
startLiveMessageButton(send: send, update: update)
|
startLiveMessageButton(send: send, update: update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if vmrs == .recording && !holdingVMR {
|
} else if vmrs == .recording && !holdingVMR {
|
||||||
finishVoiceMessageRecordingButton()
|
finishVoiceMessageRecordingButton()
|
||||||
|
} else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty {
|
||||||
|
cancelLiveMessageButton {
|
||||||
|
cancelLiveMessage?()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sendMessageButton()
|
sendMessageButton()
|
||||||
}
|
}
|
||||||
@@ -129,11 +138,13 @@ struct SendMessageView: View {
|
|||||||
.disabled(
|
.disabled(
|
||||||
!composeState.sendEnabled ||
|
!composeState.sendEnabled ||
|
||||||
composeState.disabled ||
|
composeState.disabled ||
|
||||||
(!voiceMessageAllowed && composeState.voicePreview)
|
(!voiceMessageAllowed && composeState.voicePreview) ||
|
||||||
|
composeState.endLiveDisabled
|
||||||
)
|
)
|
||||||
.frame(width: 29, height: 29)
|
.frame(width: 29, height: 29)
|
||||||
|
|
||||||
if composeState.liveMessage == nil,
|
if composeState.liveMessage == nil,
|
||||||
|
case .noContextItem = composeState.contextItem,
|
||||||
!composeState.voicePreview && !composeState.editing,
|
!composeState.voicePreview && !composeState.editing,
|
||||||
let send = sendLiveMessage,
|
let send = sendLiveMessage,
|
||||||
let update = updateLiveMessage {
|
let update = updateLiveMessage {
|
||||||
@@ -220,6 +231,20 @@ struct SendMessageView: View {
|
|||||||
.padding([.bottom, .trailing], 4)
|
.padding([.bottom, .trailing], 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func cancelLiveMessageButton(cancel: @escaping () -> Void) -> some View {
|
||||||
|
return Button {
|
||||||
|
cancel()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "multiply")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.frame(width: 15, height: 15)
|
||||||
|
}
|
||||||
|
.frame(width: 29, height: 29)
|
||||||
|
.padding([.bottom, .horizontal], 4)
|
||||||
|
}
|
||||||
|
|
||||||
private func startLiveMessageButton(send: @escaping () async -> Void, update: @escaping () async -> Void) -> some View {
|
private func startLiveMessageButton(send: @escaping () async -> Void, update: @escaping () async -> Void) -> some View {
|
||||||
return Button {
|
return Button {
|
||||||
switch composeState.preview {
|
switch composeState.preview {
|
||||||
@@ -271,9 +296,12 @@ struct SendMessageView: View {
|
|||||||
sendButtonOpacity = 1
|
sendButtonOpacity = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { t in
|
Task {
|
||||||
if composeState.liveMessage == nil { t.invalidate() }
|
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||||
Task { await update() }
|
while composeState.liveMessage != nil {
|
||||||
|
await update()
|
||||||
|
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ struct ChatListView: View {
|
|||||||
destination: chatView(),
|
destination: chatView(),
|
||||||
isActive: Binding(
|
isActive: Binding(
|
||||||
get: { chatModel.chatId != nil },
|
get: { chatModel.chatId != nil },
|
||||||
set: { _, _ in chatModel.chatId = nil }
|
set: { _ in }
|
||||||
)
|
)
|
||||||
) { EmptyView() }
|
) { EmptyView() }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ private struct SheetForItem<T, C>: ViewModifier where T: Identifiable, C: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct PrivacySensitive: ViewModifier {
|
private struct PrivacySensitive: ViewModifier {
|
||||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = true
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
@Environment(\.scenePhase) var scenePhase
|
@Environment(\.scenePhase) var scenePhase
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import UIKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension View {
|
extension View {
|
||||||
func uiKitContextMenu(title: String = "", actions: [UIAction]) -> some View {
|
func uiKitContextMenu(menu: Binding<UIMenu>) -> some View {
|
||||||
self.overlay(Color(uiColor: .systemBackground))
|
self.overlay(Color(uiColor: .systemBackground))
|
||||||
.overlay(
|
.overlay(
|
||||||
InteractionView(content: self, menu: UIMenu(title: title, children: actions))
|
InteractionView(content: self, menu: menu)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ private struct InteractionConfig<Content: View> {
|
|||||||
|
|
||||||
private struct InteractionView<Content: View>: UIViewRepresentable {
|
private struct InteractionView<Content: View>: UIViewRepresentable {
|
||||||
let content: Content
|
let content: Content
|
||||||
let menu: UIMenu
|
@Binding var menu: UIMenu
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIView {
|
func makeUIView(context: Context) -> UIView {
|
||||||
let view = UIView()
|
let view = UIView()
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ struct PreferencesView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View {
|
private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding<FeatureAllowed>) -> some View {
|
||||||
Text(ChatFeature.timedMessages.allowDescription(allowFeature.wrappedValue))
|
Text(feature.allowDescription(allowFeature.wrappedValue))
|
||||||
.frame(height: 36, alignment: .topLeading)
|
.frame(height: 36, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ struct PrivacySettings: View {
|
|||||||
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
||||||
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
@AppStorage(GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE, store: groupDefaults) private var transferImagesInline = false
|
@AppStorage(GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE, store: groupDefaults) private var transferImagesInline = true
|
||||||
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
||||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = true
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
|||||||
@@ -144,12 +144,12 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lldd" xml:space="preserve">
|
<trans-unit id="%lldd" xml:space="preserve">
|
||||||
<source>%lldd</source>
|
<source>%lldd</source>
|
||||||
<target>%lldd</target>
|
<target>%lldT</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lldh" xml:space="preserve">
|
<trans-unit id="%lldh" xml:space="preserve">
|
||||||
<source>%lldh</source>
|
<source>%lldh</source>
|
||||||
<target>%lldh</target>
|
<target>%lldH</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lldk" xml:space="preserve">
|
<trans-unit id="%lldk" xml:space="preserve">
|
||||||
@@ -159,22 +159,22 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lldm" xml:space="preserve">
|
<trans-unit id="%lldm" xml:space="preserve">
|
||||||
<source>%lldm</source>
|
<source>%lldm</source>
|
||||||
<target>%lldm</target>
|
<target>%lldmin</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lldmth" xml:space="preserve">
|
<trans-unit id="%lldmth" xml:space="preserve">
|
||||||
<source>%lldmth</source>
|
<source>%lldmth</source>
|
||||||
<target>%lldmth</target>
|
<target>%lldMon</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%llds" xml:space="preserve">
|
<trans-unit id="%llds" xml:space="preserve">
|
||||||
<source>%llds</source>
|
<source>%llds</source>
|
||||||
<target>%llds</target>
|
<target>%lldsek</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="%lldw" xml:space="preserve">
|
<trans-unit id="%lldw" xml:space="preserve">
|
||||||
<source>%lldw</source>
|
<source>%lldw</source>
|
||||||
<target>%lldw</target>
|
<target>%lldW</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="(" xml:space="preserve">
|
<trans-unit id="(" xml:space="preserve">
|
||||||
@@ -405,12 +405,12 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve">
|
<trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve">
|
||||||
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
|
<source>Allow irreversible message deletion only if your contact allows it to you.</source>
|
||||||
<target>Erlauben Sie das unwiederbringliche löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</target>
|
<target>Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Allow sending direct messages to members." xml:space="preserve">
|
<trans-unit id="Allow sending direct messages to members." xml:space="preserve">
|
||||||
<source>Allow sending direct messages to members.</source>
|
<source>Allow sending direct messages to members.</source>
|
||||||
<target>Erlauben Sie das Senden von Direktnachrichten an Mitglieder</target>
|
<target>Das Senden von Direktnachrichten an Gruppenmitglieder erlauben.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Allow sending disappearing messages." xml:space="preserve">
|
<trans-unit id="Allow sending disappearing messages." xml:space="preserve">
|
||||||
@@ -420,12 +420,12 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
|
<trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve">
|
||||||
<source>Allow to irreversibly delete sent messages.</source>
|
<source>Allow to irreversibly delete sent messages.</source>
|
||||||
<target>Unwiederbringliches Löschen von gesendeten Nachrichten erlauben.</target>
|
<target>Unwiederbringliches löschen von gesendeten Nachrichten erlauben.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Allow to send voice messages." xml:space="preserve">
|
<trans-unit id="Allow to send voice messages." xml:space="preserve">
|
||||||
<source>Allow to send voice messages.</source>
|
<source>Allow to send voice messages.</source>
|
||||||
<target>Senden von Sprachnachrichten erlauben.</target>
|
<target>Das Senden von Sprachnachrichten erlauben.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve">
|
<trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve">
|
||||||
@@ -670,7 +670,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Clear verification" xml:space="preserve">
|
<trans-unit id="Clear verification" xml:space="preserve">
|
||||||
<source>Clear verification</source>
|
<source>Clear verification</source>
|
||||||
<target>Überprüfung erfolgt</target>
|
<target>Überprüfung zurücknehmen</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Colors" xml:space="preserve">
|
<trans-unit id="Colors" xml:space="preserve">
|
||||||
@@ -1158,7 +1158,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited in this group.</source>
|
||||||
<target>In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht möglich.</target>
|
<target>In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||||
@@ -1433,7 +1433,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Error saving SMP servers" xml:space="preserve">
|
<trans-unit id="Error saving SMP servers" xml:space="preserve">
|
||||||
<source>Error saving SMP servers</source>
|
<source>Error saving SMP servers</source>
|
||||||
<target>Fehler beim Speichern der SMP Server</target>
|
<target>Fehler beim Speichern der SMP-Server</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Error saving group profile" xml:space="preserve">
|
<trans-unit id="Error saving group profile" xml:space="preserve">
|
||||||
@@ -1613,7 +1613,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Group members can send voice messages.</source>
|
||||||
<target>Gruppenmitglieder können Sprachnachrichten senden.</target>
|
<target>Gruppenmitglieder können Sprachnachrichten versenden.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group message:" xml:space="preserve">
|
<trans-unit id="Group message:" xml:space="preserve">
|
||||||
@@ -1633,7 +1633,7 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve">
|
<trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve">
|
||||||
<source>Group profile is stored on members' devices, not on the servers.</source>
|
<source>Group profile is stored on members' devices, not on the servers.</source>
|
||||||
<target>Das Gruppenprofil wird nur auf den Mitglieds-Systemen gespeichtert und nicht auf den Servern.</target>
|
<target>Das Gruppenprofil wird nur auf den Mitglieds-Systemen gespeichert und nicht auf den Servern.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve">
|
<trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve">
|
||||||
@@ -1840,17 +1840,17 @@
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion" xml:space="preserve">
|
<trans-unit id="Irreversible message deletion" xml:space="preserve">
|
||||||
<source>Irreversible message deletion</source>
|
<source>Irreversible message deletion</source>
|
||||||
<target>Unwiederbringliches Löschen einer Nachricht</target>
|
<target>Unwiederbringliches löschen einer Nachricht</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||||
<target>In diesem Chat ist das unwiederbringliche Löschen von Nachrichten untersagt.</target>
|
<target>In diesem Chat ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited in this group.</source>
|
||||||
<target>In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten verboten.</target>
|
<target>In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||||
@@ -1965,7 +1965,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve">
|
<trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve">
|
||||||
<source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source>
|
<source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source>
|
||||||
<target>Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</target>
|
<target>Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve">
|
<trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve">
|
||||||
@@ -2230,7 +2230,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only you can send voice messages." xml:space="preserve">
|
<trans-unit id="Only you can send voice messages." xml:space="preserve">
|
||||||
<source>Only you can send voice messages.</source>
|
<source>Only you can send voice messages.</source>
|
||||||
<target>Nur Sie können Sprachnachrichten senden.</target>
|
<target>Nur Sie können Sprachnachrichten versenden.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
|
<trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve">
|
||||||
@@ -2245,7 +2245,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only your contact can send voice messages." xml:space="preserve">
|
<trans-unit id="Only your contact can send voice messages." xml:space="preserve">
|
||||||
<source>Only your contact can send voice messages.</source>
|
<source>Only your contact can send voice messages.</source>
|
||||||
<target>Nur Ihr Kontakt kann Sprachnachrichten senden.</target>
|
<target>Nur Ihr Kontakt kann Sprachnachrichten versenden.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open Settings" xml:space="preserve">
|
<trans-unit id="Open Settings" xml:space="preserve">
|
||||||
@@ -2385,12 +2385,12 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Prohibit irreversible message deletion." xml:space="preserve">
|
<trans-unit id="Prohibit irreversible message deletion." xml:space="preserve">
|
||||||
<source>Prohibit irreversible message deletion.</source>
|
<source>Prohibit irreversible message deletion.</source>
|
||||||
<target>Unwiederbringliches Löschen von Nachrichten verbieten.</target>
|
<target>Unwiederbringliches löschen von Nachrichten nicht erlauben.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
<trans-unit id="Prohibit sending direct messages to members." xml:space="preserve">
|
||||||
<source>Prohibit sending direct messages to members.</source>
|
<source>Prohibit sending direct messages to members.</source>
|
||||||
<target>Verbieten Sie das Senden von Direktnachrichten an Mitglieder</target>
|
<target>Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Prohibit sending disappearing messages." xml:space="preserve">
|
<trans-unit id="Prohibit sending disappearing messages." xml:space="preserve">
|
||||||
@@ -2400,7 +2400,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
|
<trans-unit id="Prohibit sending voice messages." xml:space="preserve">
|
||||||
<source>Prohibit sending voice messages.</source>
|
<source>Prohibit sending voice messages.</source>
|
||||||
<target>Senden von Sprachnachrichten untersagen.</target>
|
<target>Das Senden von Sprachnachrichten nicht erlauben.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Protect app screen" xml:space="preserve">
|
<trans-unit id="Protect app screen" xml:space="preserve">
|
||||||
@@ -2625,7 +2625,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Save servers" xml:space="preserve">
|
<trans-unit id="Save servers" xml:space="preserve">
|
||||||
<source>Save servers</source>
|
<source>Save servers</source>
|
||||||
<target>Server speichern</target>
|
<target>Alle Server speichern</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve">
|
<trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve">
|
||||||
@@ -2745,7 +2745,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
|
<trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve">
|
||||||
<source>Server requires authorization to create queues, check password</source>
|
<source>Server requires authorization to create queues, check password</source>
|
||||||
<target>Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort.</target>
|
<target>Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort</target>
|
||||||
<note>server test error</note>
|
<note>server test error</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Server test failed!" xml:space="preserve">
|
<trans-unit id="Server test failed!" xml:space="preserve">
|
||||||
@@ -3259,7 +3259,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Use server" xml:space="preserve">
|
<trans-unit id="Use server" xml:space="preserve">
|
||||||
<source>Use server</source>
|
<source>Use server</source>
|
||||||
<target>Benutze Server</target>
|
<target>Server nutzen</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve">
|
<trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve">
|
||||||
@@ -3304,17 +3304,17 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<target>In diesem Chat sind Sprachnachrichten untersagt.</target>
|
<target>In diesem Chat sind Sprachnachrichten nicht erlaubt.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited in this group.</source>
|
||||||
<target>In dieser Gruppe sind Sprachnachrichten untersagt.</target>
|
<target>In dieser Gruppe sind Sprachnachrichten nicht erlaubt.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
<source>Voice messages prohibited!</source>
|
<source>Voice messages prohibited!</source>
|
||||||
<target>Sprachnachrichten sind untersagt!</target>
|
<target>Sprachnachrichten sind nicht erlaubt!</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice message…" xml:space="preserve">
|
<trans-unit id="Voice message…" xml:space="preserve">
|
||||||
@@ -3746,6 +3746,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
<target>Anrufen…</target>
|
<target>Anrufen…</target>
|
||||||
<note>call status</note>
|
<note>call status</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="cancelled %@" xml:space="preserve">
|
||||||
|
<source>cancelled %@</source>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="changed address for you" xml:space="preserve">
|
<trans-unit id="changed address for you" xml:space="preserve">
|
||||||
<source>changed address for you</source>
|
<source>changed address for you</source>
|
||||||
<target>wechselte die Adresse für Sie</target>
|
<target>wechselte die Adresse für Sie</target>
|
||||||
@@ -4057,6 +4061,14 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
|||||||
<note>enabled status
|
<note>enabled status
|
||||||
group pref value</note>
|
group pref value</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@" xml:space="preserve">
|
||||||
|
<source>offered %@</source>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@: %@" xml:space="preserve">
|
||||||
|
<source>offered %1$@: %2$@</source>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="on" xml:space="preserve">
|
<trans-unit id="on" xml:space="preserve">
|
||||||
<source>on</source>
|
<source>on</source>
|
||||||
<target>Ein</target>
|
<target>Ein</target>
|
||||||
|
|||||||
@@ -3746,6 +3746,11 @@ SimpleX servers cannot see your profile.</target>
|
|||||||
<target>calling…</target>
|
<target>calling…</target>
|
||||||
<note>call status</note>
|
<note>call status</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="cancelled %@" xml:space="preserve">
|
||||||
|
<source>cancelled %@</source>
|
||||||
|
<target>cancelled %@</target>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="changed address for you" xml:space="preserve">
|
<trans-unit id="changed address for you" xml:space="preserve">
|
||||||
<source>changed address for you</source>
|
<source>changed address for you</source>
|
||||||
<target>changed address for you</target>
|
<target>changed address for you</target>
|
||||||
@@ -4057,6 +4062,16 @@ SimpleX servers cannot see your profile.</target>
|
|||||||
<note>enabled status
|
<note>enabled status
|
||||||
group pref value</note>
|
group pref value</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@" xml:space="preserve">
|
||||||
|
<source>offered %@</source>
|
||||||
|
<target>offered %@</target>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@: %@" xml:space="preserve">
|
||||||
|
<source>offered %1$@: %2$@</source>
|
||||||
|
<target>offered %1$@: %2$@</target>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="on" xml:space="preserve">
|
<trans-unit id="on" xml:space="preserve">
|
||||||
<source>on</source>
|
<source>on</source>
|
||||||
<target>on</target>
|
<target>on</target>
|
||||||
|
|||||||
@@ -1970,7 +1970,7 @@ Nous allons ajouter une redondance des serveurs pour éviter la perte de message
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve">
|
<trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve">
|
||||||
<source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source>
|
<source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source>
|
||||||
<target>Beaucoup de gens ont demandé : *si SimpleX n'a pas d'identifiants d'utilisateur, comment peut-il délivrer des messages?*</target>
|
<target>Beaucoup se demandent : *si SimpleX n'a pas d'identifiants d'utilisateur, comment peut-il délivrer des messages?*</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Mark deleted for everyone" xml:space="preserve">
|
<trans-unit id="Mark deleted for everyone" xml:space="preserve">
|
||||||
@@ -2100,7 +2100,7 @@ Nous allons ajouter une redondance des serveurs pour éviter la perte de message
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="New in %@" xml:space="preserve">
|
<trans-unit id="New in %@" xml:space="preserve">
|
||||||
<source>New in %@</source>
|
<source>New in %@</source>
|
||||||
<target>Nouveau en %@</target>
|
<target>Nouveautés de la %@</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="New member role" xml:space="preserve">
|
<trans-unit id="New member role" xml:space="preserve">
|
||||||
@@ -3085,7 +3085,7 @@ Nous allons ajouter une redondance des serveurs pour éviter la perte de message
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To ask any questions and to receive updates:" xml:space="preserve">
|
<trans-unit id="To ask any questions and to receive updates:" xml:space="preserve">
|
||||||
<source>To ask any questions and to receive updates:</source>
|
<source>To ask any questions and to receive updates:</source>
|
||||||
<target>Pour poser toute question et recevoir des mises à jour :</target>
|
<target>Si vous avez des questions et que vous souhaitez des réponses :</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</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">
|
<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">
|
||||||
@@ -3105,7 +3105,7 @@ Nous allons ajouter une redondance des serveurs pour éviter la perte de message
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
||||||
<target>Pour protéger la vie privée, au lieu des ID utilisateurs utilisés par toutes les autres plateformes, SimpleX a des ID pour les queues de messages, distinctes pour chacun de vos contacts.</target>
|
<target>Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve">
|
<trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve">
|
||||||
@@ -3444,7 +3444,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien
|
|||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
||||||
<target>Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** des messages de vos contacts.</target>
|
<target>Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** les messages de vos contacts.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
@@ -3746,6 +3746,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
|
|||||||
<target>appel…</target>
|
<target>appel…</target>
|
||||||
<note>call status</note>
|
<note>call status</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="cancelled %@" xml:space="preserve">
|
||||||
|
<source>cancelled %@</source>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="changed address for you" xml:space="preserve">
|
<trans-unit id="changed address for you" xml:space="preserve">
|
||||||
<source>changed address for you</source>
|
<source>changed address for you</source>
|
||||||
<target>adresse modifiée pour vous</target>
|
<target>adresse modifiée pour vous</target>
|
||||||
@@ -4057,6 +4061,14 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target>
|
|||||||
<note>enabled status
|
<note>enabled status
|
||||||
group pref value</note>
|
group pref value</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@" xml:space="preserve">
|
||||||
|
<source>offered %@</source>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@: %@" xml:space="preserve">
|
||||||
|
<source>offered %1$@: %2$@</source>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="on" xml:space="preserve">
|
<trans-unit id="on" xml:space="preserve">
|
||||||
<source>on</source>
|
<source>on</source>
|
||||||
<target>on</target>
|
<target>on</target>
|
||||||
|
|||||||
@@ -3746,6 +3746,11 @@ SimpleX серверы не могут получить доступ к ваше
|
|||||||
<target>входящий звонок…</target>
|
<target>входящий звонок…</target>
|
||||||
<note>call status</note>
|
<note>call status</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="cancelled %@" xml:space="preserve">
|
||||||
|
<source>cancelled %@</source>
|
||||||
|
<target>отменил(a) %@</target>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="changed address for you" xml:space="preserve">
|
<trans-unit id="changed address for you" xml:space="preserve">
|
||||||
<source>changed address for you</source>
|
<source>changed address for you</source>
|
||||||
<target>поменял(а) адрес для вас</target>
|
<target>поменял(а) адрес для вас</target>
|
||||||
@@ -4057,6 +4062,16 @@ SimpleX серверы не могут получить доступ к ваше
|
|||||||
<note>enabled status
|
<note>enabled status
|
||||||
group pref value</note>
|
group pref value</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@" xml:space="preserve">
|
||||||
|
<source>offered %@</source>
|
||||||
|
<target>предложил(a) %@</target>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="offered %@: %@" xml:space="preserve">
|
||||||
|
<source>offered %1$@: %2$@</source>
|
||||||
|
<target>предложил(a) %1$@: %2$@</target>
|
||||||
|
<note>feature offered item</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="on" xml:space="preserve">
|
<trans-unit id="on" xml:space="preserve">
|
||||||
<source>on</source>
|
<source>on</source>
|
||||||
<target>да</target>
|
<target>да</target>
|
||||||
|
|||||||
@@ -51,11 +51,6 @@
|
|||||||
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
|
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; };
|
||||||
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; };
|
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; };
|
||||||
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */; };
|
5C7031162953C97F00150A12 /* CIFeaturePreferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */; };
|
||||||
5C70311C2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C7031172955080900150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO-ghc8.10.7.a */; };
|
|
||||||
5C70311D2955080A00150A12 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C7031182955080900150A12 /* libgmp.a */; };
|
|
||||||
5C70311E2955080A00150A12 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C7031192955080A00150A12 /* libffi.a */; };
|
|
||||||
5C70311F2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C70311A2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a */; };
|
|
||||||
5C7031202955080A00150A12 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C70311B2955080A00150A12 /* libgmpxx.a */; };
|
|
||||||
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
|
5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; };
|
||||||
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; };
|
||||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
||||||
@@ -139,6 +134,11 @@
|
|||||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||||
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
||||||
|
6407BA8929704D910082BA18 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6407BA8429704D910082BA18 /* libgmpxx.a */; };
|
||||||
|
6407BA8A29704D910082BA18 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6407BA8529704D910082BA18 /* libgmp.a */; };
|
||||||
|
6407BA8B29704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6407BA8629704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x.a */; };
|
||||||
|
6407BA8C29704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6407BA8729704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x-ghc8.10.7.a */; };
|
||||||
|
6407BA8D29704D910082BA18 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6407BA8829704D910082BA18 /* libffi.a */; };
|
||||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||||
@@ -268,11 +268,6 @@
|
|||||||
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
|
5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = "<group>"; };
|
||||||
5C6BA666289BD954009B8ECC /* DismissSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissSheets.swift; sourceTree = "<group>"; };
|
5C6BA666289BD954009B8ECC /* DismissSheets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissSheets.swift; sourceTree = "<group>"; };
|
||||||
5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFeaturePreferenceView.swift; sourceTree = "<group>"; };
|
5C7031152953C97F00150A12 /* CIFeaturePreferenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFeaturePreferenceView.swift; sourceTree = "<group>"; };
|
||||||
5C7031172955080900150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO-ghc8.10.7.a"; sourceTree = "<group>"; };
|
|
||||||
5C7031182955080900150A12 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
|
||||||
5C7031192955080A00150A12 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
|
||||||
5C70311A2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a"; sourceTree = "<group>"; };
|
|
||||||
5C70311B2955080A00150A12 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
|
||||||
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = "<group>"; };
|
||||||
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
|
||||||
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
|
||||||
@@ -363,6 +358,11 @@
|
|||||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
||||||
|
6407BA8429704D910082BA18 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
|
6407BA8529704D910082BA18 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
|
6407BA8629704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x.a"; sourceTree = "<group>"; };
|
||||||
|
6407BA8729704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||||
|
6407BA8829704D910082BA18 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||||
@@ -420,13 +420,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5C70311F2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a in Frameworks */,
|
6407BA8929704D910082BA18 /* libgmpxx.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
|
6407BA8B29704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x.a in Frameworks */,
|
||||||
|
6407BA8A29704D910082BA18 /* libgmp.a in Frameworks */,
|
||||||
|
6407BA8D29704D910082BA18 /* libffi.a in Frameworks */,
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5C70311D2955080A00150A12 /* libgmp.a in Frameworks */,
|
6407BA8C29704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x-ghc8.10.7.a in Frameworks */,
|
||||||
5C70311E2955080A00150A12 /* libffi.a in Frameworks */,
|
|
||||||
5C70311C2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO-ghc8.10.7.a in Frameworks */,
|
|
||||||
5C7031202955080A00150A12 /* libgmpxx.a in Frameworks */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -484,11 +484,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C7031192955080A00150A12 /* libffi.a */,
|
6407BA8829704D910082BA18 /* libffi.a */,
|
||||||
5C7031182955080900150A12 /* libgmp.a */,
|
6407BA8529704D910082BA18 /* libgmp.a */,
|
||||||
5C70311B2955080A00150A12 /* libgmpxx.a */,
|
6407BA8429704D910082BA18 /* libgmpxx.a */,
|
||||||
5C7031172955080900150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO-ghc8.10.7.a */,
|
6407BA8729704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x-ghc8.10.7.a */,
|
||||||
5C70311A2955080A00150A12 /* libHSsimplex-chat-4.4.0-AHPp9UIBWT5C2IlT3cD6QO.a */,
|
6407BA8629704D910082BA18 /* libHSsimplex-chat-4.4.2-4Gu4VKBxHYB3XfShhkyX2x.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1305,7 +1305,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 107;
|
CURRENT_PROJECT_VERSION = 114;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1326,7 +1326,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 4.4;
|
MARKETING_VERSION = 4.4.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1347,7 +1347,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 107;
|
CURRENT_PROJECT_VERSION = 114;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -1368,7 +1368,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 4.4;
|
MARKETING_VERSION = 4.4.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||||
PRODUCT_NAME = SimpleX;
|
PRODUCT_NAME = SimpleX;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1426,7 +1426,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 107;
|
CURRENT_PROJECT_VERSION = 114;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1439,7 +1439,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 4.4;
|
MARKETING_VERSION = 4.4.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -1456,7 +1456,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 107;
|
CURRENT_PROJECT_VERSION = 114;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -1469,7 +1469,7 @@
|
|||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
"@executable_path/../../Frameworks",
|
"@executable_path/../../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 4.4;
|
MARKETING_VERSION = 4.4.2;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ public func registerGroupDefaults() {
|
|||||||
GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt,
|
GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT: KeepAliveOpts.defaults.keepCnt,
|
||||||
GROUP_DEFAULT_INCOGNITO: false,
|
GROUP_DEFAULT_INCOGNITO: false,
|
||||||
GROUP_DEFAULT_STORE_DB_PASSPHRASE: true,
|
GROUP_DEFAULT_STORE_DB_PASSPHRASE: true,
|
||||||
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false
|
GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false,
|
||||||
|
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||||
|
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: true
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1670,6 +1670,7 @@ public struct ChatItem: Identifiable, Decodable {
|
|||||||
public var file: CIFile?
|
public var file: CIFile?
|
||||||
|
|
||||||
public var viewTimestamp = Date.now
|
public var viewTimestamp = Date.now
|
||||||
|
public var isLiveDummy: Bool = false
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case chatDir, meta, content, formattedText, quotedItem, file
|
case chatDir, meta, content, formattedText, quotedItem, file
|
||||||
@@ -1862,6 +1863,29 @@ public struct ChatItem: Identifiable, Decodable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func liveDummy(_ chatType: ChatType) -> ChatItem {
|
||||||
|
var item = ChatItem(
|
||||||
|
chatDir: chatType == ChatType.direct ? CIDirection.directSnd : CIDirection.groupSnd,
|
||||||
|
meta: CIMeta(
|
||||||
|
itemId: -2,
|
||||||
|
itemTs: .now,
|
||||||
|
itemText: "",
|
||||||
|
itemStatus: .rcvRead,
|
||||||
|
createdAt: .now,
|
||||||
|
updatedAt: .now,
|
||||||
|
itemDeleted: false,
|
||||||
|
itemEdited: false,
|
||||||
|
itemLive: true,
|
||||||
|
editable: false
|
||||||
|
),
|
||||||
|
content: .sndMsgContent(msgContent: .text("")),
|
||||||
|
quotedItem: nil,
|
||||||
|
file: nil
|
||||||
|
)
|
||||||
|
item.isLiveDummy = true
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
public static func invalidJSON(_ json: String) -> ChatItem {
|
public static func invalidJSON(_ json: String) -> ChatItem {
|
||||||
ChatItem(
|
ChatItem(
|
||||||
chatDir: CIDirection.directSnd,
|
chatDir: CIDirection.directSnd,
|
||||||
@@ -2060,10 +2084,10 @@ public enum CIContent: Decodable, ItemContent {
|
|||||||
|
|
||||||
public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String {
|
public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String {
|
||||||
allowed != .no && feature.hasParam && param != nil
|
allowed != .no && feature.hasParam && param != nil
|
||||||
? "offered \(feature.text): \(TimedMessagesPreference.ttlText(param))"
|
? String.localizedStringWithFormat(NSLocalizedString("offered %@: %@", comment: "feature offered item"), feature.text, TimedMessagesPreference.ttlText(param))
|
||||||
: allowed != .no
|
: allowed != .no
|
||||||
? "offered \(feature.text)"
|
? String.localizedStringWithFormat(NSLocalizedString("offered %@", comment: "feature offered item"), feature.text)
|
||||||
: "cancelled \(feature.text)"
|
: String.localizedStringWithFormat(NSLocalizedString("cancelled %@", comment: "feature offered item"), feature.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var msgContent: MsgContent? {
|
public var msgContent: MsgContent? {
|
||||||
|
|||||||
@@ -143,25 +143,25 @@
|
|||||||
"%lld second(s)" = "%lld Sekunde(n)";
|
"%lld second(s)" = "%lld Sekunde(n)";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lldd" = "%lldd";
|
"%lldd" = "%lldT";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lldh" = "%lldh";
|
"%lldh" = "%lldH";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lldk" = "%lldk";
|
"%lldk" = "%lldk";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lldm" = "%lldm";
|
"%lldm" = "%lldmin";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lldmth" = "%lldmth";
|
"%lldmth" = "%lldMon";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%llds" = "%llds";
|
"%llds" = "%lldsek";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"%lldw" = "%lldw";
|
"%lldw" = "%lldW";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"`a + b`" = "\\`a + b`";
|
"`a + b`" = "\\`a + b`";
|
||||||
@@ -264,19 +264,19 @@
|
|||||||
"Allow disappearing messages only if your contact allows it to you." = "Verschwindende Nachrichten nur erlauben, wenn Ihr Kontakt das ebenfalls erlaubt.";
|
"Allow disappearing messages only if your contact allows it to you." = "Verschwindende Nachrichten nur erlauben, wenn Ihr Kontakt das ebenfalls erlaubt.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Allow irreversible message deletion only if your contact allows it to you." = "Erlauben Sie das unwiederbringliche löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.";
|
"Allow irreversible message deletion only if your contact allows it to you." = "Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Allow sending direct messages to members." = "Erlauben Sie das Senden von Direktnachrichten an Mitglieder";
|
"Allow sending direct messages to members." = "Das Senden von Direktnachrichten an Gruppenmitglieder erlauben.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Allow sending disappearing messages." = "Das Senden von verschwindenden Nachrichten erlauben.";
|
"Allow sending disappearing messages." = "Das Senden von verschwindenden Nachrichten erlauben.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Allow to irreversibly delete sent messages." = "Unwiederbringliches Löschen von gesendeten Nachrichten erlauben.";
|
"Allow to irreversibly delete sent messages." = "Unwiederbringliches löschen von gesendeten Nachrichten erlauben.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Allow to send voice messages." = "Senden von Sprachnachrichten erlauben.";
|
"Allow to send voice messages." = "Das Senden von Sprachnachrichten erlauben.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Allow voice messages only if your contact allows them." = "Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt.";
|
"Allow voice messages only if your contact allows them." = "Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt.";
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
"Clear conversation?" = "Unterhaltung löschen?";
|
"Clear conversation?" = "Unterhaltung löschen?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Clear verification" = "Überprüfung erfolgt";
|
"Clear verification" = "Überprüfung zurücknehmen";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"colored" = "farbig";
|
"colored" = "farbig";
|
||||||
@@ -810,7 +810,7 @@
|
|||||||
"Direct messages" = "Direkte Nachrichten";
|
"Direct messages" = "Direkte Nachrichten";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Direct messages between members are prohibited in this group." = "In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht möglich.";
|
"Direct messages between members are prohibited in this group." = "In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.";
|
||||||
|
|
||||||
/* authentication reason */
|
/* authentication reason */
|
||||||
"Disable SimpleX Lock" = "SimpleX Sperre deaktivieren";
|
"Disable SimpleX Lock" = "SimpleX Sperre deaktivieren";
|
||||||
@@ -1005,7 +1005,7 @@
|
|||||||
"Error saving passphrase to keychain" = "Fehler beim Speichern des Passworts in den Schlüsselbund";
|
"Error saving passphrase to keychain" = "Fehler beim Speichern des Passworts in den Schlüsselbund";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Error saving SMP servers" = "Fehler beim Speichern der SMP Server";
|
"Error saving SMP servers" = "Fehler beim Speichern der SMP-Server";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Error sending message" = "Fehler beim Senden der Nachricht";
|
"Error sending message" = "Fehler beim Senden der Nachricht";
|
||||||
@@ -1110,7 +1110,7 @@
|
|||||||
"Group members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten senden.";
|
"Group members can send disappearing messages." = "Gruppenmitglieder können verschwindende Nachrichten senden.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Group members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten senden.";
|
"Group members can send voice messages." = "Gruppenmitglieder können Sprachnachrichten versenden.";
|
||||||
|
|
||||||
/* notification */
|
/* notification */
|
||||||
"Group message:" = "Grppennachricht:";
|
"Group message:" = "Grppennachricht:";
|
||||||
@@ -1122,7 +1122,7 @@
|
|||||||
"Group profile" = "Gruppenprofil";
|
"Group profile" = "Gruppenprofil";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Group profile is stored on members' devices, not on the servers." = "Das Gruppenprofil wird nur auf den Mitglieds-Systemen gespeichtert und nicht auf den Servern.";
|
"Group profile is stored on members' devices, not on the servers." = "Das Gruppenprofil wird nur auf den Mitglieds-Systemen gespeichert und nicht auf den Servern.";
|
||||||
|
|
||||||
/* snd group event chat item */
|
/* snd group event chat item */
|
||||||
"group profile updated" = "Gruppenprofil aktualisiert";
|
"group profile updated" = "Gruppenprofil aktualisiert";
|
||||||
@@ -1290,13 +1290,13 @@
|
|||||||
"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Für die sichere Speicherung des Passworts nach dem Neustart der App und dem Wechsel des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen.";
|
"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Für die sichere Speicherung des Passworts nach dem Neustart der App und dem Wechsel des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Irreversible message deletion" = "Unwiederbringliches Löschen einer Nachricht";
|
"Irreversible message deletion" = "Unwiederbringliches löschen einer Nachricht";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Irreversible message deletion is prohibited in this chat." = "In diesem Chat ist das unwiederbringliche Löschen von Nachrichten untersagt.";
|
"Irreversible message deletion is prohibited in this chat." = "In diesem Chat ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Irreversible message deletion is prohibited in this group." = "In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten verboten.";
|
"Irreversible message deletion is prohibited in this group." = "In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Er ermöglicht mehrere anonyme Verbindungen in einem einzigen Chat-Profil ohne Daten zwischen diesen zu teilen.";
|
"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Er ermöglicht mehrere anonyme Verbindungen in einem einzigen Chat-Profil ohne Daten zwischen diesen zu teilen.";
|
||||||
@@ -1368,7 +1368,7 @@
|
|||||||
"Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@)." = "Stellen Sie sicher, dass die SMP-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind (%@).";
|
"Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@)." = "Stellen Sie sicher, dass die SMP-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind (%@).";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.";
|
"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Viele Menschen haben gefragt: *Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?*";
|
"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Viele Menschen haben gefragt: *Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?*";
|
||||||
@@ -1561,7 +1561,7 @@
|
|||||||
"Only you can send disappearing messages." = "Nur Sie können verschwindende Nachrichten senden.";
|
"Only you can send disappearing messages." = "Nur Sie können verschwindende Nachrichten senden.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Only you can send voice messages." = "Nur Sie können Sprachnachrichten senden.";
|
"Only you can send voice messages." = "Nur Sie können Sprachnachrichten versenden.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).";
|
"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).";
|
||||||
@@ -1570,7 +1570,7 @@
|
|||||||
"Only your contact can send disappearing messages." = "Nur Ihr Kontakt kann verschwindende Nachrichten senden.";
|
"Only your contact can send disappearing messages." = "Nur Ihr Kontakt kann verschwindende Nachrichten senden.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten senden.";
|
"Only your contact can send voice messages." = "Nur Ihr Kontakt kann Sprachnachrichten versenden.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Open chat" = "Chat öffnen";
|
"Open chat" = "Chat öffnen";
|
||||||
@@ -1663,16 +1663,16 @@
|
|||||||
"Profile image" = "Profilbild";
|
"Profile image" = "Profilbild";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Prohibit irreversible message deletion." = "Unwiederbringliches Löschen von Nachrichten verbieten.";
|
"Prohibit irreversible message deletion." = "Unwiederbringliches löschen von Nachrichten nicht erlauben.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Prohibit sending direct messages to members." = "Verbieten Sie das Senden von Direktnachrichten an Mitglieder";
|
"Prohibit sending direct messages to members." = "Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Prohibit sending disappearing messages." = "Das Senden von verschwindenden Nachrichten verbieten.";
|
"Prohibit sending disappearing messages." = "Das Senden von verschwindenden Nachrichten verbieten.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Prohibit sending voice messages." = "Senden von Sprachnachrichten untersagen.";
|
"Prohibit sending voice messages." = "Das Senden von Sprachnachrichten nicht erlauben.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Protect app screen" = "App-Bildschirm schützen";
|
"Protect app screen" = "App-Bildschirm schützen";
|
||||||
@@ -1822,7 +1822,7 @@
|
|||||||
"Save preferences?" = "Präferenzen speichern?";
|
"Save preferences?" = "Präferenzen speichern?";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Save servers" = "Server speichern";
|
"Save servers" = "Alle Server speichern";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt";
|
"Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt";
|
||||||
@@ -1900,7 +1900,7 @@
|
|||||||
"Sent messages will be deleted after set time." = "Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht.";
|
"Sent messages will be deleted after set time." = "Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht.";
|
||||||
|
|
||||||
/* server test error */
|
/* server test error */
|
||||||
"Server requires authorization to create queues, check password" = "Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort.";
|
"Server requires authorization to create queues, check password" = "Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Server test failed!" = "Server Test ist fehlgeschlagen!";
|
"Server test failed!" = "Server Test ist fehlgeschlagen!";
|
||||||
@@ -2221,7 +2221,7 @@
|
|||||||
"Use for new connections" = "Für neue Verbindungen nutzen";
|
"Use for new connections" = "Für neue Verbindungen nutzen";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Use server" = "Benutze Server";
|
"Use server" = "Server nutzen";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Use SimpleX Chat servers?" = "Verwenden Sie SimpleX Chat Server?";
|
"Use SimpleX Chat servers?" = "Verwenden Sie SimpleX Chat Server?";
|
||||||
@@ -2272,13 +2272,13 @@
|
|||||||
"Voice messages" = "Sprachnachrichten";
|
"Voice messages" = "Sprachnachrichten";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Voice messages are prohibited in this chat." = "In diesem Chat sind Sprachnachrichten untersagt.";
|
"Voice messages are prohibited in this chat." = "In diesem Chat sind Sprachnachrichten nicht erlaubt.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Voice messages are prohibited in this group." = "In dieser Gruppe sind Sprachnachrichten untersagt.";
|
"Voice messages are prohibited in this group." = "In dieser Gruppe sind Sprachnachrichten nicht erlaubt.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Voice messages prohibited!" = "Sprachnachrichten sind untersagt!";
|
"Voice messages prohibited!" = "Sprachnachrichten sind nicht erlaubt!";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"waiting for answer…" = "Warten auf Antwort…";
|
"waiting for answer…" = "Warten auf Antwort…";
|
||||||
|
|||||||
@@ -1371,7 +1371,7 @@
|
|||||||
"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.";
|
"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Beaucoup de gens ont demandé : *si SimpleX n'a pas d'identifiants d'utilisateur, comment peut-il délivrer des messages?*";
|
"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Beaucoup se demandent : *si SimpleX n'a pas d'identifiants d'utilisateur, comment peut-il délivrer des messages?*";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Mark deleted for everyone" = "Marquer comme supprimé pour tout le monde";
|
"Mark deleted for everyone" = "Marquer comme supprimé pour tout le monde";
|
||||||
@@ -1467,7 +1467,7 @@
|
|||||||
"New database archive" = "Nouvelle archive de base de données";
|
"New database archive" = "Nouvelle archive de base de données";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"New in %@" = "Nouveau en %@";
|
"New in %@" = "Nouveautés de la %@";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"New member role" = "Nouveau rôle";
|
"New member role" = "Nouveau rôle";
|
||||||
@@ -2116,7 +2116,7 @@
|
|||||||
"This group no longer exists." = "Ce groupe n'existe plus.";
|
"This group no longer exists." = "Ce groupe n'existe plus.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"To ask any questions and to receive updates:" = "Pour poser toute question et recevoir des mises à jour :";
|
"To ask any questions and to receive updates:" = "Si vous avez des questions et que vous souhaitez des réponses :";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* 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." = "Pour trouver le profil utilisé lors d'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat.";
|
"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Pour trouver le profil utilisé lors d'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat.";
|
||||||
@@ -2128,7 +2128,7 @@
|
|||||||
"To prevent the call interruption, enable Do Not Disturb mode." = "Pour éviter l'interruption des appels, activez le mode Ne pas déranger.";
|
"To prevent the call interruption, enable Do Not Disturb mode." = "Pour éviter l'interruption des appels, activez le mode Ne pas déranger.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Pour protéger la vie privée, au lieu des ID utilisateurs utilisés par toutes les autres plateformes, SimpleX a des ID pour les queues de messages, distinctes pour chacun de vos contacts.";
|
"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Pour protéger votre vie privée, au lieu d’IDs utilisés par toutes les autres plateformes, SimpleX a des IDs pour les queues de messages, distinctes pour chacun de vos contacts.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Pour protéger vos informations, activez la fonction SimpleX Lock.\nVous serez invité à confirmer l'authentification avant que cette fonction ne soit activée.";
|
"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Pour protéger vos informations, activez la fonction SimpleX Lock.\nVous serez invité à confirmer l'authentification avant que cette fonction ne soit activée.";
|
||||||
@@ -2380,7 +2380,7 @@
|
|||||||
"you changed role of %@ to %@" = "vous avez modifié le rôle de %1$@ pour %2$@";
|
"you changed role of %@ to %@" = "vous avez modifié le rôle de %1$@ pour %2$@";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** des messages de vos contacts.";
|
"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Vous contrôlez par quel·s serveur·s vous pouvez **transmettre** ainsi que par quel·s serveur·s vous pouvez **recevoir** les messages de vos contacts.";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"You could not be verified; please try again." = "Vous n'avez pas pu être vérifié·e ; veuillez réessayer.";
|
"You could not be verified; please try again." = "Vous n'avez pas pu être vérifié·e ; veuillez réessayer.";
|
||||||
|
|||||||
@@ -377,6 +377,9 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Cancel" = "Отменить";
|
"Cancel" = "Отменить";
|
||||||
|
|
||||||
|
/* feature offered item */
|
||||||
|
"cancelled %@" = "отменил(a) %@";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Cannot access keychain to save database password" = "Ошибка доступа к Keychain при сохранении пароля";
|
"Cannot access keychain to save database password" = "Ошибка доступа к Keychain при сохранении пароля";
|
||||||
|
|
||||||
@@ -1521,6 +1524,12 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Off (Local)" = "Выключить (Локальные)";
|
"Off (Local)" = "Выключить (Локальные)";
|
||||||
|
|
||||||
|
/* feature offered item */
|
||||||
|
"offered %@" = "предложил(a) %@";
|
||||||
|
|
||||||
|
/* feature offered item */
|
||||||
|
"offered %@: %@" = "предложил(a) %1$@: %2$@";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Ok" = "Ок";
|
"Ok" = "Ок";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
layout: layouts/article.html
|
||||||
|
title: "SimpleX Chat v4.4 released – with disappearing messages, live messages, connection security verification and French language!"
|
||||||
|
date: 2023-01-03
|
||||||
|
image: images/20230103-disappearing1.png
|
||||||
|
imageBottom: true
|
||||||
|
previewBody: blog_previews/20230103.html
|
||||||
|
permalink: "/blog/20230103-simplex-chat-v4.4-disappearing-messages.html"
|
||||||
|
---
|
||||||
|
|
||||||
|
# SimpleX Chat v4.4 released – with disappearing messages, live messages, connection security verification and French language!
|
||||||
|
|
||||||
|
**Published:** Jan 3, 2023
|
||||||
|
|
||||||
|
## What's new in v4.4
|
||||||
|
|
||||||
|
- [disappearing messages](#disappearing-messages).
|
||||||
|
- ["live" messages](#live-messages).
|
||||||
|
- [connection security verification](#connection-security-verification).
|
||||||
|
- [animated images and stickers](#animated-images-and-stickers) – now on iOS too.
|
||||||
|
|
||||||
|
Also, we added [French language interface](#french-language-interface), thanks to the users' community and Weblate!
|
||||||
|
|
||||||
|
### Disappearing messages
|
||||||
|
|
||||||
|
<img src="./images/20230103-disappearing1.png" width="288"> <img src="./images/20230103-disappearing2.png" width="288">
|
||||||
|
|
||||||
|
It is now possible to send the messages that will be deleted from both sender and recipient device after set time – for the sender from the time they were sent, and for the recipient - from the time they were read.
|
||||||
|
|
||||||
|
Unlike in most other messengers, it requires agreement of both sides, not just the sender decision. I [wrote previously](./20221206-simplex-chat-v4.3-voice-messages.md#irreversible-message-deletion) why we believe it is wrong to allow the senders to delete their messages without recipient consent, and the same logic applies here – if you want to send the message that will disappear after some time, your contact should be ok with that too.
|
||||||
|
|
||||||
|
In group conversations disappearing messages can be enabled by the group owners, by default they are disabled.
|
||||||
|
|
||||||
|
### "Live" messages
|
||||||
|
|
||||||
|
<img src="./images/20230103-live.png" width="288">
|
||||||
|
|
||||||
|
Pressing "bolt" button before you start typing the message will start a "live" message. Now, as you type it, it will be updated for all recipients every several seconds, including only complete words. To finish the message you need to press "checkmark" button.
|
||||||
|
|
||||||
|
You can also start a live message after you started typing or after you chose the image – long-press send button and then press "Send live message".
|
||||||
|
|
||||||
|
### Connection security verification
|
||||||
|
|
||||||
|
<img src="./images/20230103-verification.png" width="288">
|
||||||
|
|
||||||
|
SimpleX Chat design prevents the possibility of messaging servers substituting the key during the initial connection (man-in-the-middle attack) by requiring that the invitation link is passed via another channel. I wrote more about how MITM attack works in [this post](https://www.poberezkin.com/posts/2022-12-07-why-privacy-needs-to-be-redefined.html). But this other channel, however unlikely, could still have been compromised by an attacker to replace the invitation link you sent. That is the reason why we recommend sharing QR code in a video call – this is very complex for an attacker to replace it in this case.
|
||||||
|
|
||||||
|
This new feature allows you to verify, via yet another channel, that the connection is secure and the keys were not replaced. You can either scan the security code from your contact's app, or compare codes visually, or even read it in a voice call – if your and your contact's app have the same security code for each other then the connection is secure.
|
||||||
|
|
||||||
|
If you are sending direct messages to some group members then it might also be important to verify security of these connections, as in this case the invitations were exchanged via the member who added you or another member, and if this member's client was modified, they could have replaced the keys and the addresses, and intercept the entire conversation.
|
||||||
|
|
||||||
|
Regardless how connection is established, verifying the connection proves its security. Technically, this security code is the hash of associated data used in the end-to-end encryption, which in turn is taken by combining public keys from the initial key exchange.
|
||||||
|
|
||||||
|
### Animated images and stickers
|
||||||
|
|
||||||
|
<img src="./images/20230103-stickers1.png" width="288"> <img src="./images/20230103-stickers2.png" width="303">
|
||||||
|
|
||||||
|
Android app supported GIFs and stickers for some time, now you can view and send them from iOS app as well, e.g. using GIPHY keyboard - you no longer need to choose between privacy and stickers. Just bear in mind, that third party keyboards can be insecure, so you should not be using them for typing sensitive information.
|
||||||
|
|
||||||
|
### French language interface
|
||||||
|
|
||||||
|
Thanks to our users' community and to [Weblate](https://weblate.org/en-gb/) kindly providing a free hosting plan for SimpleX Chat translations we can now support more languages in the interface – this version adds French.
|
||||||
|
|
||||||
|
Please get in touch if you want to translate the interface into your language!
|
||||||
|
|
||||||
|
## SimpleX platform
|
||||||
|
|
||||||
|
Some links to answer the most common questions:
|
||||||
|
|
||||||
|
[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers).
|
||||||
|
|
||||||
|
[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users).
|
||||||
|
|
||||||
|
[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).
|
||||||
|
|
||||||
|
Please also see our [website](https://simplex.chat).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
It is possible to donate via:
|
||||||
|
|
||||||
|
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
|
||||||
|
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
|
||||||
|
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt - Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||||
|
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||||
|
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||||
|
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
|
||||||
|
|
||||||
|
Thank you,
|
||||||
|
|
||||||
|
Evgeny
|
||||||
|
|
||||||
|
SimpleX Chat founder
|
||||||
+10
-1
@@ -1,6 +1,15 @@
|
|||||||
# Blog
|
# Blog
|
||||||
|
|
||||||
Dec 12, 2022 [SimpleX Chat reviews and v4.3 released]
|
Jan 3, 2023 [SimpleX Chat v4.4 released](./20230103-simplex-chat-v4.4-disappearing-messages.md)
|
||||||
|
|
||||||
|
- disappearing messages.
|
||||||
|
- "live" messages.
|
||||||
|
- connection security verification.
|
||||||
|
- animated images and stickers – now on iOS too.
|
||||||
|
|
||||||
|
Also, we added [French language interface](#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:
|
November reviews:
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 353 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 343 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 480 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
+1
-1
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 874f9f5cd67310ddfbe5540d6abfa5943a534f37
|
tag: 058e3ac55e8577280267f9341ccd7d3e971bc51a
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
Generated
+6
-6
@@ -302,11 +302,11 @@
|
|||||||
"hackage": {
|
"hackage": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1669857312,
|
"lastModified": 1672446463,
|
||||||
"narHash": "sha256-m0jYF2gOKTaCcedV+dZkCjVbfv0CWkRziCeEk/NF/34=",
|
"narHash": "sha256-N5dcK1V+BLQeri5oB0ZSk2ABPs/hTGBC7jZvcY9CVLs=",
|
||||||
"owner": "input-output-hk",
|
"owner": "input-output-hk",
|
||||||
"repo": "hackage.nix",
|
"repo": "hackage.nix",
|
||||||
"rev": "8299f5acc68f0e91563e7688f24cbc70391600bf",
|
"rev": "7289869780da23633bc193e11d2da94ecee1489d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -343,11 +343,11 @@
|
|||||||
"tullia": "tullia"
|
"tullia": "tullia"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1669979126,
|
"lastModified": 1672501055,
|
||||||
"narHash": "sha256-CZBKwljLLv2GhE7jq7Gr54rJhxM0TRTFSM+Ix5H52ak=",
|
"narHash": "sha256-Wy6KqoYqQOP1rBvfHUvM3ex8HIdBA4kwnvZj2qQ1VLU=",
|
||||||
"owner": "simplex-chat",
|
"owner": "simplex-chat",
|
||||||
"repo": "haskell.nix",
|
"repo": "haskell.nix",
|
||||||
"rev": "3634dc742f197396880593d9007465bccbb7292c",
|
"rev": "dc719cd6dc318923c4a524d005c13f474c00d3d3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 4.4.0
|
version: 4.4.2
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function readlink() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [ -z ${1} ]; then
|
if [ -z ${1} ]; then
|
||||||
echo "Job repo is unset. Provide it via first argument like: $(readlink $0)/download_libs_aarch64.sh https://something.com/job/something"
|
echo "Job repo is unset. Provide it via first argument like: $(readlink $0)/download_libs_aarch64.sh https://something.com/job/something/{master,stable}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -22,12 +22,12 @@ output_dir="$root_dir/apps/android/app/src/main/cpp/libs/$output_arch/"
|
|||||||
|
|
||||||
mkdir -p "$output_dir" 2> /dev/null
|
mkdir -p "$output_dir" 2> /dev/null
|
||||||
|
|
||||||
curl --location -o libsupport.zip $job_repo/simplex-chat/$arch-android:lib:support.x86_64-linux/latest/download/1 && \
|
curl --location -o libsupport.zip $job_repo/$arch-android:lib:support.x86_64-linux/latest/download/1 && \
|
||||||
unzip -o libsupport.zip && \
|
unzip -o libsupport.zip && \
|
||||||
mv libsupport.so "$output_dir" && \
|
mv libsupport.so "$output_dir" && \
|
||||||
rm libsupport.zip
|
rm libsupport.zip
|
||||||
|
|
||||||
curl --location -o libsimplex.zip $job_repo/simplex-chat/$arch-android:lib:simplex-chat.x86_64-linux/latest/download/1 && \
|
curl --location -o libsimplex.zip $job_repo/$arch-android:lib:simplex-chat.x86_64-linux/latest/download/1 && \
|
||||||
unzip -o libsimplex.zip && \
|
unzip -o libsimplex.zip && \
|
||||||
mv libsimplex.so "$output_dir" && \
|
mv libsimplex.so "$output_dir" && \
|
||||||
rm libsimplex.zip
|
rm libsimplex.zip
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function readlink() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [ -z ${1} ]; then
|
if [ -z ${1} ]; then
|
||||||
echo "Job repo is unset. Provide it via first argument like: $(readlink $0)/download_libs_aarch64.sh https://something.com/job/something"
|
echo "Job repo is unset. Provide it via first argument like: $(readlink $0)/download_libs_aarch64.sh https://something.com/job/something/{master,stable}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ job_repo=$1
|
|||||||
|
|
||||||
root_dir="$(dirname $(dirname $(readlink $0)))"
|
root_dir="$(dirname $(dirname $(readlink $0)))"
|
||||||
|
|
||||||
curl --location -o ~/Downloads/pkg-ios-aarch64-swift-json.zip $job_repo/simplex-chat/aarch64-darwin-ios:lib:simplex-chat.aarch64-darwin/latest/download/1 && \
|
curl --location -o ~/Downloads/pkg-ios-aarch64-swift-json.zip $job_repo/aarch64-darwin-ios:lib:simplex-chat.aarch64-darwin/latest/download/1 && \
|
||||||
unzip -o ~/Downloads/pkg-ios-aarch64-swift-json.zip -d ~/Downloads/pkg-ios-aarch64-swift-json
|
unzip -o ~/Downloads/pkg-ios-aarch64-swift-json.zip -d ~/Downloads/pkg-ios-aarch64-swift-json
|
||||||
|
|
||||||
curl --location -o ~/Downloads/pkg-ios-x86_64-swift-json.zip $job_repo/simplex-chat/x86_64-darwin-ios:lib:simplex-chat.x86_64-darwin/latest/download/1 && \
|
curl --location -o ~/Downloads/pkg-ios-x86_64-swift-json.zip $job_repo/x86_64-darwin-ios:lib:simplex-chat.x86_64-darwin/latest/download/1 && \
|
||||||
unzip -o ~/Downloads/pkg-ios-x86_64-swift-json.zip -d ~/Downloads/pkg-ios-x86_64-swift-json
|
unzip -o ~/Downloads/pkg-ios-x86_64-swift-json.zip -d ~/Downloads/pkg-ios-x86_64-swift-json
|
||||||
|
|
||||||
sh $root_dir/scripts/ios/prepare-x86_64.sh
|
sh $root_dir/scripts/ios/prepare-x86_64.sh
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."874f9f5cd67310ddfbe5540d6abfa5943a534f37" = "0nyhsrg9nb205x57h1dwrhwi9j7bwh3j6hpp7p1kp9pqdsxhcvdl";
|
"https://github.com/simplex-chat/simplexmq.git"."058e3ac55e8577280267f9341ccd7d3e971bc51a" = "1rw0j3d5higdrq5klsgnj8b8zfh08g5zv72hqcm7wkw1mmllpfrk";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
|
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
|
||||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 4.4.0
|
version: 4.4.2
|
||||||
category: Web, System, Services, Cryptography
|
category: Web, System, Services, Cryptography
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
author: simplex.chat
|
author: simplex.chat
|
||||||
@@ -73,6 +73,7 @@ library
|
|||||||
Simplex.Chat.Migrations.M20221222_chat_ts
|
Simplex.Chat.Migrations.M20221222_chat_ts
|
||||||
Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status
|
Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status
|
||||||
Simplex.Chat.Migrations.M20221230_idxs
|
Simplex.Chat.Migrations.M20221230_idxs
|
||||||
|
Simplex.Chat.Migrations.M20230107_connections_auth_err_counter
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Options
|
Simplex.Chat.Options
|
||||||
Simplex.Chat.ProfileGenerator
|
Simplex.Chat.ProfileGenerator
|
||||||
|
|||||||
+147
-78
@@ -227,13 +227,21 @@ restoreCalls user = do
|
|||||||
calls <- asks currentCalls
|
calls <- asks currentCalls
|
||||||
atomically $ writeTVar calls callsMap
|
atomically $ writeTVar calls callsMap
|
||||||
|
|
||||||
stopChatController :: MonadUnliftIO m => ChatController -> m ()
|
stopChatController :: forall m. MonadUnliftIO m => ChatController -> m ()
|
||||||
stopChatController ChatController {smpAgent, agentAsync = s, expireCIs} = do
|
stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIs} = do
|
||||||
disconnectAgentClient smpAgent
|
disconnectAgentClient smpAgent
|
||||||
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
|
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
|
||||||
|
closeFiles sndFiles
|
||||||
|
closeFiles rcvFiles
|
||||||
atomically $ do
|
atomically $ do
|
||||||
writeTVar expireCIs False
|
writeTVar expireCIs False
|
||||||
writeTVar s Nothing
|
writeTVar s Nothing
|
||||||
|
where
|
||||||
|
closeFiles :: TVar (Map Int64 Handle) -> m ()
|
||||||
|
closeFiles files = do
|
||||||
|
fs <- readTVarIO files
|
||||||
|
mapM_ hClose fs
|
||||||
|
atomically $ writeTVar files M.empty
|
||||||
|
|
||||||
execChatCommand :: (MonadUnliftIO m, MonadReader ChatController m) => ByteString -> m ChatResponse
|
execChatCommand :: (MonadUnliftIO m, MonadReader ChatController m) => ByteString -> m ChatResponse
|
||||||
execChatCommand s = case parseChatCommand s of
|
execChatCommand s = case parseChatCommand s of
|
||||||
@@ -383,12 +391,14 @@ processChatCommand = \case
|
|||||||
pure (fileInvitation, ciFile, ft)
|
pure (fileInvitation, ciFile, ft)
|
||||||
sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m ()
|
sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m ()
|
||||||
sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} =
|
sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} =
|
||||||
when (fileInline == Just IFMSent) . forM_ ms $ \case
|
when (fileInline == Just IFMSent) . forM_ ms $ \m ->
|
||||||
m@GroupMember {activeConn = Just conn@Connection {connStatus}} ->
|
processMember m `catchError` (toView . CRChatError)
|
||||||
|
where
|
||||||
|
processMember m@GroupMember {activeConn = Just conn@Connection {connStatus}} =
|
||||||
when (connStatus == ConnReady || connStatus == ConnSndReady) $ do
|
when (connStatus == ConnReady || connStatus == ConnSndReady) $ do
|
||||||
void . withStore' $ \db -> createSndGroupInlineFT db m conn ft
|
void . withStore' $ \db -> createSndGroupInlineFT db m conn ft
|
||||||
sendMemberFileInline m conn ft sharedMsgId
|
sendMemberFileInline m conn ft sharedMsgId
|
||||||
_ -> pure ()
|
processMember _ = pure ()
|
||||||
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> GroupMember -> m (MsgContainer, Maybe (CIQuote 'CTGroup))
|
prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> GroupMember -> m (MsgContainer, Maybe (CIQuote 'CTGroup))
|
||||||
prepareMsg fileInvitation_ timed_ membership = case quotedItemId_ of
|
prepareMsg fileInvitation_ timed_ membership = case quotedItemId_ of
|
||||||
Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_ (ttl' <$> timed_) (justTrue live)), Nothing)
|
Nothing -> pure (MCSimple (ExtMsgContent mc fileInvitation_ (ttl' <$> timed_) (justTrue live)), Nothing)
|
||||||
@@ -832,6 +842,17 @@ processChatCommand = \case
|
|||||||
case activeConn of
|
case activeConn of
|
||||||
Just conn -> verifyConnectionCode user conn code
|
Just conn -> verifyConnectionCode user conn code
|
||||||
_ -> throwChatError CEGroupMemberNotActive
|
_ -> throwChatError CEGroupMemberNotActive
|
||||||
|
APIEnableContact contactId -> withUser $ \user -> do
|
||||||
|
Contact {activeConn} <- withStore $ \db -> getContact db user contactId
|
||||||
|
withStore' $ \db -> setConnectionAuthErrCounter db user activeConn 0
|
||||||
|
pure CRCmdOk
|
||||||
|
APIEnableGroupMember gId gMemberId -> withUser $ \user -> do
|
||||||
|
GroupMember {activeConn} <- withStore $ \db -> getGroupMember db user gId gMemberId
|
||||||
|
case activeConn of
|
||||||
|
Just conn -> do
|
||||||
|
withStore' $ \db -> setConnectionAuthErrCounter db user conn 0
|
||||||
|
pure CRCmdOk
|
||||||
|
_ -> throwChatError CEGroupMemberNotActive
|
||||||
ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do
|
ShowMessages (ChatName cType name) ntfOn -> withUser $ \user -> do
|
||||||
chatId <- case cType of
|
chatId <- case cType of
|
||||||
CTDirect -> withStore $ \db -> getContactIdByName db user name
|
CTDirect -> withStore $ \db -> getContactIdByName db user name
|
||||||
@@ -846,6 +867,8 @@ processChatCommand = \case
|
|||||||
GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode
|
GetGroupMemberCode gName mName -> withMemberName gName mName APIGetGroupMemberCode
|
||||||
VerifyContact cName code -> withContactName cName (`APIVerifyContact` code)
|
VerifyContact cName code -> withContactName cName (`APIVerifyContact` code)
|
||||||
VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code
|
VerifyGroupMember gName mName code -> withMemberName gName mName $ \gId mId -> APIVerifyGroupMember gId mId code
|
||||||
|
EnableContact cName -> withContactName cName APIEnableContact
|
||||||
|
EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId
|
||||||
ChatHelp section -> pure $ CRChatHelp section
|
ChatHelp section -> pure $ CRChatHelp section
|
||||||
Welcome -> withUser $ pure . CRWelcome
|
Welcome -> withUser $ pure . CRWelcome
|
||||||
AddContact -> withUser $ \User {userId} -> withChatLock "addContact" . procCmd $ do
|
AddContact -> withUser $ \User {userId} -> withChatLock "addContact" . procCmd $ do
|
||||||
@@ -1136,8 +1159,8 @@ processChatCommand = \case
|
|||||||
where
|
where
|
||||||
processError ft = \case
|
processError ft = \case
|
||||||
-- TODO AChatItem in Cancelled events
|
-- TODO AChatItem in Cancelled events
|
||||||
ChatErrorAgent (SMP SMP.AUTH) -> pure $ CRRcvFileAcceptedSndCancelled ft
|
ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled ft
|
||||||
ChatErrorAgent (CONN DUPLICATE) -> pure $ CRRcvFileAcceptedSndCancelled ft
|
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled ft
|
||||||
e -> throwError e
|
e -> throwError e
|
||||||
CancelFile fileId -> withUser $ \user@User {userId} ->
|
CancelFile fileId -> withUser $ \user@User {userId} ->
|
||||||
withChatLock "cancelFile" . procCmd $
|
withChatLock "cancelFile" . procCmd $
|
||||||
@@ -1306,13 +1329,16 @@ processChatCommand = \case
|
|||||||
asks currentUser >>= atomically . (`writeTVar` Just user')
|
asks currentUser >>= atomically . (`writeTVar` Just user')
|
||||||
withChatLock "updateProfile" . procCmd $ do
|
withChatLock "updateProfile" . procCmd $ do
|
||||||
forM_ contacts $ \ct -> do
|
forM_ contacts $ \ct -> do
|
||||||
let mergedProfile = userProfileToSend user Nothing $ Just ct
|
processContact user' ct `catchError` (toView . CRChatError)
|
||||||
ct' = updateMergedPreferences user' ct
|
|
||||||
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
|
|
||||||
when (mergedProfile' /= mergedProfile) $ do
|
|
||||||
void (sendDirectContactMessage ct' $ XInfo mergedProfile') `catchError` (toView . CRChatError)
|
|
||||||
when (directOrUsed ct') $ createSndFeatureItems user' ct ct'
|
|
||||||
pure $ CRUserProfileUpdated (fromLocalProfile p) p'
|
pure $ CRUserProfileUpdated (fromLocalProfile p) p'
|
||||||
|
where
|
||||||
|
processContact user' ct = do
|
||||||
|
let mergedProfile = userProfileToSend user Nothing $ Just ct
|
||||||
|
ct' = updateMergedPreferences user' ct
|
||||||
|
mergedProfile' = userProfileToSend user' Nothing $ Just ct'
|
||||||
|
when (mergedProfile' /= mergedProfile) $ do
|
||||||
|
void $ sendDirectContactMessage ct' (XInfo mergedProfile')
|
||||||
|
when (directOrUsed ct') $ createSndFeatureItems user' ct ct'
|
||||||
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
|
updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse
|
||||||
updateContactPrefs user@User {userId} ct@Contact {activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
|
updateContactPrefs user@User {userId} ct@Contact {activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs'
|
||||||
| contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated ct ct
|
| contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated ct ct
|
||||||
@@ -1724,7 +1750,7 @@ subscribeUserConnections agentBatchSubscribe user = do
|
|||||||
pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> m ()
|
pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> m ()
|
||||||
pendingConnSubsToView rs = toView . CRPendingSubSummary . map (uncurry PendingSubStatus) . resultsFor rs
|
pendingConnSubsToView rs = toView . CRPendingSubSummary . map (uncurry PendingSubStatus) . resultsFor rs
|
||||||
withStore_ :: (DB.Connection -> User -> IO [a]) -> m [a]
|
withStore_ :: (DB.Connection -> User -> IO [a]) -> m [a]
|
||||||
withStore_ a = withStore' (`a` user) `catchError` \_ -> pure []
|
withStore_ a = withStore' (`a` user) `catchError` \e -> toView (CRChatError e) >> pure []
|
||||||
filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)]
|
filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)]
|
||||||
filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_)
|
filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_)
|
||||||
resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)]
|
resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)]
|
||||||
@@ -1734,7 +1760,7 @@ subscribeUserConnections agentBatchSubscribe user = do
|
|||||||
addResult connId = (:) . (,err)
|
addResult connId = (:) . (,err)
|
||||||
where
|
where
|
||||||
err = case M.lookup connId rs of
|
err = case M.lookup connId rs of
|
||||||
Just (Left e) -> Just $ ChatErrorAgent e
|
Just (Left e) -> Just $ ChatErrorAgent e Nothing
|
||||||
Just _ -> Nothing
|
Just _ -> Nothing
|
||||||
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
|
_ -> Just . ChatError . CEAgentNoSubResult $ AgentConnId connId
|
||||||
|
|
||||||
@@ -1867,18 +1893,19 @@ processAgentMessage (Just user) _ agentConnId END =
|
|||||||
showToast (c <> "> ") "connected to another client"
|
showToast (c <> "> ") "connected to another client"
|
||||||
unsetActive $ ActiveC c
|
unsetActive $ ActiveC c
|
||||||
entity -> toView $ CRSubscriptionEnd entity
|
entity -> toView $ CRSubscriptionEnd entity
|
||||||
processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = do
|
||||||
(withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= updateConnStatus) >>= \case
|
entity <- withStore (\db -> getConnectionEntity db user $ AgentConnId agentConnId) >>= updateConnStatus
|
||||||
|
case entity of
|
||||||
RcvDirectMsgConnection conn contact_ ->
|
RcvDirectMsgConnection conn contact_ ->
|
||||||
processDirectMessage agentMessage conn contact_
|
processDirectMessage agentMessage entity conn contact_
|
||||||
RcvGroupMsgConnection conn gInfo m ->
|
RcvGroupMsgConnection conn gInfo m ->
|
||||||
processGroupMessage agentMessage conn gInfo m
|
processGroupMessage agentMessage entity conn gInfo m
|
||||||
RcvFileConnection conn ft ->
|
RcvFileConnection conn ft ->
|
||||||
processRcvFileConn agentMessage conn ft
|
processRcvFileConn agentMessage entity conn ft
|
||||||
SndFileConnection conn ft ->
|
SndFileConnection conn ft ->
|
||||||
processSndFileConn agentMessage conn ft
|
processSndFileConn agentMessage entity conn ft
|
||||||
UserContactConnection conn uc ->
|
UserContactConnection conn uc ->
|
||||||
processUserContactRequest agentMessage conn uc
|
processUserContactRequest agentMessage entity conn uc
|
||||||
where
|
where
|
||||||
updateConnStatus :: ConnectionEntity -> m ConnectionEntity
|
updateConnStatus :: ConnectionEntity -> m ConnectionEntity
|
||||||
updateConnStatus acEntity = case agentMsgConnStatus agentMessage of
|
updateConnStatus acEntity = case agentMsgConnStatus agentMessage of
|
||||||
@@ -1899,8 +1926,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
CON -> Just ConnReady
|
CON -> Just ConnReady
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
processDirectMessage :: ACommand 'Agent -> Connection -> Maybe Contact -> m ()
|
processDirectMessage :: ACommand 'Agent -> ConnectionEntity -> Connection -> Maybe Contact -> m ()
|
||||||
processDirectMessage agentMsg conn@Connection {connId, viaUserContactLink, groupLinkId, customUserProfileId} = \case
|
processDirectMessage agentMsg connEntity conn@Connection {connId, viaUserContactLink, groupLinkId, customUserProfileId} = \case
|
||||||
Nothing -> case agentMsg of
|
Nothing -> case agentMsg of
|
||||||
CONF confId _ connInfo -> do
|
CONF confId _ connInfo -> do
|
||||||
-- [incognito] send saved profile
|
-- [incognito] send saved profile
|
||||||
@@ -1916,15 +1943,16 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
_ <- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId
|
_ <- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId
|
||||||
withAckMessage agentConnId cmdId meta $ pure ()
|
withAckMessage agentConnId cmdId meta $ pure ()
|
||||||
SENT msgId ->
|
SENT msgId ->
|
||||||
-- ? updateDirectChatItemStatus
|
|
||||||
sentMsgDeliveryEvent conn msgId
|
sentMsgDeliveryEvent conn msgId
|
||||||
OK ->
|
OK ->
|
||||||
-- [async agent commands] continuation on receiving OK
|
-- [async agent commands] continuation on receiving OK
|
||||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
||||||
when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId
|
when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId
|
||||||
MERR _ err -> toView . CRChatError $ ChatErrorAgent err -- ? updateDirectChatItemStatus
|
MERR _ err -> do
|
||||||
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
|
incAuthErrCounter connEntity conn err
|
||||||
ERR err -> do
|
ERR err -> do
|
||||||
toView . CRChatError $ ChatErrorAgent err
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
@@ -2037,14 +2065,16 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
forM_ chatItemId_ $ \chatItemId -> do
|
forM_ chatItemId_ $ \chatItemId -> do
|
||||||
chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId chatItemId (agentErrToItemStatus err)
|
chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId chatItemId (agentErrToItemStatus err)
|
||||||
toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
toView $ CRChatItemStatusUpdated (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem)
|
||||||
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
|
incAuthErrCounter connEntity conn err
|
||||||
ERR err -> do
|
ERR err -> do
|
||||||
toView . CRChatError $ ChatErrorAgent err
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
processGroupMessage :: ACommand 'Agent -> Connection -> GroupInfo -> GroupMember -> m ()
|
processGroupMessage :: ACommand 'Agent -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> m ()
|
||||||
processGroupMessage agentMsg conn@Connection {connId} gInfo@GroupInfo {groupId, localDisplayName = gName, groupProfile, membership, chatSettings} m = case agentMsg of
|
processGroupMessage agentMsg connEntity conn@Connection {connId} gInfo@GroupInfo {groupId, localDisplayName = gName, groupProfile, membership, chatSettings} m = case agentMsg of
|
||||||
INV (ACR _ cReq) ->
|
INV (ACR _ cReq) ->
|
||||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
|
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
|
||||||
case cReq of
|
case cReq of
|
||||||
@@ -2114,7 +2144,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
updateGroupMemberStatus db userId m GSMemConnected
|
updateGroupMemberStatus db userId m GSMemConnected
|
||||||
unless (memberActive membership) $
|
unless (memberActive membership) $
|
||||||
updateGroupMemberStatus db userId membership GSMemConnected
|
updateGroupMemberStatus db userId membership GSMemConnected
|
||||||
sendPendingGroupMessages m conn
|
-- possible improvement: check for each pending message, requires keeping track of connection state
|
||||||
|
unless (connDisabled conn) $ sendPendingGroupMessages m conn
|
||||||
withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ enableNtfs chatSettings
|
withAgent $ \a -> toggleConnectionNtfs a (aConnId conn) $ enableNtfs chatSettings
|
||||||
case memberCategory m of
|
case memberCategory m of
|
||||||
GCHostMember -> do
|
GCHostMember -> do
|
||||||
@@ -2132,9 +2163,12 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected"
|
showToast ("#" <> gName) $ "member " <> localDisplayName (m :: GroupMember) <> " is connected"
|
||||||
intros <- withStore' $ \db -> createIntroductions db members m
|
intros <- withStore' $ \db -> createIntroductions db members m
|
||||||
void . sendGroupMessage gInfo members . XGrpMemNew $ memberInfo m
|
void . sendGroupMessage gInfo members . XGrpMemNew $ memberInfo m
|
||||||
forM_ intros $ \intro@GroupMemberIntro {introId} -> do
|
forM_ intros $ \intro ->
|
||||||
void $ sendDirectMessage conn (XGrpMemIntro . memberInfo $ reMember intro) (GroupId groupId)
|
processIntro intro `catchError` (toView . CRChatError)
|
||||||
withStore' $ \db -> updateIntroStatus db introId GMIntroSent
|
where
|
||||||
|
processIntro intro@GroupMemberIntro {introId} = do
|
||||||
|
void $ sendDirectMessage conn (XGrpMemIntro . memberInfo $ reMember intro) (GroupId groupId)
|
||||||
|
withStore' $ \db -> updateIntroStatus db introId GMIntroSent
|
||||||
_ -> do
|
_ -> do
|
||||||
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
|
-- TODO send probe and decide whether to use existing contact connection or the new contact connection
|
||||||
-- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
|
-- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table
|
||||||
@@ -2183,15 +2217,17 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
-- [async agent commands] continuation on receiving OK
|
-- [async agent commands] continuation on receiving OK
|
||||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} ->
|
||||||
when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId
|
when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId
|
||||||
MERR _ err -> toView . CRChatError $ ChatErrorAgent err
|
MERR _ err -> do
|
||||||
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
|
incAuthErrCounter connEntity conn err
|
||||||
ERR err -> do
|
ERR err -> do
|
||||||
toView . CRChatError $ ChatErrorAgent err
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
processSndFileConn :: ACommand 'Agent -> Connection -> SndFileTransfer -> m ()
|
processSndFileConn :: ACommand 'Agent -> ConnectionEntity -> Connection -> SndFileTransfer -> m ()
|
||||||
processSndFileConn agentMsg conn ft@SndFileTransfer {fileId, fileName, fileStatus} =
|
processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} =
|
||||||
case agentMsg of
|
case agentMsg of
|
||||||
-- SMP CONF for SndFileConnection happens for direct file protocol
|
-- SMP CONF for SndFileConnection happens for direct file protocol
|
||||||
-- when recipient of the file "joins" connection created by the sender
|
-- when recipient of the file "joins" connection created by the sender
|
||||||
@@ -2229,13 +2265,13 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
-- [async agent commands] continuation on receiving OK
|
-- [async agent commands] continuation on receiving OK
|
||||||
withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
ERR err -> do
|
ERR err -> do
|
||||||
toView . CRChatError $ ChatErrorAgent err
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
processRcvFileConn :: ACommand 'Agent -> Connection -> RcvFileTransfer -> m ()
|
processRcvFileConn :: ACommand 'Agent -> ConnectionEntity -> Connection -> RcvFileTransfer -> m ()
|
||||||
processRcvFileConn agentMsg conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} =
|
processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} =
|
||||||
case agentMsg of
|
case agentMsg of
|
||||||
INV (ACR _ cReq) ->
|
INV (ACR _ cReq) ->
|
||||||
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
|
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
|
||||||
@@ -2274,9 +2310,11 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
OK ->
|
OK ->
|
||||||
-- [async agent commands] continuation on receiving OK
|
-- [async agent commands] continuation on receiving OK
|
||||||
withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
MERR _ err -> toView . CRChatError $ ChatErrorAgent err
|
MERR _ err -> do
|
||||||
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
|
incAuthErrCounter connEntity conn err
|
||||||
ERR err -> do
|
ERR err -> do
|
||||||
toView . CRChatError $ ChatErrorAgent err
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
@@ -2323,8 +2361,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
RcvChunkDuplicate -> pure ()
|
RcvChunkDuplicate -> pure ()
|
||||||
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
|
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
|
||||||
|
|
||||||
processUserContactRequest :: ACommand 'Agent -> Connection -> UserContact -> m ()
|
processUserContactRequest :: ACommand 'Agent -> ConnectionEntity -> Connection -> UserContact -> m ()
|
||||||
processUserContactRequest agentMsg conn UserContact {userContactLinkId} = case agentMsg of
|
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
|
||||||
REQ invId _ connInfo -> do
|
REQ invId _ connInfo -> do
|
||||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||||
case chatMsgEvent of
|
case chatMsgEvent of
|
||||||
@@ -2332,9 +2370,11 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
XInfo p -> profileContactRequest invId p Nothing
|
XInfo p -> profileContactRequest invId p Nothing
|
||||||
-- TODO show/log error, other events in contact request
|
-- TODO show/log error, other events in contact request
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
MERR _ err -> toView . CRChatError $ ChatErrorAgent err
|
MERR _ err -> do
|
||||||
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
|
incAuthErrCounter connEntity conn err
|
||||||
ERR err -> do
|
ERR err -> do
|
||||||
toView . CRChatError $ ChatErrorAgent err
|
toView . CRChatError $ ChatErrorAgent err (Just connEntity)
|
||||||
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
|
||||||
-- TODO add debugging output
|
-- TODO add debugging output
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
@@ -2363,6 +2403,15 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
showToast (localDisplayName <> "> ") "wants to connect to you"
|
showToast (localDisplayName <> "> ") "wants to connect to you"
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
|
incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> m ()
|
||||||
|
incAuthErrCounter connEntity conn err = do
|
||||||
|
case err of
|
||||||
|
SMP SMP.AUTH -> do
|
||||||
|
authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn
|
||||||
|
when (authErrCounter' >= authErrDisableCount) $ do
|
||||||
|
toView $ CRConnectionDisabled connEntity
|
||||||
|
_ -> pure ()
|
||||||
|
|
||||||
updateChatLock :: MsgEncodingI e => String -> ChatMsgEvent e -> m ()
|
updateChatLock :: MsgEncodingI e => String -> ChatMsgEvent e -> m ()
|
||||||
updateChatLock name event = do
|
updateChatLock name event = do
|
||||||
l <- asks chatLock
|
l <- asks chatLock
|
||||||
@@ -2398,11 +2447,15 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage =
|
|||||||
|
|
||||||
ackMsgDeliveryEvent :: Connection -> CommandId -> m ()
|
ackMsgDeliveryEvent :: Connection -> CommandId -> m ()
|
||||||
ackMsgDeliveryEvent Connection {connId} ackCmdId =
|
ackMsgDeliveryEvent Connection {connId} ackCmdId =
|
||||||
withStore' $ \db -> createRcvMsgDeliveryEvent db connId ackCmdId MDSRcvAcknowledged
|
withStoreCtx'
|
||||||
|
(Just $ "createRcvMsgDeliveryEvent, connId: " <> show connId <> ", ackCmdId: " <> show ackCmdId <> ", msgDeliveryStatus: MDSRcvAcknowledged")
|
||||||
|
$ \db -> createRcvMsgDeliveryEvent db connId ackCmdId MDSRcvAcknowledged
|
||||||
|
|
||||||
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m ()
|
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> m ()
|
||||||
sentMsgDeliveryEvent Connection {connId} msgId =
|
sentMsgDeliveryEvent Connection {connId} msgId =
|
||||||
withStore $ \db -> createSndMsgDeliveryEvent db connId msgId MDSSndSent
|
withStoreCtx
|
||||||
|
(Just $ "createSndMsgDeliveryEvent, connId: " <> show connId <> ", msgId: " <> show msgId <> ", msgDeliveryStatus: MDSSndSent")
|
||||||
|
$ \db -> createSndMsgDeliveryEvent db connId msgId MDSSndSent
|
||||||
|
|
||||||
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd
|
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd
|
||||||
agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth
|
agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth
|
||||||
@@ -3215,8 +3268,7 @@ getFileHandle fileId filePath files ioMode = do
|
|||||||
maybe (newHandle fs) pure h_
|
maybe (newHandle fs) pure h_
|
||||||
where
|
where
|
||||||
newHandle fs = do
|
newHandle fs = do
|
||||||
-- TODO handle errors
|
h <- liftIO (openFile filePath ioMode) `E.catch` (throwChatError . CEFileInternal . (show :: E.SomeException -> String))
|
||||||
h <- liftIO (openFile filePath ioMode)
|
|
||||||
atomically . modifyTVar fs $ M.insert fileId h
|
atomically . modifyTVar fs $ M.insert fileId h
|
||||||
pure h
|
pure h
|
||||||
|
|
||||||
@@ -3276,13 +3328,14 @@ deleteOrUpdateMemberRecord user@User {userId} member =
|
|||||||
Nothing -> deleteGroupMember db user member
|
Nothing -> deleteGroupMember db user member
|
||||||
|
|
||||||
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
|
sendDirectContactMessage :: (MsgEncodingI e, ChatMonad m) => Contact -> ChatMsgEvent e -> m (SndMessage, Int64)
|
||||||
sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent = do
|
sendDirectContactMessage ct@Contact {activeConn = conn@Connection {connId, connStatus}} chatMsgEvent
|
||||||
if connStatus == ConnReady || connStatus == ConnSndReady
|
| connStatus /= ConnReady && connStatus /= ConnSndReady = throwChatError $ CEContactNotReady ct
|
||||||
then sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
| connDisabled conn = throwChatError $ CEContactDisabled ct
|
||||||
else throwChatError $ CEContactNotReady ct
|
| otherwise = sendDirectMessage conn chatMsgEvent (ConnectionId connId)
|
||||||
|
|
||||||
sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64)
|
sendDirectMessage :: (MsgEncodingI e, ChatMonad m) => Connection -> ChatMsgEvent e -> ConnOrGroupId -> m (SndMessage, Int64)
|
||||||
sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
sendDirectMessage conn chatMsgEvent connOrGroupId = do
|
||||||
|
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
|
||||||
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId
|
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent connOrGroupId
|
||||||
(msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
(msg,) <$> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId
|
||||||
|
|
||||||
@@ -3301,7 +3354,9 @@ deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do
|
|||||||
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
|
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
|
||||||
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
|
agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody
|
||||||
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId}
|
||||||
withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId
|
withStoreCtx'
|
||||||
|
(Just $ "createSndMsgDelivery, sndMsgDelivery: " <> show sndMsgDelivery <> ", msgId: " <> show msgId <> ", cmEventTag: " <> show cmEventTag <> ", msgDeliveryStatus: MDSSndAgent")
|
||||||
|
$ \db -> createSndMsgDelivery db sndMsgDelivery msgId
|
||||||
|
|
||||||
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m SndMessage
|
sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m SndMessage
|
||||||
sendGroupMessage GroupInfo {groupId} members chatMsgEvent =
|
sendGroupMessage GroupInfo {groupId} members chatMsgEvent =
|
||||||
@@ -3309,31 +3364,36 @@ sendGroupMessage GroupInfo {groupId} members chatMsgEvent =
|
|||||||
|
|
||||||
sendGroupMessage' :: (MsgEncodingI e, ChatMonad m) => [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m SndMessage
|
sendGroupMessage' :: (MsgEncodingI e, ChatMonad m) => [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m SndMessage
|
||||||
sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do
|
sendGroupMessage' members chatMsgEvent groupId introId_ postDeliver = do
|
||||||
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId)
|
msg <- createSndMessage chatMsgEvent (GroupId groupId)
|
||||||
-- TODO collect failed deliveries into a single error
|
-- TODO collect failed deliveries into a single error
|
||||||
forM_ (filter memberCurrent members) $ \m@GroupMember {groupMemberId} ->
|
forM_ (filter memberCurrent members) $ \m ->
|
||||||
case memberConn m of
|
messageMember m msg `catchError` (toView . CRChatError)
|
||||||
|
pure msg
|
||||||
|
where
|
||||||
|
messageMember m@GroupMember {groupMemberId} SndMessage {msgId, msgBody} = case memberConn m of
|
||||||
Nothing -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
Nothing -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||||
Just conn@Connection {connStatus}
|
Just conn@Connection {connStatus}
|
||||||
|
| connDisabled conn || connStatus == ConnDeleted -> pure ()
|
||||||
| connStatus == ConnSndReady || connStatus == ConnReady -> do
|
| connStatus == ConnSndReady || connStatus == ConnReady -> do
|
||||||
let tag = toCMEventTag chatMsgEvent
|
let tag = toCMEventTag chatMsgEvent
|
||||||
(deliverMessage conn tag msgBody msgId >> postDeliver) `catchError` const (pure ())
|
deliverMessage conn tag msgBody msgId >> postDeliver
|
||||||
| connStatus == ConnDeleted -> pure ()
|
|
||||||
| otherwise -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
| otherwise -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_
|
||||||
pure msg
|
|
||||||
|
|
||||||
sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m ()
|
sendPendingGroupMessages :: ChatMonad m => GroupMember -> Connection -> m ()
|
||||||
sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do
|
sendPendingGroupMessages GroupMember {groupMemberId, localDisplayName} conn = do
|
||||||
pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
|
pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
|
||||||
-- TODO ensure order - pending messages interleave with user input messages
|
-- TODO ensure order - pending messages interleave with user input messages
|
||||||
forM_ pendingMessages $ \PendingGroupMessage {msgId, cmEventTag = ACMEventTag _ tag, msgBody, introId_} -> do
|
forM_ pendingMessages $ \pgm ->
|
||||||
void $ deliverMessage conn tag msgBody msgId
|
processPendingMessage pgm `catchError` (toView . CRChatError)
|
||||||
withStore' $ \db -> deletePendingGroupMessage db groupMemberId msgId
|
where
|
||||||
case tag of
|
processPendingMessage PendingGroupMessage {msgId, cmEventTag = ACMEventTag _ tag, msgBody, introId_} = do
|
||||||
XGrpMemFwd_ -> case introId_ of
|
void $ deliverMessage conn tag msgBody msgId
|
||||||
Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded
|
withStore' $ \db -> deletePendingGroupMessage db groupMemberId msgId
|
||||||
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
|
case tag of
|
||||||
_ -> pure ()
|
XGrpMemFwd_ -> case introId_ of
|
||||||
|
Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded
|
||||||
|
_ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName
|
||||||
|
_ -> pure ()
|
||||||
|
|
||||||
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m RcvMessage
|
saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m RcvMessage
|
||||||
saveRcvMSG Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
|
saveRcvMSG Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do
|
||||||
@@ -3341,7 +3401,9 @@ saveRcvMSG Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId
|
|||||||
let agentMsgId = fst $ recipient agentMsgMeta
|
let agentMsgId = fst $ recipient agentMsgMeta
|
||||||
newMsg = NewMessage {chatMsgEvent, msgBody}
|
newMsg = NewMessage {chatMsgEvent, msgBody}
|
||||||
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
|
rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId}
|
||||||
withStore' $ \db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery
|
withStoreCtx'
|
||||||
|
(Just $ "createNewMessageAndRcvMsgDelivery, rcvMsgDelivery: " <> show rcvMsgDelivery <> ", sharedMsgId_: " <> show sharedMsgId_ <> ", msgDeliveryStatus: MDSRcvAgent")
|
||||||
|
$ \db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery
|
||||||
|
|
||||||
saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> m (ChatItem c 'MDSnd)
|
saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> m (ChatItem c 'MDSnd)
|
||||||
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False
|
saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False
|
||||||
@@ -3604,22 +3666,25 @@ withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a
|
|||||||
withAgent action =
|
withAgent action =
|
||||||
asks smpAgent
|
asks smpAgent
|
||||||
>>= runExceptT . action
|
>>= runExceptT . action
|
||||||
>>= liftEither . first ChatErrorAgent
|
>>= liftEither . first (\e -> ChatErrorAgent e Nothing)
|
||||||
|
|
||||||
withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a
|
withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a
|
||||||
withStore' action = withStore $ liftIO . action
|
withStore' action = withStore $ liftIO . action
|
||||||
|
|
||||||
withStore ::
|
withStore :: ChatMonad m => (DB.Connection -> ExceptT StoreError IO a) -> m a
|
||||||
ChatMonad m =>
|
withStore = withStoreCtx Nothing
|
||||||
(DB.Connection -> ExceptT StoreError IO a) ->
|
|
||||||
m a
|
withStoreCtx' :: ChatMonad m => Maybe String -> (DB.Connection -> IO a) -> m a
|
||||||
withStore action = do
|
withStoreCtx' ctx_ action = withStoreCtx ctx_ $ liftIO . action
|
||||||
|
|
||||||
|
withStoreCtx :: ChatMonad m => Maybe String -> (DB.Connection -> ExceptT StoreError IO a) -> m a
|
||||||
|
withStoreCtx ctx_ action = do
|
||||||
ChatController {chatStore} <- ask
|
ChatController {chatStore} <- ask
|
||||||
liftEitherError ChatErrorStore $
|
liftEitherError ChatErrorStore $
|
||||||
withTransaction chatStore (runExceptT . action) `E.catch` handleInternal
|
withTransaction chatStore (runExceptT . action) `E.catch` handleInternal
|
||||||
where
|
where
|
||||||
handleInternal :: E.SomeException -> IO (Either StoreError a)
|
handleInternal :: E.SomeException -> IO (Either StoreError a)
|
||||||
handleInternal = pure . Left . SEInternalError . show
|
handleInternal e = pure . Left . SEInternalError $ show e <> maybe "" (\ctx -> " (" <> ctx <> ")") ctx_
|
||||||
|
|
||||||
chatCommandP :: Parser ChatCommand
|
chatCommandP :: Parser ChatCommand
|
||||||
chatCommandP =
|
chatCommandP =
|
||||||
@@ -3709,10 +3774,14 @@ chatCommandP =
|
|||||||
"/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal),
|
"/_get code #" *> (APIGetGroupMemberCode <$> A.decimal <* A.space <*> A.decimal),
|
||||||
"/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> textP)),
|
"/_verify code @" *> (APIVerifyContact <$> A.decimal <*> optional (A.space *> textP)),
|
||||||
"/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> textP)),
|
"/_verify code #" *> (APIVerifyGroupMember <$> A.decimal <* A.space <*> A.decimal <*> optional (A.space *> textP)),
|
||||||
|
"/_enable @" *> (APIEnableContact <$> A.decimal),
|
||||||
|
"/_enable #" *> (APIEnableGroupMember <$> A.decimal <* A.space <*> A.decimal),
|
||||||
"/code " *> char_ '@' *> (GetContactCode <$> displayName),
|
"/code " *> char_ '@' *> (GetContactCode <$> displayName),
|
||||||
"/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* char_ '@' <*> displayName),
|
"/code #" *> (GetGroupMemberCode <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||||
"/verify " *> char_ '@' *> (VerifyContact <$> displayName <*> optional (A.space *> textP)),
|
"/verify " *> char_ '@' *> (VerifyContact <$> displayName <*> optional (A.space *> textP)),
|
||||||
"/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> optional (A.space *> textP)),
|
"/verify #" *> (VerifyGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> optional (A.space *> textP)),
|
||||||
|
"/enable " *> char_ '@' *> (EnableContact <$> displayName),
|
||||||
|
"/enable #" *> (EnableGroupMember <$> displayName <* A.space <* char_ '@' <*> displayName),
|
||||||
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
|
("/help files" <|> "/help file" <|> "/hf") $> ChatHelp HSFiles,
|
||||||
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
|
("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups,
|
||||||
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
|
("/help address" <|> "/ha") $> ChatHelp HSMyAddress,
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ data ChatCommand
|
|||||||
| APIGetGroupMemberCode GroupId GroupMemberId
|
| APIGetGroupMemberCode GroupId GroupMemberId
|
||||||
| APIVerifyContact ContactId (Maybe Text)
|
| APIVerifyContact ContactId (Maybe Text)
|
||||||
| APIVerifyGroupMember GroupId GroupMemberId (Maybe Text)
|
| APIVerifyGroupMember GroupId GroupMemberId (Maybe Text)
|
||||||
|
| APIEnableContact ContactId
|
||||||
|
| APIEnableGroupMember GroupId GroupMemberId
|
||||||
| ShowMessages ChatName Bool
|
| ShowMessages ChatName Bool
|
||||||
| ContactInfo ContactName
|
| ContactInfo ContactName
|
||||||
| GroupMemberInfo GroupName ContactName
|
| GroupMemberInfo GroupName ContactName
|
||||||
@@ -219,6 +221,8 @@ data ChatCommand
|
|||||||
| GetGroupMemberCode GroupName ContactName
|
| GetGroupMemberCode GroupName ContactName
|
||||||
| VerifyContact ContactName (Maybe Text)
|
| VerifyContact ContactName (Maybe Text)
|
||||||
| VerifyGroupMember GroupName ContactName (Maybe Text)
|
| VerifyGroupMember GroupName ContactName (Maybe Text)
|
||||||
|
| EnableContact ContactName
|
||||||
|
| EnableGroupMember GroupName ContactName
|
||||||
| ChatHelp HelpSection
|
| ChatHelp HelpSection
|
||||||
| Welcome
|
| Welcome
|
||||||
| AddContact
|
| AddContact
|
||||||
@@ -414,6 +418,7 @@ data ChatResponse
|
|||||||
| CRSQLResult {rows :: [Text]}
|
| CRSQLResult {rows :: [Text]}
|
||||||
| CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks}
|
| CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks}
|
||||||
| CRAgentStats {agentStats :: [[String]]}
|
| CRAgentStats {agentStats :: [[String]]}
|
||||||
|
| CRConnectionDisabled {connectionEntity :: ConnectionEntity}
|
||||||
| CRMessageError {severity :: Text, errorMessage :: Text}
|
| CRMessageError {severity :: Text, errorMessage :: Text}
|
||||||
| CRChatCmdError {chatError :: ChatError}
|
| CRChatCmdError {chatError :: ChatError}
|
||||||
| CRChatError {chatError :: ChatError}
|
| CRChatError {chatError :: ChatError}
|
||||||
@@ -540,7 +545,7 @@ tmeToPref currentTTL tme = uncurry TimedMessagesPreference $ case tme of
|
|||||||
|
|
||||||
data ChatError
|
data ChatError
|
||||||
= ChatError {errorType :: ChatErrorType}
|
= ChatError {errorType :: ChatErrorType}
|
||||||
| ChatErrorAgent {agentError :: AgentErrorType}
|
| ChatErrorAgent {agentError :: AgentErrorType, connectionEntity_ :: Maybe ConnectionEntity}
|
||||||
| ChatErrorStore {storeError :: StoreError}
|
| ChatErrorStore {storeError :: StoreError}
|
||||||
| ChatErrorDatabase {databaseError :: DatabaseError}
|
| ChatErrorDatabase {databaseError :: DatabaseError}
|
||||||
deriving (Show, Exception, Generic)
|
deriving (Show, Exception, Generic)
|
||||||
@@ -558,6 +563,8 @@ data ChatErrorType
|
|||||||
| CEInvalidConnReq
|
| CEInvalidConnReq
|
||||||
| CEInvalidChatMessage {message :: String}
|
| CEInvalidChatMessage {message :: String}
|
||||||
| CEContactNotReady {contact :: Contact}
|
| CEContactNotReady {contact :: Contact}
|
||||||
|
| CEContactDisabled {contact :: Contact}
|
||||||
|
| CEConnectionDisabled {connection :: Connection}
|
||||||
| CEGroupUserRole
|
| CEGroupUserRole
|
||||||
| CEContactIncognitoCantInvite
|
| CEContactIncognitoCantInvite
|
||||||
| CEGroupIncognitoCantInvite
|
| CEGroupIncognitoCantInvite
|
||||||
|
|||||||
@@ -1157,6 +1157,7 @@ data SndMsgDelivery = SndMsgDelivery
|
|||||||
{ connId :: Int64,
|
{ connId :: Int64,
|
||||||
agentMsgId :: AgentMsgId
|
agentMsgId :: AgentMsgId
|
||||||
}
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
data RcvMsgDelivery = RcvMsgDelivery
|
data RcvMsgDelivery = RcvMsgDelivery
|
||||||
{ connId :: Int64,
|
{ connId :: Int64,
|
||||||
@@ -1164,6 +1165,7 @@ data RcvMsgDelivery = RcvMsgDelivery
|
|||||||
agentMsgMeta :: MsgMeta,
|
agentMsgMeta :: MsgMeta,
|
||||||
agentAckCmdId :: CommandId
|
agentAckCmdId :: CommandId
|
||||||
}
|
}
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
data MsgMetaJSON = MsgMetaJSON
|
data MsgMetaJSON = MsgMetaJSON
|
||||||
{ integrity :: Text,
|
{ integrity :: Text,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20230107_connections_auth_err_counter where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20230107_connections_auth_err_counter :: Query
|
||||||
|
m20230107_connections_auth_err_counter =
|
||||||
|
[sql|
|
||||||
|
PRAGMA ignore_check_constraints=ON;
|
||||||
|
|
||||||
|
ALTER TABLE connections ADD COLUMN auth_err_counter INTEGER DEFAULT 0 CHECK (auth_err_counter NOT NULL);
|
||||||
|
UPDATE connections SET auth_err_counter = 0;
|
||||||
|
|
||||||
|
PRAGMA ignore_check_constraints=OFF;
|
||||||
|
|]
|
||||||
@@ -263,6 +263,7 @@ CREATE TABLE connections(
|
|||||||
group_link_id BLOB,
|
group_link_id BLOB,
|
||||||
security_code TEXT NULL,
|
security_code TEXT NULL,
|
||||||
security_code_verified_at TEXT NULL,
|
security_code_verified_at TEXT NULL,
|
||||||
|
auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL),
|
||||||
FOREIGN KEY(snd_file_id, connection_id)
|
FOREIGN KEY(snd_file_id, connection_id)
|
||||||
REFERENCES snd_files(file_id, connection_id)
|
REFERENCES snd_files(file_id, connection_id)
|
||||||
ON DELETE CASCADE
|
ON DELETE CASCADE
|
||||||
|
|||||||
+42
-25
@@ -48,6 +48,8 @@ module Simplex.Chat.Store
|
|||||||
updateContactUnreadChat,
|
updateContactUnreadChat,
|
||||||
updateGroupUnreadChat,
|
updateGroupUnreadChat,
|
||||||
setConnectionVerified,
|
setConnectionVerified,
|
||||||
|
incConnectionAuthErrCounter,
|
||||||
|
setConnectionAuthErrCounter,
|
||||||
getUserContacts,
|
getUserContacts,
|
||||||
getUserContactProfiles,
|
getUserContactProfiles,
|
||||||
createUserContactLink,
|
createUserContactLink,
|
||||||
@@ -324,6 +326,7 @@ import Simplex.Chat.Migrations.M20221214_live_message
|
|||||||
import Simplex.Chat.Migrations.M20221222_chat_ts
|
import Simplex.Chat.Migrations.M20221222_chat_ts
|
||||||
import Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status
|
import Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status
|
||||||
import Simplex.Chat.Migrations.M20221230_idxs
|
import Simplex.Chat.Migrations.M20221230_idxs
|
||||||
|
import Simplex.Chat.Migrations.M20230107_connections_auth_err_counter
|
||||||
import Simplex.Chat.Protocol
|
import Simplex.Chat.Protocol
|
||||||
import Simplex.Chat.Types
|
import Simplex.Chat.Types
|
||||||
import Simplex.Chat.Util (week)
|
import Simplex.Chat.Util (week)
|
||||||
@@ -383,7 +386,8 @@ schemaMigrations =
|
|||||||
("20221214_live_message", m20221214_live_message),
|
("20221214_live_message", m20221214_live_message),
|
||||||
("20221222_chat_ts", m20221222_chat_ts),
|
("20221222_chat_ts", m20221222_chat_ts),
|
||||||
("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status),
|
("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status),
|
||||||
("20221230_idxs", m20221230_idxs)
|
("20221230_idxs", m20221230_idxs),
|
||||||
|
("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
@@ -495,7 +499,7 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||||
JOIN connections c ON c.contact_id = ct.contact_id
|
JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
@@ -570,7 +574,7 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink
|
|||||||
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
|
:. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs)
|
||||||
)
|
)
|
||||||
connId <- insertedRowId db
|
connId <- insertedRowId db
|
||||||
pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing}
|
pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0}
|
||||||
where
|
where
|
||||||
ent ct = if connType == ct then entityId else Nothing
|
ent ct = if connType == ct then entityId else Nothing
|
||||||
|
|
||||||
@@ -764,6 +768,19 @@ setConnectionVerified db User {userId} connId code = do
|
|||||||
updatedAt <- getCurrentTime
|
updatedAt <- getCurrentTime
|
||||||
DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId)
|
DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId)
|
||||||
|
|
||||||
|
incConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int
|
||||||
|
incConnectionAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do
|
||||||
|
updatedAt <- getCurrentTime
|
||||||
|
(counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId)
|
||||||
|
let counter' = fromMaybe authErrCounter counter_ + 1
|
||||||
|
DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId)
|
||||||
|
pure counter'
|
||||||
|
|
||||||
|
setConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO ()
|
||||||
|
setConnectionAuthErrCounter db User {userId} Connection {connId} counter = do
|
||||||
|
updatedAt <- getCurrentTime
|
||||||
|
DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId)
|
||||||
|
|
||||||
updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO ()
|
updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO ()
|
||||||
updateContactProfile_ db userId profileId profile = do
|
updateContactProfile_ db userId profileId profile = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
@@ -861,7 +878,7 @@ getUserAddressConnections db User {userId} = do
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||||
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
|
WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL
|
||||||
@@ -875,7 +892,7 @@ getUserContactLinks db User {userId} =
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at,
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||||
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
|
uc.user_contact_link_id, uc.conn_req_contact, uc.group_id
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||||
@@ -1008,7 +1025,7 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} =
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id
|
||||||
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
|
WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ?
|
||||||
@@ -1112,7 +1129,7 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
@@ -1250,7 +1267,7 @@ getLiveSndFileTransfers db User {userId} = do
|
|||||||
FROM files f
|
FROM files f
|
||||||
JOIN snd_files s USING (file_id)
|
JOIN snd_files s USING (file_id)
|
||||||
WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) AND s.file_inline IS NULL
|
WHERE f.user_id = ? AND s.file_status IN (?, ?, ?) AND s.file_inline IS NULL
|
||||||
AND created_at > ?
|
AND s.created_at > ?
|
||||||
|]
|
|]
|
||||||
(userId, FSNew, FSAccepted, FSConnected, cutoffTs)
|
(userId, FSNew, FSAccepted, FSConnected, cutoffTs)
|
||||||
concatMap (filter liveTransfer) . rights <$> mapM (getSndFileTransfers_ db userId) fileIds
|
concatMap (filter liveTransfer) . rights <$> mapM (getSndFileTransfers_ db userId) fileIds
|
||||||
@@ -1270,7 +1287,7 @@ getLiveRcvFileTransfers db user@User {userId} = do
|
|||||||
FROM files f
|
FROM files f
|
||||||
JOIN rcv_files r USING (file_id)
|
JOIN rcv_files r USING (file_id)
|
||||||
WHERE f.user_id = ? AND r.file_status IN (?, ?) AND r.rcv_file_inline IS NULL
|
WHERE f.user_id = ? AND r.file_status IN (?, ?) AND r.rcv_file_inline IS NULL
|
||||||
AND created_at > ?
|
AND r.created_at > ?
|
||||||
|]
|
|]
|
||||||
(userId, FSAccepted, FSConnected, cutoffTs)
|
(userId, FSAccepted, FSConnected, cutoffTs)
|
||||||
rights <$> mapM (runExceptT . getRcvFileTransfer db user) fileIds
|
rights <$> mapM (runExceptT . getRcvFileTransfer db user) fileIds
|
||||||
@@ -1311,7 +1328,7 @@ getContactConnections db userId Contact {contactId} =
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM connections c
|
FROM connections c
|
||||||
JOIN contacts ct ON ct.contact_id = c.contact_id
|
JOIN contacts ct ON ct.contact_id = c.contact_id
|
||||||
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
|
WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ?
|
||||||
@@ -1322,15 +1339,15 @@ getContactConnections db userId Contact {contactId} =
|
|||||||
|
|
||||||
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
|
type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64)
|
||||||
|
|
||||||
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime)
|
type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int)
|
||||||
|
|
||||||
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime)
|
type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int)
|
||||||
|
|
||||||
toConnection :: ConnectionRow -> Connection
|
toConnection :: ConnectionRow -> Connection
|
||||||
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_)) =
|
toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter)) =
|
||||||
let entityId = entityId_ connType
|
let entityId = entityId_ connType
|
||||||
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
|
connectionCode = SecurityCode <$> code_ <*> verifiedAt_
|
||||||
in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, createdAt}
|
in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt}
|
||||||
where
|
where
|
||||||
entityId_ :: ConnType -> Maybe Int64
|
entityId_ :: ConnType -> Maybe Int64
|
||||||
entityId_ ConnContact = contactId
|
entityId_ ConnContact = contactId
|
||||||
@@ -1340,8 +1357,8 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup
|
|||||||
entityId_ ConnUserContact = userContactLinkId
|
entityId_ ConnUserContact = userContactLinkId
|
||||||
|
|
||||||
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
|
toMaybeConnection :: MaybeConnectionRow -> Maybe Connection
|
||||||
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_)) =
|
toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter)) =
|
||||||
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_))
|
Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter))
|
||||||
toMaybeConnection _ = Nothing
|
toMaybeConnection _ = Nothing
|
||||||
|
|
||||||
getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact]
|
getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact]
|
||||||
@@ -1512,7 +1529,7 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||||
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at
|
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter
|
||||||
FROM connections
|
FROM connections
|
||||||
WHERE user_id = ? AND agent_conn_id = ?
|
WHERE user_id = ? AND agent_conn_id = ?
|
||||||
|]
|
|]
|
||||||
@@ -1612,7 +1629,7 @@ getConnectionById db User {userId} connId = ExceptT $ do
|
|||||||
db
|
db
|
||||||
[sql|
|
[sql|
|
||||||
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
|
||||||
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at
|
conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter
|
||||||
FROM connections
|
FROM connections
|
||||||
WHERE user_id = ? AND connection_id = ?
|
WHERE user_id = ? AND connection_id = ?
|
||||||
|]
|
|]
|
||||||
@@ -1657,7 +1674,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId =
|
|||||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM group_members m
|
FROM group_members m
|
||||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||||
JOIN groups g ON g.group_id = m.group_id
|
JOIN groups g ON g.group_id = m.group_id
|
||||||
@@ -1937,7 +1954,7 @@ groupMemberQuery =
|
|||||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM group_members m
|
FROM group_members m
|
||||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||||
LEFT JOIN connections c ON c.connection_id = (
|
LEFT JOIN connections c ON c.connection_id = (
|
||||||
@@ -2097,7 +2114,7 @@ getContactViaMember db user@User {userId} GroupMember {groupMemberId} =
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
|
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
|
||||||
JOIN connections c ON c.connection_id = (
|
JOIN connections c ON c.connection_id = (
|
||||||
@@ -2402,7 +2419,7 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} =
|
|||||||
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status,
|
||||||
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.local_alias, p.preferences,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM group_members m
|
FROM group_members m
|
||||||
JOIN contacts ct ON ct.contact_id = m.contact_id
|
JOIN contacts ct ON ct.contact_id = m.contact_id
|
||||||
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
|
||||||
@@ -2435,7 +2452,7 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} =
|
|||||||
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs,
|
ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs,
|
||||||
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id,
|
||||||
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
|
JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id
|
||||||
JOIN connections c ON c.connection_id = (
|
JOIN connections c ON c.connection_id = (
|
||||||
@@ -3350,7 +3367,7 @@ getDirectChatPreviews_ db user@User {userId} = do
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at,
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter,
|
||||||
-- ChatStats
|
-- ChatStats
|
||||||
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
|
COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat,
|
||||||
-- ChatItem
|
-- ChatItem
|
||||||
@@ -3673,7 +3690,7 @@ getContact db user@User {userId} contactId =
|
|||||||
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts,
|
||||||
-- Connection
|
-- Connection
|
||||||
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias,
|
||||||
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at
|
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter
|
||||||
FROM contacts ct
|
FROM contacts ct
|
||||||
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
|
||||||
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
LEFT JOIN connections c ON c.contact_id = ct.contact_id
|
||||||
|
|||||||
@@ -1603,10 +1603,17 @@ data Connection = Connection
|
|||||||
localAlias :: Text,
|
localAlias :: Text,
|
||||||
entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID
|
entityId :: Maybe Int64, -- contact, group member, file ID or user contact ID
|
||||||
connectionCode :: Maybe SecurityCode,
|
connectionCode :: Maybe SecurityCode,
|
||||||
|
authErrCounter :: Int,
|
||||||
createdAt :: UTCTime
|
createdAt :: UTCTime
|
||||||
}
|
}
|
||||||
deriving (Eq, Show, Generic)
|
deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
authErrDisableCount :: Int
|
||||||
|
authErrDisableCount = 10
|
||||||
|
|
||||||
|
connDisabled :: Connection -> Bool
|
||||||
|
connDisabled Connection {authErrCounter} = authErrCounter >= authErrDisableCount
|
||||||
|
|
||||||
data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime}
|
data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime}
|
||||||
deriving (Eq, Show, Generic)
|
deriving (Eq, Show, Generic)
|
||||||
|
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ responseToView user_ testView liveItems ts = \case
|
|||||||
plain $ "agent locks: " <> LB.unpack (J.encode agentLocks)
|
plain $ "agent locks: " <> LB.unpack (J.encode agentLocks)
|
||||||
]
|
]
|
||||||
CRAgentStats stats -> map (plain . intercalate ",") stats
|
CRAgentStats stats -> map (plain . intercalate ",") stats
|
||||||
|
CRConnectionDisabled entity -> viewConnectionEntityDisabled entity
|
||||||
CRMessageError prefix err -> [plain prefix <> ": " <> plain err]
|
CRMessageError prefix err -> [plain prefix <> ": " <> plain err]
|
||||||
CRChatError e -> viewChatError e
|
CRChatError e -> viewChatError e
|
||||||
where
|
where
|
||||||
@@ -1137,6 +1138,8 @@ viewChatError = \case
|
|||||||
CEInvalidConnReq -> viewInvalidConnReq
|
CEInvalidConnReq -> viewInvalidConnReq
|
||||||
CEInvalidChatMessage e -> ["chat message error: " <> sShow e]
|
CEInvalidChatMessage e -> ["chat message error: " <> sShow e]
|
||||||
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
||||||
|
CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
||||||
|
CEConnectionDisabled _ -> []
|
||||||
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
|
CEGroupDuplicateMember c -> ["contact " <> ttyContact c <> " is already in the group"]
|
||||||
CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"]
|
CEGroupDuplicateMemberId -> ["cannot add member - duplicate member ID"]
|
||||||
CEGroupUserRole -> ["you have insufficient permissions for this group command"]
|
CEGroupUserRole -> ["you have insufficient permissions for this group command"]
|
||||||
@@ -1201,21 +1204,56 @@ viewChatError = \case
|
|||||||
DBErrorExport e -> ["error encrypting database: " <> sqliteError' e]
|
DBErrorExport e -> ["error encrypting database: " <> sqliteError' e]
|
||||||
DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e]
|
DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e]
|
||||||
e -> ["chat database error: " <> sShow e]
|
e -> ["chat database error: " <> sShow e]
|
||||||
ChatErrorAgent err -> case err of
|
ChatErrorAgent err entity_ -> case err of
|
||||||
SMP SMP.AUTH ->
|
SMP SMP.AUTH ->
|
||||||
[ "error: connection authorization failed - this could happen if connection was deleted,\
|
[ withConnEntity
|
||||||
\ secured with different credentials, or due to a bug - please re-create the connection"
|
<> "error: connection authorization failed - this could happen if connection was deleted,\
|
||||||
|
\ secured with different credentials, or due to a bug - please re-create the connection"
|
||||||
]
|
]
|
||||||
AGENT A_DUPLICATE -> []
|
AGENT A_DUPLICATE -> []
|
||||||
AGENT A_PROHIBITED -> []
|
AGENT A_PROHIBITED -> []
|
||||||
CONN NOT_FOUND -> []
|
CONN NOT_FOUND -> []
|
||||||
e -> ["smp agent error: " <> sShow e]
|
e -> [withConnEntity <> "smp agent error: " <> sShow e]
|
||||||
|
where
|
||||||
|
withConnEntity = case entity_ of
|
||||||
|
Just entity@(RcvDirectMsgConnection conn contact_) -> case contact_ of
|
||||||
|
Just Contact {contactId} ->
|
||||||
|
"[" <> connEntityLabel entity <> ", contactId: " <> sShow contactId <> ", connId: " <> cId conn <> "] "
|
||||||
|
Nothing ->
|
||||||
|
"[" <> connEntityLabel entity <> ", connId: " <> cId conn <> "] "
|
||||||
|
Just entity@(RcvGroupMsgConnection conn GroupInfo {groupId} GroupMember {groupMemberId}) ->
|
||||||
|
"[" <> connEntityLabel entity <> ", groupId: " <> sShow groupId <> ", memberId: " <> sShow groupMemberId <> ", connId: " <> cId conn <> "] "
|
||||||
|
Just entity@(RcvFileConnection conn RcvFileTransfer {fileId}) ->
|
||||||
|
"[" <> connEntityLabel entity <> ", fileId: " <> sShow fileId <> ", connId: " <> cId conn <> "] "
|
||||||
|
Just entity@(SndFileConnection conn SndFileTransfer {fileId}) ->
|
||||||
|
"[" <> connEntityLabel entity <> ", fileId: " <> sShow fileId <> ", connId: " <> cId conn <> "] "
|
||||||
|
Just entity@(UserContactConnection conn UserContact {userContactLinkId}) ->
|
||||||
|
"[" <> connEntityLabel entity <> ", userContactLinkId: " <> sShow userContactLinkId <> ", connId: " <> cId conn <> "] "
|
||||||
|
Nothing -> ""
|
||||||
|
cId conn = sShow (connId (conn :: Connection))
|
||||||
where
|
where
|
||||||
fileNotFound fileId = ["file " <> sShow fileId <> " not found"]
|
fileNotFound fileId = ["file " <> sShow fileId <> " not found"]
|
||||||
sqliteError' = \case
|
sqliteError' = \case
|
||||||
SQLiteErrorNotADatabase -> "wrong passphrase or invalid database file"
|
SQLiteErrorNotADatabase -> "wrong passphrase or invalid database file"
|
||||||
SQLiteError e -> sShow e
|
SQLiteError e -> sShow e
|
||||||
|
|
||||||
|
viewConnectionEntityDisabled :: ConnectionEntity -> [StyledString]
|
||||||
|
viewConnectionEntityDisabled entity = case entity of
|
||||||
|
RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
||||||
|
RcvGroupMsgConnection _ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} -> ["[" <> entityLabel <> "] connection is disabled, to enable: " <> highlight ("/enable #" <> g <> " " <> m)]
|
||||||
|
_ -> ["[" <> entityLabel <> "] connection is disabled"]
|
||||||
|
where
|
||||||
|
entityLabel = connEntityLabel entity
|
||||||
|
|
||||||
|
connEntityLabel :: ConnectionEntity -> StyledString
|
||||||
|
connEntityLabel = \case
|
||||||
|
RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c
|
||||||
|
RcvDirectMsgConnection _ Nothing -> "rcv direct msg"
|
||||||
|
RcvGroupMsgConnection _ GroupInfo {localDisplayName = g} GroupMember {localDisplayName = m} -> plain $ "#" <> g <> " " <> m
|
||||||
|
RcvFileConnection _ RcvFileTransfer {fileInvitation = FileInvitation {fileName}} -> plain $ "rcv file " <> T.pack fileName
|
||||||
|
SndFileConnection _ SndFileTransfer {fileName} -> plain $ "snd file " <> T.pack fileName
|
||||||
|
UserContactConnection _ UserContact {} -> "contact address"
|
||||||
|
|
||||||
ttyContact :: ContactName -> StyledString
|
ttyContact :: ContactName -> StyledString
|
||||||
ttyContact = styled $ colored Green
|
ttyContact = styled $ colored Green
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ extra-deps:
|
|||||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||||
# - ../simplexmq
|
# - ../simplexmq
|
||||||
- github: simplex-chat/simplexmq
|
- github: simplex-chat/simplexmq
|
||||||
commit: 874f9f5cd67310ddfbe5540d6abfa5943a534f37
|
commit: 058e3ac55e8577280267f9341ccd7d3e971bc51a
|
||||||
# - ../direct-sqlcipher
|
# - ../direct-sqlcipher
|
||||||
- github: simplex-chat/direct-sqlcipher
|
- github: simplex-chat/direct-sqlcipher
|
||||||
commit: 34309410eb2069b029b8fc1872deb1e0db123294
|
commit: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||||
|
|||||||
+103
-1
@@ -57,6 +57,8 @@ chatTests = do
|
|||||||
it "direct message quoted replies" testDirectMessageQuotedReply
|
it "direct message quoted replies" testDirectMessageQuotedReply
|
||||||
it "direct message update" testDirectMessageUpdate
|
it "direct message update" testDirectMessageUpdate
|
||||||
it "direct message delete" testDirectMessageDelete
|
it "direct message delete" testDirectMessageDelete
|
||||||
|
it "direct live message" testDirectLiveMessage
|
||||||
|
it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact
|
||||||
describe "chat groups" $ do
|
describe "chat groups" $ do
|
||||||
describe "add contacts, create group and send/receive messages" testGroup
|
describe "add contacts, create group and send/receive messages" testGroup
|
||||||
it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
|
it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages
|
||||||
@@ -73,6 +75,7 @@ chatTests = do
|
|||||||
it "group message quoted replies" testGroupMessageQuotedReply
|
it "group message quoted replies" testGroupMessageQuotedReply
|
||||||
it "group message update" testGroupMessageUpdate
|
it "group message update" testGroupMessageUpdate
|
||||||
it "group message delete" testGroupMessageDelete
|
it "group message delete" testGroupMessageDelete
|
||||||
|
it "group live message" testGroupLiveMessage
|
||||||
it "update group profile" testUpdateGroupProfile
|
it "update group profile" testUpdateGroupProfile
|
||||||
it "update member role" testUpdateMemberRole
|
it "update member role" testUpdateMemberRole
|
||||||
it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts
|
it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts
|
||||||
@@ -156,10 +159,12 @@ chatTests = do
|
|||||||
-- it "v1 to v2" testFullAsyncV1toV2
|
-- it "v1 to v2" testFullAsyncV1toV2
|
||||||
-- it "v2 to v1" testFullAsyncV2toV1
|
-- it "v2 to v1" testFullAsyncV2toV1
|
||||||
describe "async sending and receiving files" $ do
|
describe "async sending and receiving files" $ do
|
||||||
|
it "send and receive file, sender restarts" testAsyncFileTransferSenderRestarts
|
||||||
|
it "send and receive file, receiver restarts" testAsyncFileTransferReceiverRestarts
|
||||||
xdescribe "send and receive file, fully asynchronous" $ do
|
xdescribe "send and receive file, fully asynchronous" $ do
|
||||||
it "v2" testAsyncFileTransfer
|
it "v2" testAsyncFileTransfer
|
||||||
it "v1" testAsyncFileTransferV1
|
it "v1" testAsyncFileTransferV1
|
||||||
xit "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer
|
it "send and receive file to group, fully asynchronous" testAsyncGroupFileTransfer
|
||||||
describe "webrtc calls api" $ do
|
describe "webrtc calls api" $ do
|
||||||
it "negotiate call" testNegotiateCall
|
it "negotiate call" testNegotiateCall
|
||||||
describe "maintenance mode" $ do
|
describe "maintenance mode" $ do
|
||||||
@@ -495,6 +500,42 @@ testDirectMessageDelete =
|
|||||||
bob #$> ("/_delete item @2 " <> itemId 4 <> " internal", id, "message deleted")
|
bob #$> ("/_delete item @2 " <> itemId 4 <> " internal", id, "message deleted")
|
||||||
bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))])
|
bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))])
|
||||||
|
|
||||||
|
testDirectLiveMessage :: IO ()
|
||||||
|
testDirectLiveMessage =
|
||||||
|
testChat2 aliceProfile bobProfile $ \alice bob -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
-- non-empty live message is sent instantly
|
||||||
|
alice `send` "/live @bob hello"
|
||||||
|
bob <# "alice> [LIVE started] use /show [on/off/4] hello"
|
||||||
|
alice ##> ("/_update item @2 " <> itemId 1 <> " text hello there")
|
||||||
|
alice <# "@bob [LIVE] hello there"
|
||||||
|
bob <# "alice> [LIVE ended] hello there"
|
||||||
|
-- empty live message is also sent instantly
|
||||||
|
alice `send` "/live @bob"
|
||||||
|
bob <# "alice> [LIVE started] use /show [on/off/5]"
|
||||||
|
alice ##> ("/_update item @2 " <> itemId 2 <> " text hello 2")
|
||||||
|
alice <# "@bob [LIVE] hello 2"
|
||||||
|
bob <# "alice> [LIVE ended] hello 2"
|
||||||
|
|
||||||
|
testRepeatAuthErrorsDisableContact :: IO ()
|
||||||
|
testRepeatAuthErrorsDisableContact =
|
||||||
|
testChat2 aliceProfile bobProfile $ \alice bob -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
alice <##> bob
|
||||||
|
bob ##> "/d alice"
|
||||||
|
bob <## "alice: contact is deleted"
|
||||||
|
forM_ [1 .. authErrDisableCount] $ \_ -> sendAuth alice
|
||||||
|
alice <## "[bob] connection is disabled, to enable: /enable bob, to delete: /d bob"
|
||||||
|
alice ##> "@bob hey"
|
||||||
|
alice <## "bob: disabled, to enable: /enable bob, to delete: /d bob"
|
||||||
|
alice ##> "/enable bob"
|
||||||
|
alice <## "ok"
|
||||||
|
sendAuth alice
|
||||||
|
where
|
||||||
|
sendAuth alice = do
|
||||||
|
alice #> "@bob hey"
|
||||||
|
alice <## "[bob, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
|
||||||
|
|
||||||
testGroup :: Spec
|
testGroup :: Spec
|
||||||
testGroup = versionTestMatrix3 runTestGroup
|
testGroup = versionTestMatrix3 runTestGroup
|
||||||
where
|
where
|
||||||
@@ -1004,6 +1045,7 @@ testGroupDeleteInvitedContact =
|
|||||||
alice ##> "@bob hey"
|
alice ##> "@bob hey"
|
||||||
alice <## "no contact bob"
|
alice <## "no contact bob"
|
||||||
bob #> "@alice hey"
|
bob #> "@alice hey"
|
||||||
|
bob <## "[alice, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
|
||||||
(alice </)
|
(alice </)
|
||||||
|
|
||||||
testDeleteGroupMemberProfileKept :: IO ()
|
testDeleteGroupMemberProfileKept :: IO ()
|
||||||
@@ -1055,6 +1097,7 @@ testDeleteGroupMemberProfileKept =
|
|||||||
alice ##> "@bob hey"
|
alice ##> "@bob hey"
|
||||||
alice <## "no contact bob"
|
alice <## "no contact bob"
|
||||||
bob #> "@alice hey"
|
bob #> "@alice hey"
|
||||||
|
bob <## "[alice, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection"
|
||||||
(alice </)
|
(alice </)
|
||||||
-- delete group 1
|
-- delete group 1
|
||||||
alice ##> "/d #team"
|
alice ##> "/d #team"
|
||||||
@@ -1371,6 +1414,30 @@ testGroupMessageDelete =
|
|||||||
bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "how are you? [marked deleted]"), Nothing)])
|
bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "how are you? [marked deleted]"), Nothing)])
|
||||||
cath #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!")), ((1, "how are you? [marked deleted]"), Nothing)])
|
cath #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!")), ((1, "how are you? [marked deleted]"), Nothing)])
|
||||||
|
|
||||||
|
testGroupLiveMessage :: IO ()
|
||||||
|
testGroupLiveMessage =
|
||||||
|
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
|
||||||
|
createGroup3 "team" alice bob cath
|
||||||
|
threadDelay 500000
|
||||||
|
-- non-empty live message is sent instantly
|
||||||
|
alice `send` "/live #team hello"
|
||||||
|
msgItemId1 <- lastItemId alice
|
||||||
|
bob <#. "#team alice> [LIVE started]"
|
||||||
|
cath <#. "#team alice> [LIVE started]"
|
||||||
|
alice ##> ("/_update item #1 " <> msgItemId1 <> " text hello there")
|
||||||
|
alice <# "#team [LIVE] hello there"
|
||||||
|
bob <# "#team alice> [LIVE ended] hello there"
|
||||||
|
cath <# "#team alice> [LIVE ended] hello there"
|
||||||
|
-- empty live message is also sent instantly
|
||||||
|
alice `send` "/live #team"
|
||||||
|
msgItemId2 <- lastItemId alice
|
||||||
|
bob <#. "#team alice> [LIVE started]"
|
||||||
|
cath <#. "#team alice> [LIVE started]"
|
||||||
|
alice ##> ("/_update item #1 " <> msgItemId2 <> " text hello 2")
|
||||||
|
alice <# "#team [LIVE] hello 2"
|
||||||
|
bob <# "#team alice> [LIVE ended] hello 2"
|
||||||
|
cath <# "#team alice> [LIVE ended] hello 2"
|
||||||
|
|
||||||
testUpdateGroupProfile :: IO ()
|
testUpdateGroupProfile :: IO ()
|
||||||
testUpdateGroupProfile =
|
testUpdateGroupProfile =
|
||||||
testChat3 aliceProfile bobProfile cathProfile $
|
testChat3 aliceProfile bobProfile cathProfile $
|
||||||
@@ -3945,6 +4012,34 @@ testFullAsyncV2toV1 = withTmpFiles $ do
|
|||||||
withNewBob = withNewTestChat "bob" bobProfile
|
withNewBob = withNewTestChat "bob" bobProfile
|
||||||
withBob = withTestChat "bob"
|
withBob = withTestChat "bob"
|
||||||
|
|
||||||
|
testAsyncFileTransferSenderRestarts :: IO ()
|
||||||
|
testAsyncFileTransferSenderRestarts = withTmpFiles $ do
|
||||||
|
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||||
|
withNewTestChat "alice" aliceProfile $ \alice -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
startFileTransfer' alice bob "test_1MB.pdf" "1017.7 KiB / 1042157 bytes"
|
||||||
|
threadDelay 100000
|
||||||
|
withTestChatContactConnected "alice" $ \alice -> do
|
||||||
|
alice <## "completed sending file 1 (test_1MB.pdf) to bob"
|
||||||
|
bob <## "completed receiving file 1 (test_1MB.pdf) from alice"
|
||||||
|
src <- B.readFile "./tests/fixtures/test_1MB.pdf"
|
||||||
|
dest <- B.readFile "./tests/tmp/test_1MB.pdf"
|
||||||
|
dest `shouldBe` src
|
||||||
|
|
||||||
|
testAsyncFileTransferReceiverRestarts :: IO ()
|
||||||
|
testAsyncFileTransferReceiverRestarts = withTmpFiles $ do
|
||||||
|
withNewTestChat "alice" aliceProfile $ \alice -> do
|
||||||
|
withNewTestChat "bob" bobProfile $ \bob -> do
|
||||||
|
connectUsers alice bob
|
||||||
|
startFileTransfer' alice bob "test_1MB.pdf" "1017.7 KiB / 1042157 bytes"
|
||||||
|
threadDelay 100000
|
||||||
|
withTestChatContactConnected "bob" $ \bob -> do
|
||||||
|
alice <## "completed sending file 1 (test_1MB.pdf) to bob"
|
||||||
|
bob <## "completed receiving file 1 (test_1MB.pdf) from alice"
|
||||||
|
src <- B.readFile "./tests/fixtures/test_1MB.pdf"
|
||||||
|
dest <- B.readFile "./tests/tmp/test_1MB.pdf"
|
||||||
|
dest `shouldBe` src
|
||||||
|
|
||||||
testAsyncFileTransfer :: IO ()
|
testAsyncFileTransfer :: IO ()
|
||||||
testAsyncFileTransfer = withTmpFiles $ do
|
testAsyncFileTransfer = withTmpFiles $ do
|
||||||
withNewTestChat "alice" aliceProfile $ \alice ->
|
withNewTestChat "alice" aliceProfile $ \alice ->
|
||||||
@@ -5062,6 +5157,13 @@ cc <##. line = do
|
|||||||
unless prefix $ print ("expected to start from: " <> line, ", got: " <> l)
|
unless prefix $ print ("expected to start from: " <> line, ", got: " <> l)
|
||||||
prefix `shouldBe` True
|
prefix `shouldBe` True
|
||||||
|
|
||||||
|
(<#.) :: TestCC -> String -> Expectation
|
||||||
|
cc <#. line = do
|
||||||
|
l <- dropTime <$> getTermLine cc
|
||||||
|
let prefix = line `isPrefixOf` l
|
||||||
|
unless prefix $ print ("expected to start from: " <> line, ", got: " <> l)
|
||||||
|
prefix `shouldBe` True
|
||||||
|
|
||||||
(<##..) :: TestCC -> [String] -> Expectation
|
(<##..) :: TestCC -> [String] -> Expectation
|
||||||
cc <##.. ls = do
|
cc <##.. ls = do
|
||||||
l <- getTermLine cc
|
l <- getTermLine cc
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<p>v4.4 is released:</p>
|
||||||
|
|
||||||
|
<ul class="mb-[12px]">
|
||||||
|
<li>disappearing messages!</li>
|
||||||
|
<li>live messages</li>
|
||||||
|
<li>connection security verification</li>
|
||||||
|
<li>support for GIFs and stickers</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>Also, the app interface is now available in French - thanks to Weblate and our users!</p>
|
||||||
Reference in New Issue
Block a user