Compare commits

...

43 Commits

Author SHA1 Message Date
JRoberts e73f5c40cf 4.4.2: ios 114, Android 88 2023-01-12 18:38:58 +04:00
JRoberts 4c960bdc44 4.4.2 2023-01-12 16:46:28 +04:00
JRoberts dcb82951ed core: catch errors when sending messages in loops where they haven't been previously caught (#1729) 2023-01-12 16:31:27 +04:00
Stanislav Dmitrenko 138cce4436 ios: fix opening via notification returning to chat list when screen lock is enabled (#1725)
* ios: Fixed broken chat push/pop logic

* remove binding parameter

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-11 22:48:52 +00:00
Evgeny Poberezkin 98417dafc4 4.4.1: ios 113, Android 87 2023-01-11 17:54:24 +00:00
Evgeny Poberezkin e8374be19c mobile: set defaults consistently (protected screen: iOS off/Android on, accept images: on, faster image transfer: on) (#1724)
* ios: set defaults consistently (protected screen: off, accept images: on, faster image transfer: on)

* android: transfer images faster by default
2023-01-11 17:09:17 +00:00
Stanislav Dmitrenko 62a2f61751 android: Fix non-unique chat item id in listState (#1723)
* android: Fix non-unique chat item id in listState

* Test

* Revert "Test"

This reverts commit 6625bce138.
2023-01-11 16:41:34 +00:00
Evgeny Poberezkin 2d47175f94 ios: disable reply/edit actions and deletion of live item in live mode (#1722) 2023-01-11 17:29:09 +04:00
Evgeny Poberezkin a6d7604d21 mobile: send live message when there is any content (#1721)
* ios: send live message when there is any content

* android: improve live message logic

* fix, refactor

* prohibit live messages with quotes
2023-01-11 12:01:02 +00:00
JRoberts 9e3573fc76 android: fix send button being disabled on images, files & voice messages (#1720)
* android: fix send button being disabled on images, files & voice messages

* rename view

* format
2023-01-11 08:59:04 +00:00
Evgeny Poberezkin 13ebaf587e 4.4.1-beta.1: iOS 112, Android 86 2023-01-10 23:21:37 +00:00
Stanislav Dmitrenko 61e20550bc core: Updated scripts for downloading libs (#1712) 2023-01-10 20:22:18 +00:00
Stanislav Dmitrenko d1cc5c1769 ios: Better check for existing of image's alpha (#1718)
* ios: Better check for existing of image's alpha

* Allow non-transparent pixels

* optimize

* remove prints

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-10 19:39:42 +00:00
Stanislav Dmitrenko 16b041c8c6 ios: Live messages without sending an empty text (#1714)
* ios: Live messages without sending an empty text

* Custom Equatable

* Changes

* Change

* Fix liveMessage not hiding

* Refactoring

* Refactoring

* No animation when removing dummy live message item

* Check

* Anim

* Animation

* whitespace

* refactor

* Fix race

* Better fix of race

* fix race condition

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-10 19:12:48 +00:00
JRoberts 810f248c74 core: test async file transfer (sender & receiver restarts); close files in stopChatController; handle openFile error in getFileHandle (#1716) 2023-01-10 20:52:59 +04:00
JRoberts 813fecddfe core: fix live file transfers queries (#1715) 2023-01-10 16:22:21 +04:00
Stanislav Dmitrenko 5a7d61c964 android: Live messages without sending an empty text (#1709)
* android: Live messages without sending an empty text

* Better quoted messages handling

* Do not add item into preview

* Change

* Changes
2023-01-09 18:30:26 +00:00
Evgeny Poberezkin cba24983e6 4.4.1-beta.0: iOS 111, Android 85 2023-01-08 16:41:40 +00:00
Evgeny Poberezkin 4dc2a1b72d Revert "core: include commit information in /v response (#1705)"
This reverts commit 3d4e4e2ef9.
2023-01-08 13:13:13 +00:00
Evgeny Poberezkin 3d4e4e2ef9 core: include commit information in /v response (#1705) 2023-01-07 16:38:35 +00:00
JRoberts 113c67ec95 core: disable connections on repeat AUTH errors (#1704) 2023-01-07 19:47:51 +04:00
Stanislav Dmitrenko a2e887024f android: Fixed compatibility with API 29 (#1674) 2023-01-07 14:24:28 +00:00
JRoberts 37262b3ed5 core: fix view agent error context text (#1700) 2023-01-06 19:58:03 +04:00
Evgeny Poberezkin dca4fe7701 Merge branch 'stable' 2023-01-06 13:16:21 +00:00
Evgeny Poberezkin 88c9334d18 readme: SOL address for donations 2023-01-06 13:14:26 +00:00
JRoberts 58f06aa821 core: print error context on store internal errors (#1699) 2023-01-06 14:22:16 +04:00
JRoberts ae5deab8d3 core: print error context on agent errors (#1697) 2023-01-06 13:11:21 +04:00
Evgeny Poberezkin edfece3206 core: test for live messages (#1694) 2023-01-05 09:08:31 +00:00
Evgeny Poberezkin c32cf8055d ios: update library (better quote error handing) 2023-01-04 20:51:16 +00:00
Evgeny Poberezkin 72ec03a822 ios: localize "feature offered/cancelled" items (#1689)
* ios: localize "feature offered/cancelled" items

* import
2023-01-04 20:48:23 +00:00
Evgeny Poberezkin d89e0efedd translations: ios German, French (#1687)
* translations: ios German, French

* reexport ios
2023-01-04 20:36:50 +00:00
Evgeny Poberezkin 707e8592d9 translations: German, French (#1682)
* Translated using Weblate (German)

Currently translated at 100.0% (906 of 906 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/

* Translated using Weblate (German)

Currently translated at 100.0% (850 of 850 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/

* Translated using Weblate (French)

Currently translated at 100.0% (906 of 906 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fr/

* Translated using Weblate (French)

Currently translated at 100.0% (850 of 850 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/

* Translated using Weblate (German)

Currently translated at 100.0% (906 of 906 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/de/

* Translated using Weblate (German)

Currently translated at 100.0% (850 of 850 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/de/

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <ptlfr@pm.me>
2023-01-04 20:25:33 +00:00
Evgeny Poberezkin a1b27e9a99 update simplexmq (better handling of quota errors) 2023-01-04 15:07:33 +00:00
Evgeny Poberezkin f68d8fd97c update readme 2023-01-03 13:13:51 +00:00
Evgeny Poberezkin abff42a264 blog: v4.4 (#1675)
* blog: v4.4

* add images

* update post

* update readme, roadmap

* corrections

* correction
2023-01-03 13:07:30 +00:00
Evgeny Poberezkin c1ced70836 Merge branch 'stable' 2023-01-01 14:06:51 +00:00
Evgeny Poberezkin e14966d36e blog: v4.4 release announcement placeholder 2023-01-01 14:05:05 +00:00
Evgeny Poberezkin 97943fc609 ios: v4.4 release, build 110 2023-01-01 14:01:21 +00:00
shum 0f143b2e77 nix: bump lock 2022-12-31 19:42:31 +00:00
Evgeny Poberezkin be10dcbcfc ios: build 109 2022-12-31 19:40:43 +00:00
Evgeny Poberezkin 97dbec927c ios: build 108 2022-12-31 18:23:26 +00:00
Evgeny Poberezkin b4879ca2a3 ios: fix user chat preferences view 2022-12-31 13:38:55 +00:00
Evgeny Poberezkin 768c497025 readme: change group links (#1657) 2022-12-28 11:30:57 +00:00
62 changed files with 1047 additions and 371 deletions
+22 -17
View File
@@ -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,
+2 -2
View File
@@ -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)
}
} }
} }
@@ -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 &amp; Server</string> <string name="network_and_servers">Netzwerk &amp; 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>
+1 -1
View File
@@ -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
+33 -2
View File
@@ -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
+36 -20
View File
@@ -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? {
+12 -6
View File
@@ -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() }
) )
+1 -1
View File
@@ -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 dIDs 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.&#10;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.&#10;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>
+28 -28
View File
@@ -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;
+3 -1
View File
@@ -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
]) ])
} }
+27 -3
View File
@@ -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? {
+30 -30
View File
@@ -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…";
+5 -5
View File
@@ -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 dIDs 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.";
+9
View File
@@ -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"> &nbsp;&nbsp; <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"> &nbsp;&nbsp; <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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+8 -1
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
+7
View File
@@ -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)
+42 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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>