Compare commits

...

474 Commits

Author SHA1 Message Date
Evgeny Poberezkin d9a8d333f7 4.5.1: Android 99, iOS 124 2023-02-05 23:37:58 +00:00
Evgeny Poberezkin 73f8c543e3 translations (#1900)
* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 26.7% (255 of 954 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (954 of 954 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% (895 of 895 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 8.5% (82 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 24.4% (219 of 895 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.8% (18 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 56.4% (538 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 1.6% (15 of 891 strings)

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

* Added translation using Weblate (Croatian)

* Added translation using Weblate (Croatian)

* Translated using Weblate (Dutch)

Currently translated at 63.6% (607 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.6% (950 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 66.1% (589 of 891 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* ios: import localizations

* ios: export localizations

---------

Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: xqhjay <137847720@qq.com>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: tomato potato <4ryo49@protonmail.com>
Co-authored-by: Albert Bob <vicdorke@gmail.com>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Mehmed <pajazetovicmeho@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2023-02-05 22:36:52 +00:00
Evgeny Poberezkin 14945a9296 ios: update core library 2023-02-05 22:30:03 +00:00
Evgeny Poberezkin 155ffd16ec core: 4.5.1.0 2023-02-05 22:06:27 +00:00
sh 1eb1e52912 call.ts: include udp stun/turn (#1892)
* call.ts: include udp stun/turn

* update JS

* show protocol, support TURNS

* mobile: add turn via UDP, remove protocol from view

* remove enums for protocol strings in ICE candidates

* 0.2.3

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-05 21:57:50 +00:00
Evgeny Poberezkin af173ee5c4 blog: v4.5 (#1886)
* blog: v4.5

* update post, images

* update readme, post
2023-02-05 13:53:41 +00:00
Evgeny Poberezkin 06a2f7e4da mobile: remove option "transfer images faster" (#1891) 2023-02-05 11:25:31 +00:00
Evgeny Poberezkin 958299784d core: update simplexmq (more strict transport host parser 2023-02-04 23:31:01 +00:00
Evgeny Poberezkin 49b6979ff0 core: update simplexmq (not to fail batch subscriptions), terminal: log contact errors with -c option (#1890) 2023-02-04 23:13:20 +00:00
Evgeny Poberezkin 3c493db613 mobile: use TCP for ICE requests of WebRTC calls (#1888)
* ios: support query string parameters in ICE server addresses

* android: support query params in ICE server address, add transport=TCP to default servers
2023-02-04 15:44:39 +00:00
Evgeny Poberezkin 86cc85b3a5 mobile: change default of "transfer images faster/inline" to off, mark as BETA (#1889)
* mobile: change default of "transfer images faster/inline" to off, mark as BETA

* ios: import localizations
2023-02-04 15:19:12 +00:00
Evgeny Poberezkin 0427d2e578 core: prevent failure to acknowledge a group message in case its parsing or saving fails (potential cause for stuck delivery) (#1887) 2023-02-04 12:25:11 +00:00
Evgeny Poberezkin c90d911d2a 4.5.0: Android 98, iOS 123 2023-02-03 15:19:26 +00:00
Evgeny Poberezkin 2473d14baa core: 4.5.0.4, update simplexmq 2023-02-03 11:32:32 +00:00
Evgeny Poberezkin a36f2147d8 mobile: current profile button in profile menu opens settings (#1882) 2023-02-03 11:22:17 +00:00
Evgeny Poberezkin 3837e92556 ios: fix advanced network config (#1881) 2023-02-03 11:17:56 +00:00
Evgeny Poberezkin 76505afff2 ios: import/export translations 2023-02-02 23:28:31 +00:00
Evgeny Poberezkin 89c9a01b20 mobile: translations (#1878)
* Translated using Weblate (Russian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (German)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (895 of 895 strings)

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

* Translated using Weblate (French)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Italian)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 42.1% (402 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 3.8% (37 of 954 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (895 of 895 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (954 of 954 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% (895 of 895 strings)

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

* Translated using Weblate (Czech)

Currently translated at 3.9% (38 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: mlanp <github@lang.xyz>
2023-02-02 23:21:08 +00:00
Evgeny Poberezkin 68517cf852 android: disable chat preferences when chat is stopped (#1877) 2023-02-02 21:27:22 +00:00
Stanislav Dmitrenko fbbad55a0f android: Fix constraints of Compose that could crash the app (#1876)
* android: Fix constraints of Compose that could crash the app

* made constant
2023-02-02 19:46:33 +00:00
Stanislav Dmitrenko bcca27bfdb ios: show notifications for different users (#1874)
* ios: show notifications for different users

* refactore

* terminate background taks on chat item update

* refactor

* refactor2

* refactor3

* refactor 4

* refactor5

* fix chat item update in Android

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-02 16:09:36 +00:00
Evgeny Poberezkin c55a7692c5 4.5.0-beta.3: Android 97, iOS 122 2023-02-02 12:16:58 +00:00
Evgeny Poberezkin a8aa829e4c android: what is new in v45, ios: update texts 2023-02-02 11:20:37 +00:00
Evgeny Poberezkin d5af03ce18 ios: import/export localizations (#1873)
* ios: import localizations

* ios: export localizations
2023-02-02 10:43:18 +00:00
Evgeny Poberezkin 101ef7a81a translations: updates for user profiles, new languages (#1872)
* Added translation using Weblate (Spanish)

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Dutch)

* Translated using Weblate (Russian)

Currently translated at 100.0% (936 of 936 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 11.0% (103 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (936 of 936 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% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 13.8% (130 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 14.9% (131 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 13.7% (129 of 936 strings)

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

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Spanish)

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Dutch)

* Translated using Weblate (Russian)

Currently translated at 100.0% (936 of 936 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 11.0% (103 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (936 of 936 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% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 13.8% (130 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 14.9% (131 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 13.7% (129 of 936 strings)

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

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Portuguese (Brazil))

* Translated using Weblate (French)

Currently translated at 100.0% (936 of 936 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% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 17.3% (162 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 16.2% (143 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 15.7% (147 of 936 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 21.9% (206 of 939 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 20.7% (182 of 878 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 24.3% (229 of 939 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 7.8% (74 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 31.2% (294 of 940 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 12.6% (119 of 940 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 13.5% (127 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 22.7% (214 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 21.7% (191 of 878 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 2.5% (24 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 23.6% (222 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 24.3% (214 of 878 strings)

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

---------

Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Raman <translations.0l5zc@simplelogin.com>
Co-authored-by: tomato potato <4ryo49@protonmail.com>
2023-02-02 10:30:40 +00:00
Evgeny Poberezkin 71daeed81a ios: what is new in v4.5 2023-02-02 10:11:11 +00:00
Evgeny Poberezkin d44324eb4d readme: update weblate link 2023-02-02 09:47:39 +00:00
Evgeny Poberezkin 93d2ef66cf readme: translations (#1871) 2023-02-02 09:41:53 +00:00
Evgeny Poberezkin 8a78943e94 blog: v4.5 release announcement page 2023-02-02 09:00:34 +00:00
Evgeny Poberezkin d0f0013755 core: 4.5.0.3 2023-02-02 08:20:12 +00:00
Stanislav Dmitrenko f22ee1a6cf mobile: prevent WebRTC call failure/hanging when webview "failed" state happens before 30 sec timeout (#1866)
* mobile: do not end calls

* better way of continue connection and end with timeout

* making data classes instead of classes for making logs informative

* refactor

* update webrtc package version

* refactor

* fix

* clear conneciton timeout on disconnection

* refactor

* v0.2.1

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-01 19:23:28 +00:00
Evgeny Poberezkin 4a58ca60ac core: split tests (#1870) 2023-02-01 17:21:13 +00:00
Evgeny Poberezkin b206868730 core: add grop member role "observer" (#1868)
* core: add grop member role "observer"

* disable observer role until supported by most clients
2023-02-01 13:57:39 +00:00
Evgeny Poberezkin bd4c10b224 v4.5.0-beta.2: Android 96, iOS 121 2023-02-01 08:57:38 +00:00
Evgeny Poberezkin 46d15d1811 core: 4.5.0.2 2023-02-01 00:03:37 +00:00
Evgeny Poberezkin ea64be55e1 core: fix cancelling inline file transfer (#1867)
* core: fix cancelling inline file transfer

* fix test
2023-02-01 00:01:22 +00:00
Stanislav Dmitrenko c2cd58f63d android: adapted UserExists error (#1863)
* android: Adapted UserExists error

* updated texts

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-31 18:50:20 +00:00
Stanislav Dmitrenko f21fc76ced ios: adapted UserExists error (#1864)
* ios: adapted UserExists alert

* updated texts

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-31 15:55:41 +00:00
Evgeny Poberezkin 13bd51b97d core: prevent making all users inactive when duplicate user is created (#1862)
* core: prevent making all users inactive when duplicate user is created

* skip async group test
2023-01-31 12:24:18 +00:00
Evgeny Poberezkin a1ed0a84b8 core: use port 7001 for test server (#1857)
* core: use port 7001 for test server

* enable only failing tests

* start/stop server for every test

* log message that failed to parse

* stop chat synchronously

* print call stack

* add HasCallStack

* increase test timeout

* add call stacks

* more call stacks

* fix test

* disable failing test

* add delay between the tests

* make delay more visible

* remove change in error message

* reduce test delay, increase timeout

* increase delay between the tests

* run each test with a database in a different folder

* folder name

* refactor

* update nix file, more stacks
2023-01-31 11:07:48 +00:00
Stanislav Dmitrenko 4815e447fa android: show alert instead of crash on user errors (#1861)
* android: show alert instead of crash on user errors

* show meaningful alert

* update alert messages

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-30 15:12:39 +00:00
Stanislav Dmitrenko d80cad57b6 android: require auth when opening users list (#1860)
* android: require auth when opening users list

* different logic in asking to auth

* unused code
2023-01-30 13:24:15 +00:00
Stanislav Dmitrenko a58be6ebb6 ios: limit number of items in console (#1859) 2023-01-30 12:07:06 +00:00
Stanislav Dmitrenko dfa0272065 android: user picker UI changes + share button (#1858)
* android: user picker UI changes + share button

* limit terminal items size in a different way
2023-01-30 11:19:26 +00:00
Evgeny Poberezkin 9723c47b25 4.5.0-beta.1: Android 95, iOS 120 2023-01-29 19:37:37 +00:00
Evgeny Poberezkin 3eb51eca58 core: v4.5.0.1 2023-01-29 18:52:38 +00:00
Evgeny Poberezkin 86151d4ec2 core: drop index causing slow queries (#1855)
* core: drop index causing slow queries

* update schema
2023-01-29 15:22:09 +00:00
Evgeny Poberezkin 717d05c4a3 android: fix bug when creating group with image 2023-01-29 12:01:19 +00:00
Evgeny Poberezkin 3c43c5d254 ios: import/export localizations 2023-01-28 17:48:37 +00:00
Evgeny Poberezkin 0bae260fae mobile: translations (#1850)
* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.6% (23 of 854 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 7.0% (60 of 854 strings)

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

* 4.5-beta.0: Android 93, iOS 119

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.6% (23 of 854 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 7.0% (60 of 854 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (917 of 917 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% (859 of 859 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (917 of 917 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (859 of 859 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (917 of 917 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% (859 of 859 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 10.7% (92 of 859 strings)

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

* Added translation using Weblate (Chinese (Traditional))

* Added translation using Weblate (Chinese (Traditional))

* Added translation using Weblate (English (United Kingdom))

* Added translation using Weblate (Spanish)

* Translated using Weblate (Chinese (Simplified))

Currently translated at 10.1% (95 of 936 strings)

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

* remove UK English file

---------

Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Albert Bob <vicdorke@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2023-01-28 17:44:12 +00:00
Evgeny Poberezkin e694848cd5 4.5-beta.0: Android 94, iOS 119 2023-01-28 17:11:32 +00:00
Evgeny Poberezkin f5f61c5806 core: update simplexmq, v4.5.0.0 2023-01-28 13:59:46 +00:00
Stanislav Dmitrenko 96c1c1d439 android: drafts (#1849)
* android: drafts

* empty line

* delete unused voice files

* finish recording properly

* refactor

* fix

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-28 13:35:31 +00:00
Evgeny Poberezkin 3e560278b6 ios: update chat previews, show filename in drafts (#1847)
* ios: update chat previews, show filename in drafts

* save and restore images/file/voice for draft

* refactor image

* it was a wrong value

* use param label

* proper stop of voice recording

* safe draft logic

* different way of finishing recording

* keep condition

* refactor

* fix live

* fix

* refactor

* fix

* simplify

* add space after filename in draft

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-01-27 22:09:39 +00:00
Evgeny Poberezkin 6e131e0bad ios: disable current user profile button 2023-01-27 12:54:42 +00:00
Stanislav Dmitrenko bd158f3b0d android: user-specific settings (#1848)
* android: multiuser-peruser

* padding

* bigger padding

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-27 12:28:44 +00:00
M Sarmad Qadeer a96fb2f8d1 website: improvements to design (#1405)
* updated the design of join simplex

* updated the design of Comparison section

* updated the design of Simplex explained

* updated the design of Simplex network

* updated the design of private section

* updated the design of features section

* updated the design of unique section

* updated the design of privacy matters

* added improvements in design

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-27 12:07:22 +00:00
Evgeny Poberezkin 148261a1ee ios: allow to reply in another chat without losing draft 2023-01-26 23:28:56 +00:00
Stanislav Dmitrenko 22b7aa90b2 android: multiuser-notifications (#1846) 2023-01-26 23:00:54 +00:00
Stanislav Dmitrenko 88d9e70ef8 android: mutliuser-calls (#1845)
* android: mutliuser-calls

* tint color of icon

* userId from function

* better line

* missing question

* bigger avatar

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-26 21:36:49 +00:00
Evgeny Poberezkin a0cfc19063 Merge pull request #1844 from simplex-chat/av/multiuser-ui
android: multiusers-profilemanager
2023-01-26 20:49:02 +00:00
Avently 451aab46dc disable deleting the last user 2023-01-26 23:46:10 +03:00
Avently 3b3db562cd added description 2023-01-26 23:14:33 +03:00
Avently 15ed6ea4a6 android: multiusers-profilemanager 2023-01-26 21:23:52 +03:00
Evgeny Poberezkin 9f661ff4e2 Merge pull request #1698 from simplex-chat/users
support multiple user profiles
2023-01-26 10:14:22 +00:00
Evgeny Poberezkin 52c39c9641 Merge branch 'master' into users 2023-01-25 23:31:18 +00:00
Evgeny Poberezkin 0dab486ac8 readme: update group links 2023-01-25 22:48:08 +00:00
Evgeny Poberezkin d640f4a5d5 readme: remove broken twitter badge 2023-01-25 21:04:19 +00:00
Stanislav Dmitrenko 1c47bfbf44 android: better user picker layout (#1842)
* android: multiuser-fixes

* update paddings

* progressIndicator

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-25 20:43:02 +00:00
Stanislav Dmitrenko db3fc4ee7b android: multiuser-userpicker (#1839)
* android: multiuser-userpicker

* sizes of buttons

* update paddings

* change names

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-25 15:30:30 +00:00
JRoberts 74df35d3b0 core: add multiple users tests for subscription, chat item expiration, timed messages (#1840) 2023-01-25 19:29:09 +04:00
Evgeny Poberezkin c01c629f73 mobile: use GMT timezone in filenames to prevent leaking user location (#1837)
* ios: use GMT timezone in filenames to prevent leaking user location

* android: use GMT timestamp in generated file names
2023-01-25 11:48:54 +00:00
Evgeny Poberezkin 25e4a1e86d ios: fix layout of voice message (#1836)
* ios: fix layout of voice message

* fix layout

* prevent translations
2023-01-25 10:18:02 +00:00
Evgeny Poberezkin e27013071b ios: preserve message draft in the latest chat only (#1834)
* ios: preserve message draft in the latest chat only

* show attachment icon and formatting in draft

* button to remove message text

* show icon for active draft, refactor

* add voice message duration to draft
2023-01-25 08:35:25 +00:00
Stanislav Dmitrenko 2679bc2e94 ios: enable swipe to go back from chat to list (#1824)
* ios: Testing workaround of a crash

* another try

* complete

* added file

* enable swipe to go back from ChatView

* Revert "enable swipe to go back from ChatView"

This reverts commit 22de79505c.

* ios: enable swipe to go back from ChatView

* remove title change

* remove unused

* remove unused variable

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-24 19:24:46 +00:00
Evgeny Poberezkin 93ab713748 ios: choose user deletion mode (#1833)
* ios: choose user deletion mode

* update text

* refactor, disable button

* darker profile icon colors

* do not delete active user if changing user failed
2023-01-24 19:00:30 +00:00
JRoberts bc1d86e303 core: send agent DEL events to view (#1832) 2023-01-24 20:07:35 +04:00
Evgeny Poberezkin b386346cf1 core: update syntact for /_delete (#1831) 2023-01-24 16:00:32 +00:00
JRoberts b6db41dd50 update simplexmq (complete) 2023-01-24 18:46:02 +04:00
Stanislav Dmitrenko 6bca013e67 android: multiuser-api (#1829)
* android: multiuser-api

* add line in try-catch block

* changed lines position

* when -> if

* take condition outside

* mutable version of objects and usage of a new function

* changed additional places in code

* added toMap() so state will be updated

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-24 14:29:20 +00:00
Evgeny Poberezkin e538826cd8 Merge branch 'master' into users 2023-01-24 14:09:38 +00:00
JRoberts a7a56ea1d9 core: use batch delete api when deleting unused group contacts (#1830) 2023-01-24 17:58:08 +04:00
Evgeny Poberezkin 77d0f70270 update simplexmq 2023-01-24 13:39:12 +00:00
JRoberts 2a20f78877 core: use batch connection deletion api (#1814) 2023-01-24 16:24:34 +04:00
Evgeny Poberezkin b027708828 4.4.4: Android 92, iOS 118 2023-01-23 20:17:52 +00:00
Evgeny Poberezkin a0bf298b66 Merge branch 'master' into users 2023-01-23 18:45:52 +00:00
Evgeny Poberezkin e3b22d83ad Merge branch 'master' into users 2023-01-23 18:45:24 +00:00
JRoberts ab4e4e1db9 core: test cancelling inline file transfer (#1827)
* core: test cancelling inline file transfer

* tests
2023-01-23 18:27:44 +00:00
Stanislav Dmitrenko a393bc8163 ios: Testing workaround of a crash (#1789)
* ios: Testing workaround of a crash

* another try

* complete

* added file

* enable swipe to go back from ChatView

* Revert "enable swipe to go back from ChatView"

This reverts commit 22de79505c.

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-23 18:17:33 +00:00
Evgeny Poberezkin 3bc4fd222c core: fix cancelling sending inline files (#1826)
* core: fix cancelling sending inline files

* add comments

* typos

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-01-23 21:17:42 +04:00
JRoberts 1b01dcec6d core: fix inline file transfer - optional connection ids in RcvFileInfo, update rcv_file_inline on accept (#1823)
* core: optional connection id in RcvFileInfo

* query

* check snd cancel

* Revert "check snd cancel"

This reverts commit f16651345d.

* update rcv_file_inline
2023-01-23 15:55:19 +00:00
Stanislav Dmitrenko 1d5c361b9a ios: restore scroll and update user profile in user profile menu (#1811)
* ios: Small UserPicker fixes

* update scroll

* update current user and update users list

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-23 15:48:29 +00:00
Stanislav Dmitrenko 4cd396a0d2 ios: Multiuser calls (#1800)
* ios: Multiuser calls

* counter update on badge

* padding before profile info in call view

* underline in name

* change after merge

* do not show Simplex Info button if users already created

* unread counter

* do not increase badge counter when chat has disabled notifications

* update incoming call

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-23 13:20:58 +00:00
Evgeny Poberezkin bcc80be8e9 Merge branch 'master' into users 2023-01-22 23:08:53 +00:00
Evgeny Poberezkin 5d12217eab 4.4.4: Android 91, iOS 117 2023-01-22 19:30:19 +00:00
Evgeny Poberezkin 4b0046a60b mobile: show version information (#1820)
* mobile: show version information

* export localizations
2023-01-22 18:34:01 +00:00
Evgeny Poberezkin 114b76e3f8 core: update version response (#1819)
* core: update version response

* add simplexmq commit and version to version info

* refactor
2023-01-22 15:16:45 +00:00
Evgeny Poberezkin c72aa5d074 Merge branch 'master' into users 2023-01-21 23:14:26 +00:00
Evgeny Poberezkin 2e9882b0bd Merge branch 'master' into users 2023-01-21 23:00:06 +00:00
Evgeny Poberezkin 8ff8f9d695 core: add build timestamp to version information (#1816) 2023-01-21 22:56:33 +00:00
Evgeny Poberezkin 1e3c2024bb android: add localization of "offered/cancelled" feature items, closes #1766 (#1815) 2023-01-21 16:47:26 +00:00
Evgeny Poberezkin 8bec0004cc mobile: UI to choose transport isolation mode (#1813)
* mobile: UI to choose transport isolation mode

* typo

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* ios: update alerts

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-01-21 16:05:09 +00:00
JRoberts c337a6888d core: delete previous contact calls when receiving a new one (#1812) 2023-01-21 16:40:24 +04:00
JRoberts e72d4638d2 core: exlude muted chats from user unread count (#1810) 2023-01-20 20:48:24 +04:00
JRoberts 7dd4dc3b40 core: support accepting contact requests for non active users (for accepting via notification) (#1809)
* core: support accepting contact requests for non active users (for accepting via notification)

* getContactRequest'
2023-01-20 17:55:57 +04:00
JRoberts 980c7a9ddd ios: use agent connection id as key for network statuses map (#1808) 2023-01-20 17:35:39 +04:00
Evgeny Poberezkin cb5f26d354 ios: only show menu if there is more than 1 user, do not show unread count (only badge) 2023-01-20 13:17:41 +00:00
Evgeny Poberezkin 04d886e546 ios: user profiles view, per-user settings (#1801)
* ios: user profiles view, per-user settings

* remove comment

* bold profile name
2023-01-20 12:38:38 +00:00
Evgeny Poberezkin ed12ccaac2 ios: update library 2023-01-20 12:28:45 +00:00
Evgeny Poberezkin e9e9286fbb Merge branch 'master' into users 2023-01-20 12:22:29 +00:00
Evgeny Poberezkin f4ffa5237c 4.4.4-beta.1: Android 90, iOS 116 2023-01-20 12:02:24 +00:00
JRoberts ef15dca0b4 core: don't filter out non active user connections on UP & DOWN agent events; use agent connection id instead of db connection id for ContactRef (#1807) 2023-01-20 15:02:27 +04:00
JRoberts 396b3ae639 ios: maintain connections network statuses map separately from chats (allows to keep track of network statuses for all users) (#1803) 2023-01-20 14:56:05 +04:00
Evgeny Poberezkin c409f58067 mobile: add PING count to network config, make advanced network config available without dev tools (#1805)
* mobile: add PING count to network config, make advanced network config available without dev tools

* export ios translations

* add 120 sec PING interval back
2023-01-20 10:55:12 +00:00
Evgeny Poberezkin 69ca731641 core: process push notifications for any user (#1806)
* core: process push notifications for any user

* return regardless

* refactor

* more refactor
2023-01-20 10:48:25 +00:00
Evgeny Poberezkin 006a30e65c update simplexmq 2023-01-19 19:41:19 +00:00
Evgeny Poberezkin 0a9aa8b3d2 translations: update French and Italian (#1799)
* Translated using Weblate (French)

Currently translated at 100.0% (907 of 907 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% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (853 of 853 strings)

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

* import localizations

* re-export localizations

Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: random r <epsilin@yopmail.com>
2023-01-19 19:27:38 +00:00
Evgeny Poberezkin 69196084fc core: update simplexmq 2023-01-19 19:17:32 +00:00
JRoberts cf4105e256 core: add connection id to ContactRef (#1798) 2023-01-19 20:54:00 +04:00
Stanislav Dmitrenko ad6aa10cd2 ios: Multiusers feature continue (#1793)
* ios: Multiusers feature continue

* Logging of user in responses

* UserId

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* Undo ugly user inclusion into functions.  Now it's in backend

* Do not set active user if it's unchanged

* Blank line

* if

* Change active user function

* refactor

* refactor

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* Alert

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-01-19 16:22:56 +00:00
Stanislav Dmitrenko ba29d0242e core: add user to RcvCallInvitation (#1797)
* core: Include user into RcvCallInvitation

* update build

* parens

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-19 16:00:41 +00:00
JRoberts ca64ed9784 core: option to reuse servers for new user; support for users to configure same smp servers (add user_id to smp_servers UNIQUE constraint) (#1792) 2023-01-18 18:49:56 +04:00
JRoberts a227e21fcf core: support user deletion (#1788)
* core: support user deletion

* doSendCancel

* Apply suggestions from code review

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* sendCancel

* refactor

* error to view

* refactor

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-18 17:08:48 +04:00
Evgeny Poberezkin 7c9c358ce2 ios: update core library 2023-01-18 11:01:51 +00:00
Evgeny Poberezkin 84237f79fc core: refactor (#1764) 2023-01-18 10:20:55 +00:00
Evgeny Poberezkin 80e0bfb61f core: v4.4.4 2023-01-17 18:10:47 +00:00
Stanislav Dmitrenko 153f80fe64 ios: menu to switch active user profile (#1758)
* ios: User chooser UI

* Change

* Changes

* update view

* fix layout/refactor

* fix preview

* wider menu, update label

* hide Your profiles button

* Clickable background that hides userChooser

* No click listener

* Better animation

* Disabled scrolling for small number of items

* Separated scrollview and buttons

* No transition

* Re-indent

* Limiting width by the longest label

* UserManagerView

* Adapted API

* Hide user chooser after selection

* Top counter,  users refactor

* Padding

* use VStack to fix layout bug

* eol

* rename: rename to getUserChatData

* update layout

* s/semibold/medium

* remove SettingsButton view

* rename

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-17 17:47:37 +00:00
Evgeny Poberezkin 19881703e5 core: increase default internal queue max size to 1024 2023-01-17 14:07:47 +00:00
JRoberts a668bd5736 core: cleanup obsolete chat item deletion code (see #1625) (#1787) 2023-01-17 16:58:36 +04:00
JRoberts e8cab01c03 core: update simplexmq (fkey indexes) (#1786) 2023-01-17 16:41:19 +04:00
JRoberts 3f72633d22 Merge branch 'master' into users 2023-01-17 15:58:02 +04:00
JRoberts 5c7ad0926c core: add missing fkey indexes (#1785) 2023-01-17 15:45:37 +04:00
Evgeny Poberezkin 7eca44bb84 4.4.4-beta.0: update simplexmq 2023-01-17 10:09:36 +00:00
JRoberts 2f39cfd86f core: support marking chat items read for any user (#1784) 2023-01-17 13:08:51 +04:00
JRoberts 2fdc23274d core: return user unread counts on ListUsers command (#1763)
* core: return user unread counts on ListUsers command

* split

* tests

* refactor

* viewUserInfo

* refactor

* remove omit nothing

* corrections

* fix

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-16 18:57:31 +00:00
Evgeny Poberezkin 91a39cae23 core: fix error handling (#1761)
* core: fix error handling

* fix tests
2023-01-16 17:25:06 +00:00
Evgeny Poberezkin 882966d5d3 ios: update network config (#1760) 2023-01-16 15:10:16 +00:00
JRoberts 3ed5e6e50b core: support receiving file by id for any user (not only current) (#1759) 2023-01-16 17:51:25 +04:00
JRoberts df6cec6a32 core: update simplexmq (session mode, users commands) (#1757) 2023-01-16 17:00:24 +04:00
JRoberts 24c47657f4 Merge branch 'master' into users 2023-01-16 16:37:13 +04:00
JRoberts cf6afb7687 Merge branch 'master' into users 2023-01-16 16:24:38 +04:00
Evgeny Poberezkin 774af334fd terminal: command to show the most recent chats (#1756)
* terminal: command to show the list of the last active chats

* indent for chats without messages, help

* update command in the test
2023-01-16 12:10:47 +00:00
JRoberts 9dc6c1327f core: manage calls for all users (#1748) 2023-01-16 15:06:03 +04:00
Evgeny Poberezkin af414d7f6e terminal: options for log level and internal queue sizes (#1755)
* terminal: log levels

* option for internal queue sizes
2023-01-16 09:13:46 +00:00
JRoberts a040fa65bb core: run cleanup for all users (#1746) 2023-01-14 19:21:10 +04:00
JRoberts 9fc26ca799 core: start chat item expiration thread for new users (#1745) 2023-01-14 17:52:40 +04:00
Evgeny Poberezkin 9dad55ce8d 4.4.3: iOS 115, Android 89 2023-01-14 11:46:29 +00:00
JRoberts 6e0addbea3 core: add user to CRSmpTestResult response (#1744) 2023-01-14 15:45:42 +04:00
JRoberts e452edb781 core: subscribe all users (#1743) 2023-01-14 15:45:13 +04:00
Evgeny Poberezkin a3283708e7 translations: add Italian, updates (#1741)
* Added translation using Weblate (Italian)

* Added translation using Weblate (Italian)

* 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 99.6% (850 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 24.9% (226 of 906 strings)

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

* Translated using Weblate (Italian)

Currently translated at 26.3% (225 of 853 strings)

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

* 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% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 28.8% (261 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 58.2% (528 of 906 strings)

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

* Translated using Weblate (Italian)

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/it/

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 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% (853 of 853 strings)

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

* Added translation using Weblate (Italian)

* Added translation using Weblate (Italian)

* 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 99.6% (850 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 24.9% (226 of 906 strings)

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

* Translated using Weblate (Italian)

Currently translated at 26.3% (225 of 853 strings)

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

* 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% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 28.8% (261 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 58.2% (528 of 906 strings)

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

* Translated using Weblate (Italian)

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/it/

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 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% (853 of 853 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% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 38.5% (329 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (853 of 853 strings)

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

* correction

* correction

* correction

* ios: import localizations

* ios: re-export localizations

Co-authored-by: Ophiushi <ptlfr@pm.me>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
2023-01-13 23:14:01 +00:00
Evgeny Poberezkin e833d66557 Merge branch 'stable' 2023-01-13 19:18:51 +00:00
Stanislav Dmitrenko 71f5b51350 ios: change network config in NSE when updated via UI (#1731)
* ios: Apply changed network config to NSE

* Better

* Separate function

* rename

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-13 19:05:53 +00:00
Stanislav Dmitrenko 20cec4db11 mobile: do not reset chat preferences on profile update (#1740)
* android: Do not reset prefs on profile update

* ios: Include prefs into Profile/LocalProfile
2023-01-13 18:57:54 +00:00
Stanislav Dmitrenko 2085dc5d60 android: Fix blocked thread while making live messages (#1739)
* android: Fix blocked thread while making live messages

* Test

* Revert "Test"

This reverts commit bd8a5b6ca0.
2023-01-13 18:33:52 +00:00
JRoberts 9290fcc6b2 core: set active prompt to none when changing current user (#1738) 2023-01-13 21:01:36 +04:00
JRoberts 0c3d643408 core: expire chat items for all users (#1737) 2023-01-13 21:01:26 +04:00
Micha├лl Bitard 6d5c3ae484 Fix typo (#1732) 2023-01-13 15:49:58 +00:00
JRoberts cccdcef914 core: add delays to tests to prevent output races (#1736) 2023-01-13 16:26:55 +04:00
Evgeny Poberezkin e63e158b2d core: refactor withUserId (#1735)
* refactor withUserId

* update

* more
2023-01-13 16:24:54 +04:00
JRoberts 892b91e958 core: update simplexmq (subscribe users in different sessions) (#1734) 2023-01-13 15:14:46 +04:00
JRoberts fb04108b11 Merge branch 'master' into users 2023-01-13 14:19:21 +04:00
JRoberts 424328b9d1 core: agent users (#1727) 2023-01-13 13:54:07 +04:00
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
JRoberts 7323bb4333 Merge branch 'master' into users 2023-01-11 18:38:55 +04: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
JRoberts 41e873d5ca core: multiple users view, tests (#1710) 2023-01-11 11:00:28 +04: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
JRoberts ad1b091b18 Merge branch 'master' into users 2023-01-09 17:02:38 +04: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
JRoberts bb0482104c core, ios, android: add UserId to api commands (#1696) 2023-01-05 20:38:31 +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
JRoberts fa9e0086f6 core: multiple users api (#1679)
* api

* UCR

* Revert "UCR"

This reverts commit 1f98d25192.

* comment

* events User

* events in api User

* CRActiveUser in APISetActiveUser

* process message with/without connection

* refactor

* mute error

* user in api responses

* name

* lost response

* user in CRChatCmdError

* compiles

* user in CRChatError

* -- UserId

* mute unused warning

* catch in withUser

* remove comment

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-04 21:06:28 +04: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 15884c0169 4.4.0: iOS 107, Android 84 2022-12-31 11:47:59 +00:00
Evgeny Poberezkin 9f2d5486b6 mobile: fix race condition with live messages (when message is "revived" when it is being sent as no longer live тАУ possibly core should reject "live" updates to non-live messages too) (#1668)
* mobile: fix race condition with live messages (when message is "revived" when it is being sent as no longer live тАУ possibly core should reject "live" updates to non-live messages too)

* android: move delay for live messages
2022-12-31 10:25:32 +00:00
JRoberts 80f0108b41 ios: correctly update chat when opening from another chat via notification (#1667) 2022-12-30 21:47:11 +04:00
Evgeny Poberezkin c37a7ebfe7 ios: import/export localizations 2022-12-30 17:05:32 +00:00
Evgeny Poberezkin 02c2c65d41 translations (#1661)
* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 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% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 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% (845 of 845 strings)

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

* Translated using Weblate (Russian)

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/ru/

* Translated using Weblate (Russian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 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% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 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% (845 of 845 strings)

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

* Translated using Weblate (Russian)

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/ru/

* Translated using Weblate (Russian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

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/ru/

* Translated using Weblate (French)

Currently translated at 99.6% (847 of 850 strings)

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

* 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 90.9% (824 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 92.4% (786 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% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 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% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 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% (845 of 845 strings)

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

* Translated using Weblate (Russian)

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/ru/

* Translated using Weblate (Russian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

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/ru/

* Translated using Weblate (French)

Currently translated at 99.6% (847 of 850 strings)

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

* 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 90.9% (824 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 92.4% (786 of 850 strings)

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

* 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: Ophiushi <ptlfr@pm.me>
Co-authored-by: mlanp <github@lang.xyz>
2022-12-30 17:02:49 +00:00
JRoberts 7c4700b238 ios: check chats not empty before showing lock notice (#1666) 2022-12-30 16:17:56 +00:00
JRoberts 54190ffff9 core: add db indexes for faster group deletion (#1664) 2022-12-30 16:34:42 +04:00
Evgeny Poberezkin 17eed9662e ios: export localizations 2022-12-29 23:31:35 +00:00
Stanislav Dmitrenko bb116bccb4 android: Fallback to manual parsing of apiChats and apiChat responses (#1660)
* android: Fallback to manual parsing of apiChats and apiChat responses

* Different icon

* eol
2022-12-29 22:27:08 +00:00
JRoberts 6cc267689e ios: fallback to manual parsing of apiChats and apiChat responses (#1659) 2022-12-29 18:15:19 +04:00
Evgeny Poberezkin 0e6909845f mobile: preserve group description in profile (#1658) 2022-12-28 16:59:25 +00:00
JRoberts 96ad9faa85 docs: user profiles rfc (#1656) 2022-12-28 16:28:07 +04:00
Evgeny Poberezkin 768c497025 readme: change group links (#1657) 2022-12-28 11:30:57 +00:00
Evgeny Poberezkin 3ec29d8ef4 4.4-beta.4: ios 106, android 83 (fixes wrong type/ios crash) 2022-12-27 20:02:43 +00:00
JRoberts 6c4b92531f android: version 4.4-beta.3 (82) 2022-12-27 20:56:23 +04:00
JRoberts 46d6159da5 ios: version 4.4 beta (105) 2022-12-27 20:48:34 +04:00
JRoberts aab6e1c52f ios, android: set ttl to 1 day when accepting timed messages w/t configured ttl (#1654) 2022-12-27 19:24:33 +04:00
Evgeny Poberezkin c0a01318b5 mobile: French translations (#1655)
* mobile: items with feature offers (#1623)

* mobile: items with feature offers

* ios interactive contact/user preference change items

* android: interactive preference items

* Translated using Weblate (French)

Currently translated at 8.1% (68 of 831 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% (784 of 784 strings)

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

* Translated using Weblate (French)

Currently translated at 10.4% (87 of 831 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 10.5% (88 of 831 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 14.2% (122 of 855 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 15.9% (137 of 858 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 18.2% (157 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 36.3% (312 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 54.0% (464 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 81.0% (695 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 82.1% (705 of 858 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 85.9% (756 of 880 strings)

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

* import/export localizations

Co-authored-by: Ophiushi <ptlfr@pm.me>
2022-12-27 13:21:55 +00:00
Evgeny Poberezkin 13090ff6ed core: do not log TLS handshake errors by default, option to enable (#1652)
* core: do not log TLS handshake errors by default, option to enable

* update simplexmq
2022-12-27 12:05:13 +00:00
Evgeny Poberezkin 90a20cd52f mobile: change live message button to lightning bolt (#1653) 2022-12-27 09:18:20 +00:00
Evgeny Poberezkin 74245d3f2b core: agent stats (#1650) 2022-12-26 22:24:34 +00:00
JRoberts e48452ccff android: show what is new in the latest version (#1651) 2022-12-26 21:46:56 +04:00
JRoberts 39370ba1ef ios: fix - restore Save button in voice message context menu (#1647) 2022-12-26 14:08:58 +00:00
Evgeny Poberezkin a02cfb4f41 ios: show what is new in the latest version (#1644)
* ios: show what is new in the latest version

* add OK button to WhatsNew

* separate state for nav buttons
2022-12-26 14:08:01 +00:00
JRoberts 4370012b8a ios: fix navigation to member info view (#1648) 2022-12-26 13:45:02 +00:00
JRoberts 20c33aea72 android: show connect via url alert in chat list instead of notifications mode on fresh app (#1649) 2022-12-26 13:43:26 +00:00
Evgeny Poberezkin c11a1aa0e6 ios: disallow editing text attributes 2022-12-25 13:58:07 +00:00
Evgeny Poberezkin 166b789f3c ios: v4.4 beta (104) 2022-12-25 09:48:25 +00:00
Evgeny Poberezkin bbc26e272c v4.4-beta.2: android (81) 2022-12-24 21:59:55 +00:00
Evgeny Poberezkin 6c839f8075 android: fix voice recording in groups 2022-12-24 21:47:58 +00:00
Evgeny Poberezkin be91f97c83 ios: disable screen protection by default 2022-12-24 15:41:31 +00:00
Evgeny Poberezkin e085cb7350 4.4-beta.1: iOS 103, Android 80 2022-12-24 11:59:28 +00:00
Evgeny Poberezkin 12574bed96 ios: move image utils to app (#1642)
* ios: move image utils to app

* name in comments
2022-12-24 11:38:59 +00:00
Evgeny Poberezkin 2137893111 ios: change disappearing messages icon (#1641) 2022-12-24 09:48:27 +00:00
Evgeny Poberezkin e552a28a4d ios: re-export French translations 2022-12-23 22:24:27 +00:00
Evgeny Poberezkin b20031d875 ios: add French language (#1640) 2022-12-23 22:14:47 +00:00
Evgeny Poberezkin 558b3fa356 translations (#1639)
* Translated using Weblate (German)

Currently translated at 95.0% (783 of 824 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (824 of 824 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (824 of 824 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
2022-12-23 21:49:34 +00:00
Stanislav Dmitrenko cb337cef10 ios: Download libs script (#1634) 2022-12-23 21:24:08 +00:00
Stanislav Dmitrenko cd63f81292 ios: Animated images (GIF) support (#1636)
* ios: Animated images (GIF) support

* Moved from String path to UIImage param

* Aspect ratio

* Image frame

* gif image size

* refactor

* refactor

* fix fullscreen scroll animation

* rename UploadContent -> AnyImage

* refactor, allow using gifs in profiles

* rename back

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-23 21:22:12 +00:00
Evgeny Poberezkin 6205b03943 ios: localize ttl in disappearing messages, translations (#1638)
* ios: localize ttl in disappearing messages, translations

* more translation keys
2022-12-23 19:55:45 +00:00
Evgeny Poberezkin 82924ce8c6 translations (#1637)
* Translated using Weblate (German)

Currently translated at 93.5% (823 of 880 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (880 of 880 strings)

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

* Translated using Weblate (German)

Currently translated at 96.2% (774 of 804 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (804 of 804 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (880 of 880 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% (880 of 880 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% (804 of 804 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (804 of 804 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (880 of 880 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (804 of 804 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (880 of 880 strings)

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

Co-authored-by: J R <jr@simplex.chat>
Co-authored-by: Ophiushi <ptlfr@pm.me>
2022-12-23 18:55:14 +00:00
Evgeny Poberezkin b1067c339c android: fix meta layout/reserved space (#1635) 2022-12-23 17:27:41 +00:00
Evgeny Poberezkin 0d6e4b48f6 translations (#1633)
* Translated using Weblate (French)

Currently translated at 97.6% (785 of 804 strings)

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

* Translated using Weblate (French)

Currently translated at 97.6% (785 of 804 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
2022-12-23 14:50:23 +00:00
JRoberts 84d2c408ce core: optimize chat loading time - faster chat previews queries (item_status index for chat stats), fix live file transfers queries (#1630)
* core: optimize get chat previews queries (item_status index for chat stats)

* cleanup

* optimize chat loading time

* cleanup

* schema

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-23 18:37:02 +04:00
Evgeny Poberezkin de434b730e weblate/translations (#1632)
* mobile: items with feature offers (#1623)

* mobile: items with feature offers

* ios interactive contact/user preference change items

* android: interactive preference items

* Translated using Weblate (French)

Currently translated at 8.1% (68 of 831 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% (784 of 784 strings)

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

* Translated using Weblate (French)

Currently translated at 10.4% (87 of 831 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 10.5% (88 of 831 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 14.2% (122 of 855 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 15.9% (137 of 858 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 18.2% (157 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 36.3% (312 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 54.0% (464 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 81.0% (695 of 858 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% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 82.1% (705 of 858 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 85.9% (756 of 880 strings)

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

* revert changes

Co-authored-by: Ophiushi <ptlfr@pm.me>
2022-12-23 14:11:12 +00:00
Evgeny Poberezkin 1251dbc4b0 ios: export localizations 2022-12-23 13:13:37 +00:00
sh d115ad228b build-android: disable external repo (#1615) 2022-12-23 13:10:36 +00:00
Evgeny Poberezkin 28d6f62b74 mobile: better layout for feature/preference items (#1631)
* mobile: better layout for feature/preference items

* refactor
2022-12-23 13:10:00 +00:00
Evgeny Poberezkin 2b9238144b ios: fix double unread status (#1629) 2022-12-23 09:14:12 +00:00
Evgeny Poberezkin a2e1b7ae0a v4.4: iOS build 102, android build 79 2022-12-23 08:33:57 +00:00
Evgeny Poberezkin a00bb6d5ef android: fix enabling voice cancelling disappearing messages 2022-12-22 22:08:21 +00:00
Evgeny Poberezkin da12b651e4 4.4.0 2022-12-22 21:31:07 +00:00
Evgeny Poberezkin a936c14cf2 mobile: items with feature offers (#1627)
* mobile: items with feature offers

* ios interactive contact/user preference change items

* android: interactive preference items

* add missing view

* revert change
2022-12-22 21:01:29 +00:00
Stanislav Dmitrenko e6aad24e5f android: Timed messages TTL logic in preferences (#1624)
* android: Timed messages TTL logic in preferences

* do not set ttl in global timed message prefs

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-22 15:39:24 +00:00
JRoberts 8dac96f415 core: wait chat started in deleteTimedItem threads (#1626) 2022-12-22 19:18:38 +04:00
Evgeny Poberezkin aae0802ec8 core: chat items with offered feature (#1620)
* core: chat items with offered feature

* texts

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* new preference items

* test

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-12-22 14:56:29 +00:00
JRoberts 74a20ef70c core: sort chat previews by chat ts (#1625) 2022-12-22 17:43:10 +04:00
Evgeny Poberezkin a2a29628a7 ios: remove unused code (#1621) 2022-12-21 22:36:05 +00:00
Stanislav Dmitrenko 0b046315ac android: Disappearing messages (#1619)
* android: Disappearing messages

* remove unused func

* remove paren

* outlined timer in meta

* reserving space for meta takes into account ttl text

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-21 22:07:37 +00:00
Stanislav Dmitrenko 372d7ffaa9 ios: Better check for alpha channel existing (#1616)
* ios: Better check for PNG

* Renamed
2022-12-21 16:05:45 +00:00
JRoberts ece928d57e core: update ttl in contact user preference on profile update, fix api, tests; fix global user preferences not being updated in controller state (#1617) 2022-12-21 19:54:44 +04:00
Evgeny Poberezkin e1740a8be4 ios: disappearing messages (#1614)
* ios: disappearing messages

* show ttl in meta if different

* mark messages as disappearing when read

* previews
2022-12-21 12:59:45 +00:00
Evgeny Poberezkin 36eba01ef4 ios: fix padding in send button context menu (#1618) 2022-12-21 12:55:59 +00:00
JRoberts 9e045a44db Revert "core: confirm ttl change to ensure consistent setting (#1587)"
This reverts commit 34e08b2058.
2022-12-21 14:10:05 +04:00
Stanislav Dmitrenko b7d42ef889 ios: Png images support with alpha (#1613)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-21 00:15:18 +00:00
Stanislav Dmitrenko e55cd82ec3 android: Live messages (#1612)
* android: Live messages

* White color

* Spacer

* button sizes

* Do not show voice button in live mode

* Add text to the last image in a row

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-20 23:55:01 +00:00
JRoberts 34e08b2058 core: confirm ttl change to ensure consistent setting (#1587)
* core: confirm ttl change to ensure consistent setting

* wip

* confirm_pref_pending

* xInfo

* test api

* send confirmPrefProfile

* refactor

* don't return contact

* refactor profile update

* refactor further

* refactor further

* refactor xInfo

* refactor xInfo further

* refactor
2022-12-20 22:00:46 +04:00
Evgeny Poberezkin 5e9b7366cc core: refactor chat item updates (#1611)
* core: refactor chat item updates

* removed unused function

* refactor

* refactor
2022-12-20 12:58:15 +00:00
Evgeny Poberezkin 64fb1f0b85 core: do not start disappearing timer for live messages until they stop being live, and start it on item update instead, provided they are read (#1609)
* core: do not start disappearing timer for live messages until they stop being live, and start it on item update instead, provided they are read

* change delays in tests

* diffInSeconds
2022-12-20 10:17:29 +00:00
JRoberts 84e43c57f6 core: ttl in feature chat items, view responses (#1595)
* core: ttl in feature chat items, view responses

* fix tests

* fix test

* view

* refactor

* use prefChangedValue

* use groupPrefChangedValue

* use cupIntValue

* simplify types

* groupFeatureState

* groupPrefToText

* prefToText, view

* remove prefFeature

* rename intValue -> param

* int -> param

* timedTTLText

* remove pragma

* restore pragma

* simplify

* timedTTLText

* fix tests

* off, after

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-19 21:18:59 +04:00
Evgeny Poberezkin ffa37b1684 terminal: use Ctrl-L to start/continue live message, as Alt-Enter is not always supported (#1607)
* terminal: use Ctrl-L to start/continue live message, as Alt-Enter is not always supported

* refactor
2022-12-19 13:05:54 +00:00
Evgeny Poberezkin 86271fe109 terminal: support live messages (#1597)
* terminal: toggle live message updates

* terminal: send live messages (#1599)

* terminal: send live messages

* show edited messages

* send and continue live message with Alt-Enter

* truncate live messages to full words

* remove comments

* refactor

* refactor to avoid clearing live message prompt and show it faster

* $

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-12-19 11:16:50 +00:00
JRoberts 5dab099b5c core: increase thread delays in timed messages tests 2022-12-19 11:21:51 +04:00
sh 199e61e5c6 docs/webrtc: add troubleshoot section (#1602) 2022-12-18 23:42:37 +00:00
Evgeny Poberezkin 76b4fd34c1 ios: fix live messages sending incomplete words, refactor (#1604) 2022-12-18 21:20:39 +00:00
Evgeny Poberezkin b159496257 mobile: allow ending live message with an empty string (#1603) 2022-12-18 19:21:13 +00:00
Evgeny Poberezkin c0fb29d5f7 ios: remove unused package from project (#1598) 2022-12-17 18:52:02 +00:00
Evgeny Poberezkin 4ab7e5e1c8 terminal: command to get last item ID (to reference it in the tests) (#1596)
* terminal: command to get last item ID (to reference it in the tests)

* lastItemId

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-12-17 19:33:58 +04:00
Evgeny Poberezkin 9e847c2e1f ios: live messages (#1569)
* ios: live messages

* remove comments

* remove conflict

* live message buttons and alert

* only send full words

* fix double sending

* typing indicator in live items

* add live parameter to API

* typing indication, pass live parameter to API

* refactor to support live messages with attachments

* disable attachments
2022-12-17 14:02:07 +00:00
Evgeny Poberezkin d105e59655 core: set item that was live as 0, that was never live as NULL (Maybe Bool type) (#1594)
* core: set item that was live as 0, that was never live as NULL (Maybe Bool type)

* fix
2022-12-17 13:58:16 +00:00
JRoberts f128ebac87 core: timed messages terminal api, tests (#1591) 2022-12-17 14:49:03 +04:00
Stanislav Dmitrenko b4de9c266b ios: Ability to add stickers (#1593)
* ios: Ability to add stickers

* fix text alignment for correct input field height

* refactor

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-17 09:01:49 +00:00
JRoberts e410fc7736 core: fix mark read queries (#1592) 2022-12-16 14:32:37 +00:00
Evgeny Poberezkin f5bd6eb4c3 core: do not mark live items as editied until it's no longer live (#1590)
* core: do not mark live items as editied

* Update src/Simplex/Chat/Store.hs

* mark item as edited when it stops being live
2022-12-16 12:31:35 +00:00
JRoberts cee403c1ed core: simplify terminal mark messages read logic (#1589) 2022-12-16 15:56:16 +04:00
Evgeny Poberezkin 8786e2147a core, mobile: logic for enabling disappearing messages (#1588)
* core: logic for enabled for disappearing messages

* refactor

* update feature enabled in UI
2022-12-16 10:27:59 +00:00
Evgeny Poberezkin 6b8705e9f4 core: support for live messages (#1577) 2022-12-16 11:51:04 +04:00
Stanislav Dmitrenko acfb98bd81 android: Optimized chats snapshotFlow (#1578)
* android: Optimized chats snapshotFlow

* Concurrency test

* Revert "Concurrency test"

This reverts commit 911dd0c2ef.

* Comment

* Better catch
2022-12-15 19:57:57 +00:00
Stanislav Dmitrenko c240456b80 android: Progress indicator and group members loading (#1579)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-15 19:29:17 +00:00
Stanislav Dmitrenko 9e1641a154 android: Changed a path in script for downloading libs (#1580) 2022-12-15 19:25:49 +00:00
JRoberts 17cd3cdca4 core: make ttl optional in TimedMessagesPreference (#1583)
* core: make ttl Maybe in TimedMessagesPreference

* omitNothingFields
2022-12-15 18:11:08 +00:00
JRoberts aa264690ab core: add ttl to XMsgUpdate (#1581) 2022-12-15 17:29:46 +04:00
JRoberts 0e837ae392 core: timed messages (#1561)
* docs: disappearing messages rfc

* change schema

* word

* wip

* wip

* todos

* todos

* remove cancel, refactor

* revert prefs

* CITimed

* schema

* time on send direct

* time on send group

* add ttl to msg container, refactor

* timed on receive

* time on read

* getTimedItems, fix tests

* mark read in terminal - view, input, output, fix tests

* refactor

* comment

* util

* insert atomically

* refactor

* use guards

* refactor startTimedItemThread

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-15 15:17:29 +04:00
Stanislav Dmitrenko 68525b4131 android: Show error instead of crashing after failed to parse chats (#1573)
* android: Show error instead of crashing after failed to parse chats

* Just for test

* update strings

* Revert "Just for test"

This reverts commit f9c9a20ab6.

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-15 10:56:20 +00:00
Stanislav Dmitrenko 8775db7c97 android: Create group link with one click (#1575)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-14 22:47:24 +00:00
Stanislav Dmitrenko f266debd56 android: Verify connection security code (#1567)
* android: Verify connection security code

* Dividers

* Changes

* Padding

* Share connection code

* Share connection code

* Unused

* icon sizes

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-14 14:44:26 +00:00
Evgeny Poberezkin 044c7a8191 mobile: update types for timed messages preference (#1574) 2022-12-14 13:53:31 +00:00
Evgeny Poberezkin 677c6aeb2e core: types for timed and live messages (#1572)
* core: types for timed and live messages

* add protocol tests
2022-12-14 16:16:11 +04:00
Evgeny Poberezkin 7b8f5be821 core: type for group preference for timed messages (#1568)
* core: type for group preference for timed messages

* remove unused func
2022-12-14 12:30:24 +04:00
Evgeny Poberezkin 21765905a7 ios: create group link with one click (#1566)
* ios: create group link with one click

* line break

* move call
2022-12-13 17:15:45 +00:00
Evgeny Poberezkin 70a9c01477 translations (#1563)
* Translated using Weblate (Russian)

Currently translated at 100.0% (831 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 98.4% (772 of 784 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/fr/
2022-12-13 14:53:06 +00:00
Evgeny Poberezkin 678dbec3e2 core: different types for chat preferences, to allow parameters (#1565) 2022-12-13 14:52:34 +00:00
Stanislav Dmitrenko bd4c7dffbf android: Notification mode selection in onboarding stage (#1535)
* android: Notification mode selection in onboarding stage

* Change

* Different texts

* Disable service starting until on-boarding finishes

* refactor, change strings

* update layout

* update layout

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-12 19:27:28 +00:00
Stanislav Dmitrenko 1eb4030080 android: Fix crash on multiple selection of images (#1560)
* android: Fix crash on multiple selection of images

* Revert "android: Fix crash on multiple selection of images"

This reverts commit 11a6113b4f.

* Disable image selection when there are images already selected

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-12 17:27:14 +00:00
sh bcc64442e9 nix: merge android and ios (#1551) 2022-12-12 15:44:49 +00:00
Stanislav Dmitrenko 1246b9e376 android: Chat auto-scrolling behaviour (#1556)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-12 15:34:26 +00:00
Evgeny Poberezkin d6e9a87d58 ios: verify connection translations (#1558) 2022-12-12 12:50:15 +00:00
Evgeny Poberezkin cddd3cd673 FR translations (#1559)
* Translated using Weblate (German)

Currently translated at 100.0% (822 of 822 strings)

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

* Added translation using Weblate (French)

* Added translation using Weblate (French)

* Translated using Weblate (French)

Currently translated at 0.0% (0 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 0.3% (3 of 822 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% (773 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 0.9% (8 of 822 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% (773 of 773 strings)

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

Co-authored-by: Allan Nordh├╕y <epost@anotheragency.no>
Co-authored-by: Ophiushi <ptlfr@pm.me>
2022-12-12 12:33:50 +00:00
JRoberts e00ef7c7da core: improve stability of file transfer handshake by using async agent commands (#1541) 2022-12-12 16:33:07 +04:00
Evgeny Poberezkin 1a201cfadf translations: corrections to DE, add FR (#1557)
* Translated using Weblate (German)

Currently translated at 100.0% (822 of 822 strings)

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

* Added translation using Weblate (French)

* Added translation using Weblate (French)

* Translated using Weblate (French)

Currently translated at 0.0% (0 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 0.3% (3 of 822 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% (773 of 773 strings)

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

Co-authored-by: Allan Nordh├╕y <epost@anotheragency.no>
Co-authored-by: Ophiushi <ptlfr@pm.me>
2022-12-12 11:49:08 +00:00
JRoberts a4ecb41743 ios, android: show send direct message button only for active members (#1554) 2022-12-12 15:27:52 +04:00
Stanislav Dmitrenko e347f5329c android: Different style for voice button when no permissions granted (#1555)
* android: Different style for voice button when no permissions granted

* linebreaks

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-12 10:25:49 +00:00
Stanislav Dmitrenko 741b3e8848 android: Voice messages refactoring (#1511)
* android: Voice messages refactoring

* Different way to block text field from editing while recording voice

* Limited voice record max duration

* Better end of recording when it reaches timeout

* New way of doing things

* Change

* Change

* Stop event refactor

* Stopped state

* Replaced some helpers

* Replaced calls in when()

* Comments

* Change

* Change

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-12 09:36:37 +00:00
Evgeny Poberezkin 7b4710d198 ios: verify connection security code (#1542)
* ios: verify connection security code

* verification in member sheet (still crashes)

* use navigation view for members list

* ios: show verified status in the lists

* update verification status in the list of members

* verified shield layout

* update icon, make add member navigation to right

* refactor chatPreviewTitle
2022-12-12 08:59:35 +00:00
sh c77f6100c5 fdroid: fix metadata (#1550)
* fdroid: fix description

* add header

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-11 18:33:56 +00:00
Evgeny Poberezkin 138dc7fe8f 4.3.2: terminal, ios (101), android (78) 2022-12-11 15:51:23 +00:00
Evgeny Poberezkin 0535d84719 website: add F-Droid key hash to open SimpleX links in the app 2022-12-11 15:39:11 +00:00
Evgeny Poberezkin f4447ffe89 readme: add BCH address for donations 2022-12-11 14:06:22 +00:00
Evgeny Poberezkin 146d5f99bc core: clear connection verification status (#1540) 2022-12-10 12:09:45 +00:00
Evgeny Poberezkin 73e5fff8f5 core: fix parser 2022-12-10 08:43:54 +00:00
Evgeny Poberezkin 33e7538172 core: group description (#1538)
* core: group description

* support multi-line welcome message

* fix
2022-12-10 08:27:32 +00:00
Stanislav Dmitrenko 49c9c501aa android: Fix for AddressAlreadyInUse exception (#1534)
* android: Fix for AddressAlreadyInUse exception

* simplify loop

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-09 22:15:50 +00:00
Evgeny Poberezkin a177dc5a13 core: refactor parser (#1537)
* core: refactor parser

* fix
2022-12-09 21:50:01 +00:00
Evgeny Poberezkin a4f207875f show /create link command when group is created (#1536) 2022-12-09 18:22:03 +00:00
JRoberts bcca0998d5 core: optimize group deletion (#1529) 2022-12-09 20:01:31 +04:00
Evgeny Poberezkin 95cc9e1e55 core: verify connection (#1530)
* core: verify connection

* update commands

* api to get/set verification code/status

* add migration

* refactor

* change command / response names

* reset verified status if code from agent doesn't match
2022-12-09 15:26:43 +00:00
sh ab5ae2d2cb build-android: add skip flag and update logic (#1525)
* build-android: add skip flag and update logic

* build-android: change equal
2022-12-08 08:55:37 +00:00
JRoberts 40a91a7273 android: version 4.3.1 (77) 2022-12-08 10:48:13 +04:00
JRoberts 1240b31df8 ios: version 4.3.1 (100) 2022-12-08 10:41:59 +04:00
Evgeny Poberezkin ff14730738 mobile, core: fix voice message reception in groups (#1524) 2022-12-07 22:18:22 +00:00
JRoberts 0cba3a4bb3 4.3.1 2022-12-07 21:10:45 +04:00
JRoberts 208f8a3346 android: version 4.3.1 (76) 2022-12-07 21:09:52 +04:00
JRoberts caa3efb9ed ios: version 4.3.1 (99) 2022-12-07 21:04:43 +04:00
JRoberts 4beb916754 ios: deleted item preview; android: refactor removeChatItem (#1523) 2022-12-07 20:46:38 +04:00
Stanislav Dmitrenko c1ee04eed1 android: Cancel notification after message deletion (#1512)
* android: Cancel notification after message deletion

* Improve

* Temporary chat item

* Better

* Changes

* cInfo, cItem

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-12-07 19:49:17 +04:00
JRoberts 0ad3bc9993 android: show open direct chat button for direct contacts (#1521) 2022-12-07 19:07:31 +04:00
JRoberts 9893aa665a core: don't mark contacts as used on api get chat (#1522) 2022-12-07 19:05:32 +04:00
JRoberts fda8836ab8 ios: show open direct chat button for direct contacts (#1518) 2022-12-07 17:30:15 +04:00
Evgeny Poberezkin 05fdd07409 website: add SHA256 of the key signing GitHub android APK to open links in the app 2022-12-07 10:27:31 +00:00
Evgeny Poberezkin fb8f5facd0 terminal: only set contact/group as active for terminal input if it is not muted (#1514) 2022-12-07 09:58:01 +00:00
Stanislav Dmitrenko 8bdb784a14 android: Added rememberSaveable in pref screens (fix merge) (#1517) 2022-12-07 09:57:23 +00:00
Stanislav Dmitrenko 5d785aad2e android: Added rememberSaveable in pref screens (#1509) 2022-12-06 21:04:15 +00:00
Stanislav Dmitrenko ce11d58a76 android: Saving prefs alert on exit with unsaved changes (#1508)
* android: Saving prefs alert on exit with unsaved changes

* DIfferent implementation for AlertDialog with long buttons

* Braces

* Change

* Alignment

* Rename

* small changes

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-06 20:48:15 +00:00
Evgeny Poberezkin 887b374bfc readme: add mastodon link 2022-12-06 19:53:08 +00:00
Evgeny Poberezkin 94dc967197 readme: update screenshots 2022-12-06 17:30:32 +00:00
JRoberts 4319a581ca core: more test cases checking deletion of unused contacts and incognito profiles (#1513) 2022-12-06 20:19:01 +04:00
JRoberts fb05218558 core: delete unused contacts after deleting group (#1503) 2022-12-06 17:12:39 +04:00
Evgeny Poberezkin edf2d02a0d blog: v4.3 release announcement (#1510)
* blog: v4.3 release announcement

* add images

* update image URIs

* update post

* typos

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* correction

* website preview, readme update

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-12-06 12:53:14 +00:00
Evgeny Poberezkin 87ba429dfd Merge branch 'stable' 2022-12-05 20:47:16 +00:00
Evgeny Poberezkin 7af1a7cf76 docs: update f-droid store info (#1507) 2022-12-05 20:46:11 +00:00
sh df619acdd4 build-android: update nix install (#1506) 2022-12-05 18:45:18 +00:00
Evgeny Poberezkin 503d0cd451 android: make backup disabled by default (#1505) 2022-12-05 15:05:56 +00:00
Stanislav Dmitrenko 1294a00ee7 android: Vibration pattern (#1504)
* android: Vibration pattern

* update pattern

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-05 14:56:37 +00:00
Stanislav Dmitrenko 0a8069ada2 android: Notification sound (#1468)
* android: fix full screen call notification (#1466)

* android: Closing call means canceling notification too

* show full screen call when screen is off OR locked

* make notification non-silent and set category

* remove call notification category

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* android: Notification sound

* Log

* Ringtone channel

* rename call channel

* Non-hideable headsUp notification and reject button

* Removed LockScreenCallChannel

* call channel name

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-05 13:13:48 +00:00
Evgeny Poberezkin c167f594b9 website: add .well-known folder to allow mobile apps process URLs 2022-12-05 11:28:22 +00:00
Evgeny Poberezkin ce5124594d blog: permalink for v4.3 post (#1499) 2022-12-04 18:18:23 +00:00
Evgeny Poberezkin 5de96aa7c4 android: v4.3 (75) 2022-12-04 18:07:41 +00:00
Evgeny Poberezkin cdbf8e2715 ios: v4.3 (98) 2022-12-04 17:36:41 +00:00
Evgeny Poberezkin 69b2f8f535 mobile: german translations (#1498) 2022-12-04 15:18:35 +00:00
Evgeny Poberezkin ff17f89551 android: improve UX to create groups and UI of group preferences (#1496) 2022-12-04 15:16:41 +00:00
Evgeny Poberezkin 358712fa31 ios: translations (#1495) 2022-12-04 11:41:45 +00:00
Evgeny Poberezkin 75cad8a6bf ios: improve UX for contact/group preferences (#1494)
* ios: improve UX for contact/group preferences

* refactor
2022-12-04 11:30:51 +00:00
Evgeny Poberezkin e5969e197a mobile: "delete for everyone" feature, translations (#1491) 2022-12-04 09:29:00 +00:00
Evgeny Poberezkin a9ffe4e039 android: function to call api on background thread, use it for marking items read (#1493) 2022-12-04 08:36:19 +00:00
Stanislav Dmitrenko bf2129c4ae android: Making full backup optional (#1477)
* android: Making full backup optional

* move to database settings

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-04 07:50:36 +00:00
Evgeny Poberezkin 04f10aede7 ios: fix screen protection in sheets, remove screen protection from settings and image pickers (#1492) 2022-12-03 21:42:12 +00:00
Evgeny Poberezkin ffbff93374 ios: menu to hide revealed chat item (#1490) 2022-12-03 19:21:47 +00:00
JRoberts f3630d934c android: marked deleted / reveal ui (#1488)
* android: marked deleted / reveal ui

* marked deleted, reveal

* fix ios

* different alerts
2022-12-03 18:21:32 +00:00
Evgeny Poberezkin 6f59df4e33 prohibit direct messages to group contacts unless group preferences allow them (#1476)
* prohibit direct messages to group contacts unless group preferences allow them

* tests

* refactor

* more test
2022-12-03 18:06:21 +00:00
Evgeny Poberezkin e44e9a0940 mobile: broker error type (#1475)
* mobile: broker error type

* fix

* ios: update libraries

* change AgentErrorType to String
2022-12-03 18:05:32 +00:00
Evgeny Poberezkin c43ba7bf23 ios: fix item deletion in groups (#1487) 2022-12-03 15:21:14 +00:00
JRoberts 9e48e1f74a android: refactor CIVoiceView usage in FramedItemView (latter accounts only for framed voice messages) (#1486) 2022-12-03 18:28:07 +04:00
JRoberts 0001885971 obsolete comment 2022-12-03 18:24:20 +04:00
Evgeny Poberezkin e0c932c04e core: change AgentErrorType to String to preserve backward compatibility with stored errors (#1485) 2022-12-03 13:28:51 +00:00
JRoberts 01a86336c0 android: simplify logic for allowing voice messages on alert; fix non exhaustive when in SendMsgView (#1484) 2022-12-03 16:19:13 +04:00
JRoberts 48d24d3582 ios: simplify chat item context menu (#1483) 2022-12-03 15:53:46 +04:00
JRoberts 07ef6e4090 ios: marked deleted chat items, full deletion preference; android: types (#1473)
* ios: marked deleted chat items; full deletion preference

* text_, menu, backend

* android types

* more android types

* fix

* refactor ios

* restore previews

* box

* refactor menu

* revert unnecessary content.text changes

* Update apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* revert layered framed items

* clever framed view

* improve look

* restore previews

* restore previews

* refactor

* refactoring, almost looks good

* look

* add previews

* more previews

* remove preview of legacy item

* ChatItemDeleted

* flip if

* remove text_

* refactor

* abstract pref property

* move marked deleted

* revert pref change

* undo menu

* fix - change to constants

* undo pref logic

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-03 15:40:31 +04:00
Evgeny Poberezkin 19163776e3 android: fix 2022-12-03 09:06:39 +00:00
Stanislav Dmitrenko 62b1f786f1 android: Remove runningAppProcesses check (#1478)
* android: Remove runningAppProcesses check

* simplify

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-03 06:29:52 +00:00
Stanislav Dmitrenko d479e9b2bf android: Change of launchMode in an activity and different behavior of back button (#1480)
* android: Change of launchMode in an activity and different behavior of back button
- Android versions <= 10 are vulnerable to StrandHogg 1. This commit fixes the behavior of the app on affected versions of Android

* simplify condition

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-02 23:07:21 +00:00
Evgeny Poberezkin 0beb260b00 android: json parsing settings explicitNulls = false 2022-12-02 21:36:58 +00:00
Evgeny Poberezkin bc28568c63 core: update broker error type (#1474)
* core: update broker error type

* fix test

* fix test
2022-12-02 15:01:26 +00:00
Stanislav Dmitrenko a4dd520248 android: Better shared preference handling (#1471)
* android: Better shared preference handling

* To make sure we return real value, not untransformed one

* Revert "To make sure we return real value, not untransformed one"

This reverts commit 5a268e2cf4.
2022-11-30 22:20:08 +00:00
JRoberts 9ad29aa17e core: full deletion by sender based on preference; don't overwrite item content on "mark deleted" (#1470) 2022-11-30 19:42:33 +04:00
Stanislav Dmitrenko 6f24281671 android: prevent crash when decrypting DB after restore (#1469) 2022-11-30 12:20:49 +00:00
Evgeny Poberezkin eb81b62892 terminal: allow trailing spaces in terminal commands (e.g., to drag and drop files) (#1467) 2022-11-30 08:25:42 +00:00
Stanislav Dmitrenko ef1133ee98 android: fix full screen call notification (#1466)
* android: Closing call means canceling notification too

* show full screen call when screen is off OR locked

* make notification non-silent and set category

* remove call notification category

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-29 15:53:47 +00:00
Evgeny Poberezkin 1872744543 core, mobile: add group feature to allow direct messages (#1465)
* core, mobile: split group features to a separate type (to add directAllowed later)

* add directMessages group feature, update tests
2022-11-29 15:19:20 +00:00
Stanislav Dmitrenko 303aeaaba5 android: Instantly apply screen protection (#1464) 2022-11-29 14:21:41 +00:00
Stanislav Dmitrenko c5359d698c android: Voice messages enhancements (#1451)
* android: Vocie messages enhancements

* Canceling voice record when it was disabled in prefs

* Quote placement in voice message chat item

* Ordering of checks

* Showing progress logic was changed

* Showing progress logic was changed

* Update group prefs without reenter

* Optimization of voice chat items

* Stop audio playing and recoring when in call
2022-11-29 13:41:04 +00:00
JRoberts acd72fb269 ios: remove voice message preview and stop rec/play if preference change prohibits it; more robust logic to stop playback on send (#1463) 2022-11-29 15:23:54 +04:00
sh 8d096f469d docs: corrections (#1456)
* docs: corrections

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* docs: onion instead of hostname2

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-29 09:17:26 +00:00
Stanislav Dmitrenko b204d21d9e android: No crash when clicking on a link with unknown activity action (#1460)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-29 09:16:03 +00:00
JRoberts c9620a594e ios: fix - stop voice message preview playback on send (#1461) 2022-11-29 13:06:45 +04:00
Stanislav Dmitrenko 538024de61 android: Show user's camera when system camera is unavailable (#1458)
* android: Show user's camera when system camera is unavailable

* Multiple places of camera launcher

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-29 08:50:54 +00:00
JRoberts 5c9a14fdb6 ios: show button for opening settings when asking for microphone permission to record voice message (#1459) 2022-11-29 12:41:48 +04:00
JRoberts 9295bdca3e ios, android: disable save and test buttons if all servers are disabled (#1457) 2022-11-29 12:28:26 +04:00
Evgeny Poberezkin 00466f4654 android: v4.3-beta.3 2022-11-28 18:05:25 +00:00
Evgeny Poberezkin 5d976d3c67 docs: SMP servers (#1387)
* docs: SMP servers

* docs: add server configuration

* docs: small fixes

* docs: fix markdown rendering

* docs: csv file instead of URL

* docs: small fixes

* docs: check if log exist

* docs: no i allowed

* docs: correction

* docs: no need to source profile

* docs: no calue allowed

* docs: systemd fix

* docs: small corrections

* docs: some more small fixes

* docs: expand monitoring

* docs: apply suggestions

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>

* client configuration

* images

Co-authored-by: shum <shum@liber.li>
Co-authored-by: sh <37271604+shumvgolove@users.noreply.github.com>
2022-11-28 17:34:43 +00:00
JRoberts 7360bd098a ios: version 4.3 (97) 2022-11-28 20:46:06 +04:00
JRoberts 3a755286c1 ios, android: improve preference change chat items layout (#1454) 2022-11-28 20:03:39 +04:00
JRoberts e5f07993a7 mobile: don't show notifications for certain chat items (#1453) 2022-11-28 19:11:26 +04:00
JRoberts 56a3f98dc0 core: create certain informational chat items as read (#1452) 2022-11-28 16:27:22 +04:00
JRoberts 9949ac073f ios: decrease voice message progress bar width (#1450)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-28 08:50:05 +00:00
Stanislav Dmitrenko c102a884d1 android: Stickers fix (#1449)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-28 08:47:33 +00:00
Evgeny Poberezkin 1e6e9ad5e2 mobile: update version ios v4.3 (96), android v4.3-beta.2 (73) 2022-11-28 08:39:50 +00:00
JRoberts e6c5ad5833 ios: fix image context item background color (#1448) 2022-11-28 11:23:40 +04:00
Evgeny Poberezkin 8af0229f52 terminal: set voice message preferences (#1447)
* terminal: set voice message preferences

* enable all tests
2022-11-27 13:54:34 +00:00
Evgeny Poberezkin 7f0355ec67 core: only send voice messages without acceptance (#1444)
* core: only send voice messages without acceptance

* remove some unnecessary changes

* update

* refactor receiveInlineMode
2022-11-26 22:39:56 +00:00
JRoberts ade8c97b16 ios: version 4.3 (95) 2022-11-26 18:54:54 +04:00
JRoberts ee18bce964 mobile: change link to servers 2022-11-26 18:52:08 +04:00
JRoberts 7e204127b8 ios: hide voice message button in chat console (#1442) 2022-11-26 18:43:49 +04:00
JRoberts 5619152810 android: version 4.3-beta.1 (72) 2022-11-26 14:30:52 +04:00
JRoberts 6f463c16a5 ios: version 4.3 (94) 2022-11-26 14:10:55 +04:00
JRoberts 33b3557950 mobile: suppress fileAlreadyReceiving error (#1441) 2022-11-26 13:55:36 +04:00
Evgeny Poberezkin e5912e58f5 mobile: show "transfer faster" and "switch address" without dev tools (#1440) 2022-11-26 09:45:10 +00:00
JRoberts 7a0d2add17 android: add missing translation 2022-11-26 13:22:26 +04:00
JRoberts ac30602a50 4.3.0 2022-11-26 13:09:09 +04:00
JRoberts 6cc4e2e801 android: version 4.3 (71) 2022-11-26 13:07:34 +04:00
JRoberts 5650898c2c ios: version 4.3 (93) 2022-11-26 13:00:43 +04:00
JRoberts 336a170b3a mobile: hide full delete preferences (#1438) 2022-11-26 12:51:56 +04:00
JRoberts 9c06acd4bc ios: voice message repeat receive ui workaround (#1437) 2022-11-26 12:43:26 +04:00
mlanp 1d819a4af3 mobile: German translations for 4.3 (#1432)
* android/iOS: added and fixed german translations for v4.3

* import localization

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-26 11:56:10 +04:00
Evgeny Poberezkin 5263698e64 ios: show notifications alert only once, each time after notifications are disabled (#1428) 2022-11-25 21:43:10 +00:00
Evgeny Poberezkin 5d0d9a1c18 android: add context menu to voice message (aka play button) (#1427) 2022-11-25 20:53:43 +00:00
Evgeny Poberezkin 7407884223 android: protect screen (#1425)
* android: protect screen

* hide screen, translations
2022-11-25 19:33:29 +00:00
Evgeny Poberezkin 07acbfe743 mobile: remove BETA from strings 2022-11-25 17:56:56 +00:00
JRoberts 5e2c868612 core: silence repeat file receive error (#1426) 2022-11-25 21:20:55 +04:00
JRoberts f6ed099f17 ios: voice messages - improve hold to record button logic, alert if not allowed (#1424) 2022-11-25 21:05:14 +04:00
Stanislav Dmitrenko 098cbf33b6 android: Group chat items (#1423)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-25 16:42:53 +00:00
Stanislav Dmitrenko f8cf35879f android: Voice messages UI changes (#1422)
* android: Voice messages UI changes

* Alert for groups

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-25 16:31:04 +00:00
Evgeny Poberezkin 60fedbf5d2 core: only create feature items in used contacts (#1421)
* core: only create feature items in used contacts

* fix, test
2022-11-25 15:37:36 +00:00
Evgeny Poberezkin 87d306383c ios: protect screen (#1420)
* ios: protect screen

* AppSheet

* translations

* correction

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-25 14:31:37 +00:00
Evgeny Poberezkin 18b772a80b ios: translations (#1411) 2022-11-25 13:50:26 +00:00
Stanislav Dmitrenko 789c54bd5f android: Voice messages playing logic overhaul (#1418)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-25 13:06:56 +00:00
Stanislav Dmitrenko f8214b0604 android: Chat items for preferences (#1409)
* android: Chat items for preferences

* fix filled

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-25 12:50:22 +00:00
Stanislav Dmitrenko 2eec81c35e android: Duration formatter (#1419) 2022-11-25 12:23:30 +00:00
JRoberts eb099c526a core: reuse mergedPreferences/fullGroupPreferences for determining prohibited features and creating chat items instead of re-calculating (#1417) 2022-11-25 15:16:55 +04:00
JRoberts e18bb74bfd ios: show voice message button based on preference (#1416) 2022-11-25 15:16:37 +04:00
Evgeny Poberezkin 9225f437e9 android: simplex link mode setting, ios: untrusted simplex links (#1412)
* android: simplex link mode setting, ios: untrusted simplex links

* correction

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-25 09:55:51 +00:00
JRoberts de7548a9a8 ios: alert on error when recording voice message (#1415) 2022-11-25 12:30:24 +04:00
JRoberts a58a0fae29 android: preferences - fix reset & save buttons being enabled when displayed as disabled, fix info rows being interactive (#1414) 2022-11-25 11:53:29 +04:00
JRoberts e32b24ef70 android: move preferences files to respective packages; dividers (#1413) 2022-11-25 11:27:22 +04:00
Evgeny Poberezkin 5d73b364d8 android: fix type name 2022-11-24 17:34:01 +00:00
Stanislav Dmitrenko fa2f303547 android: Global preferences and per contact (#1290)
* android: Global preferences and per contact

* Fixes

* Changes in UI

* Fix

* Strings and more descriptions

* Spelling error

* No dots at the end

* Adapting changes from core

* Adapting changes from core

* Change

* Simplified user's choice with toggle

* Changes after merge

* Updated preferences to the latest changes in core

* Strings

* Changes

* Small changes

* Contact will be updated in UI too

* bigger icons in section headers

* Icons and colors

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-24 17:30:58 +00:00
JRoberts a6e4e68bc5 ios: voice messages (#1389)
* experiments

* audio recording in swiftui

* recording encapsulated

* permission + playback

* stopAudioPlayback on cancel

* method names

* check permission in recording start

* run timer on main thread

* remove obsolete view

* don't call playback timer callback unless player is playing

* compose + send view + preview + send

* animation + improve state + quality

* fix recording not stopping in time

* animate to end

* remove recorder delegate, fix cancelling during recording

* replace print with log

* recording start error constructor

* CIVoiceView file

* chat item wip

* chat item wip

* refactor settings

* layout

* send correct duration

* item previews

* more background, animation

* more layout

* more layout, send button conditions

* context, preview, quote, notification texts

* chat item actions

* use isEmpty

* remove comment

* uncomment file.loaded

* more layout, hold to record

* more layout

* preview player stop on disappear

* more layout

* comment

* only one player or recording

* remove voice message on chat close

* fix state bug

* remove commented code

* length 30

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-11-24 21:18:28 +04:00
Evgeny Poberezkin 4485d46307 mobile: simplex links in UI, core: trusted uri for simplex links (#1410) 2022-11-24 17:14:56 +00:00
Evgeny Poberezkin a7345ee4d9 core: markdown for simplex invitation links (#1408)
* core: markdown for simplex invitation links

* update markdown for simplex links

* update markdown

* update

* stabilize test
2022-11-24 13:13:26 +00:00
Evgeny Poberezkin 388aaec80b core: config to send inline files (#1406)
* core: config to send inline files

* update config

* add/update tests

* fix tests
2022-11-23 16:08:33 +00:00
Stanislav Dmitrenko 21722b3417 android: Advanced server config (#1403)
* android: Advanced server config

* Update apps/android/app/src/main/java/chat/simplex/app/views/usersettings/ScanSMPServer.kt

* Camera permission, dropping tested value, different font

* For review

* Partial redraw of the view in testing stage

* Comment

* Icon

* Icon

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-23 11:13:24 +00:00
Evgeny Poberezkin e6e5faeb9c core: chat items for group preferences (#1402)
* core: chat items for group preferences

* chat items for group preference changes and sent item for contact/user prerences changes

* prohibited features, tests

* enable all tests

* fix
2022-11-23 11:04:08 +00:00
Evgeny Poberezkin 67d78e14be Merge branch 'stable' 2022-11-22 14:12:18 +00:00
Evgeny Poberezkin 0d1a70af34 android: v4.2.2 (70) 2022-11-22 14:11:23 +00:00
Evgeny Poberezkin 2b09fb425d core: chat items showing preference changes (#1399) 2022-11-22 12:50:56 +00:00
Evgeny Poberezkin ab91c54080 ios: version 4.2.2 (92) 2022-11-22 12:44:40 +00:00
Evgeny Poberezkin c7f70f0ed0 ios: resolved packages 2022-11-22 09:47:42 +00:00
Evgeny Poberezkin 5806a2ceb4 Merge branch 'stable' 2022-11-22 08:55:01 +00:00
Evgeny Poberezkin 6b71cc59c8 blog: updates (#1400) 2022-11-22 08:49:33 +00:00
Evgeny Poberezkin 33a866463d core: fix sql that was doubling a group in the list of chats when member joined the group twice (#1378) 2022-11-21 10:37:11 +00:00
Stanislav Dmitrenko fb165622aa android: Wrapped code in SideEffect (#1396) 2022-11-21 10:24:18 +00:00
Evgeny Poberezkin 7e3d53b621 ios: advanced server configuration (#1388)
* ios: advanced server configuration

* UI is mostly working, QR code scan

* refactor

* error alerts

* fixes

* remove old view

* rename view

* translations

* only show valid QR code, spinner during server test

* update tested status on edit

* space wtf

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* moar space

* translation

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>

* translations

* translations

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2022-11-21 08:37:13 +00:00
Evgeny Poberezkin 7544d2f9e7 core: fix preset servers (#1392)
* core: fix preset servers

* simplify

* fix
2022-11-21 07:43:41 +00:00
Evgeny Poberezkin 02fa81e8aa mobile: show unknown content with attached file as file item (for partial forward compatibility with voice and video messages) (#1394)
* mobile: show unknown content with attached file as file item (for partial forward compatibility with voice and video messages)

* fix

* android: show unknown files
2022-11-21 07:42:36 +00:00
Evgeny Poberezkin 4296b6c622 android: import function to parse server address (#1391) 2022-11-21 07:39:36 +00:00
sh b5652bce81 docker: enable sqlcipher (#1390) 2022-11-20 11:56:01 +00:00
372 changed files with 88527 additions and 12324 deletions
+2 -2
View File
@@ -5,7 +5,7 @@ on:
branches:
- master
- stable
- sqlcipher
- users
tags:
- "v*"
pull_request:
@@ -109,7 +109,7 @@ jobs:
- name: Unix test
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
timeout-minutes: 10
timeout-minutes: 20
shell: bash
run: cabal test --test-show-details=direct
+1
View File
@@ -42,6 +42,7 @@ stack.yaml.lock
# Temporary test files
tests/tmp
tests/tmp*
logs/
+4 -1
View File
@@ -1,7 +1,7 @@
FROM ubuntu:focal AS build
# Install curl and simplex-chat-related dependencies
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev libssl-dev
# Install ghcup
RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/bin/ghcup && \
@@ -21,6 +21,9 @@ WORKDIR /project
# Adjust PATH
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
# Adjust build
RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
# Compile simplex-chat
RUN cabal update
RUN cabal install
+63 -28
View File
@@ -5,8 +5,8 @@
[![build](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml/badge.svg?branch=stable)](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml)
[![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
[![Follow on Twitter](https://img.shields.io/twitter/follow/SimpleXChat?style=social)](https://twitter.com/SimpleXChat)
[![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat)
[![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex)
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
&nbsp;
@@ -43,6 +43,7 @@
- [For developers](#for-developers)
- [Roadmap](#roadmap)
- [Join a user group](#join-a-user-group)
- [Translate the apps](#translate-the-apps)
- [Contribute](#contribute)
- [Help us with donations](#help-us-with-donations)
- [Disclaimers, Security contact, License](#disclaimers)
@@ -85,15 +86,15 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent updates:
[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)
[Feb 04, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.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).
[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)
[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).
[Aug 8, 2022. v3.1: secret chat groups, access via Tor, reduced battery and traffic usage, advanced network settings, etc.](./blog/20220808-simplex-chat-v3.1-chat-groups.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).
[Jul 11, 2022. v3.0: instant push notifications for iOS, e2e encrypted WebRTC audio/video calls, chat database export/import, privacy and performance improvements](./blog/20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md)
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
[All updates](./blog)
@@ -103,7 +104,7 @@ You need to share a link or scan a QR code (in person or during a video call) to
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/conversation.png" alt="Make a private connection" width="594" height="360">
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
## :zap: Quick installation of a terminal app
@@ -147,10 +148,11 @@ What is already implemented:
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
We plan to add soon:
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers termporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
1. Automatic message queue rotation. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`.
3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
@@ -187,21 +189,29 @@ If you are considering developing with SimpleX platform please get in touch for
- тЬЕ Chat database encryption.
- тЬЕ Automatic chat history deletion.
- тЬЕ Links to join groups and improve groups stability.
- тЬЕ Voice messages (with recipient opt-out per contact).
- тЬЕ Basic authentication for SMP servers (to authorize creating new queues).
- тЬЕ View deleted messages, full message deletion by sender (with recipient opt-in per contact).
- тЬЕ Block screenshots and view in recent apps.
- тЬЕ 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.
- тЬЕ Preserve message drafts.
- ЁЯПЧ File server to optimize for efficient and private sending of large files.
- ЁЯПЧ Improved audio & video calls.
- ЁЯПЧ SMP queue redundancy and rotation (manual is supported).
- ЁЯПЧ Voice messages (with recipient opt-out per contact).
- ЁЯПЧ Basic authentication for SMP servers (to authorize creating new queues).
- View deleted messages, full message deletion by sender (with recipient opt-in per contact).
- Block screenshots and view in recent apps.
- Optionally avoid re-using the same TCP session for multiple connections.
- Access password/pin (with optional alternative access password).
- ЁЯПЧ Reduced battery and traffic usage in large groups.
- ЁЯПЧ Support older Android OS and 32-bit CPUs.
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Media server to optimize sending large files to groups.
- Access password/pin (with optional alternative access password).
- Local app files encryption.
- Video messages.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- Multiple user profiles in the same chat database.
- Advanced server configuration.
- Feeds/broadcasts.
- Unconfirmed: disappearing messages (with recipient opt-in per-contact).
- Web widgets for custom interactivity in the chats.
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
- Supporting the same profile on multiple devices.
@@ -209,28 +219,50 @@ If you are considering developing with SimpleX platform please get in touch for
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups.
- Channels server for large groups and broadcast channels.
- Hosting server for large groups, communities and public channels.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- High capacity multi-node SMP relays.
## 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). Just bear in mind that it has ~300 members now, and that it is fully decentralized, so sending a message and connecting to all members in this group will take some time, only join it if you:
- want to see how larger groups work.
- traffic is not a concern (sending each message is ~5mb).
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).
You can also join a new and smaller English-speaking group if you want to ask questions without too much traffic: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D)
There are also several groups in languages other than English, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users. We do not always answer questions there, so please ask them in one of the English-speaking groups.
- [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking).
- [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking).
- [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking).
- [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
You can join 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!
## Translate the apps
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps are translated to many other languages. Join our translators to help SimpleX grow faster!
Current interface languages:
English (development language)
German: [@mlanp](https://github.com/mlanp)
French: link to be added
Italian: [@unbranched](https://github.com/unbranched)
Russian: project team
Languages in progress: Chinese, Hindi, Japanese, Dutch and [many others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed тАУ please suggest new languages and get in touch with us!
## Contribute
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.
- 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.
- 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
@@ -246,8 +278,11 @@ 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 wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- 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.
Thank you,
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 69
versionName "4.3-beta.0"
versionCode 99
versionName "4.5.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
@@ -24,6 +24,8 @@
<application
android:name="SimplexApp"
android:allowBackup="true"
android:fullBackupOnly="true"
android:backupAgent="BackupAgent"
android:icon="@mipmap/icon"
android:label="${app_name}"
android:extractNativeLibs="${extract_native_libs}"
@@ -102,7 +104,9 @@
<activity android:name=".views.call.IncomingCallActivity"
android:showOnLockScreen="true"/>
android:showOnLockScreen="true"
android:exported="false"
android:launchMode="singleTask"/>
<provider
android:name="androidx.core.content.FileProvider"
+18 -1
View File
@@ -22,10 +22,12 @@ var TransformOperation;
TransformOperation["Decrypt"] = "decrypt";
})(TransformOperation || (TransformOperation = {}));
let activeCall;
let answerTimeout = 30000;
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stun:stun.simplex.im:443"] },
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
{ urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
{ urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
];
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
return {
@@ -100,9 +102,16 @@ const processCommand = (function () {
const iceCandidates = getIceCandidates(pc, config);
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
await setupMediaStreams(call);
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
pc.addEventListener("connectionstatechange", connectionStateChange);
return call;
async function connectionStateChange() {
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
if (pc.connectionState !== "failed")
connectionHandler();
}
async function connectionHandler() {
sendMessageToNative({
resp: {
type: "connection",
@@ -115,6 +124,7 @@ const processCommand = (function () {
},
});
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
clearConnectionTimeout();
pc.removeEventListener("connectionstatechange", connectionStateChange);
if (activeCall) {
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
@@ -122,6 +132,7 @@ const processCommand = (function () {
endCall();
}
else if (pc.connectionState == "connected") {
clearConnectionTimeout();
const stats = (await pc.getStats());
for (const stat of stats.values()) {
const { type, state } = stat;
@@ -141,6 +152,12 @@ const processCommand = (function () {
}
}
}
function clearConnectionTimeout() {
if (connectionTimeout) {
clearTimeout(connectionTimeout);
connectionTimeout = undefined;
}
}
}
function serialize(x) {
return LZString.compressToBase64(JSON.stringify(x));
@@ -29,6 +29,7 @@ extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
extern char *chat_parse_markdown(const char *str);
extern char *chat_parse_server(const char *str);
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
@@ -76,3 +77,11 @@ Java_chat_simplex_app_SimplexAppKt_chatParseMarkdown(JNIEnv *env, __unused jclas
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_SimplexAppKt_chatParseServer(JNIEnv *env, __unused jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
@@ -0,0 +1,18 @@
package chat.simplex.app
import android.app.backup.BackupAgentHelper
import android.app.backup.FullBackupDataOutput
import android.content.Context
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.model.AppPreferences.Companion.SHARED_PREFS_PRIVACY_FULL_BACKUP
class BackupAgent: BackupAgentHelper() {
override fun onFullBackup(data: FullBackupDataOutput?) {
if (applicationContext
.getSharedPreferences(AppPreferences.SHARED_PREFS_ID, Context.MODE_PRIVATE)
.getBoolean(SHARED_PREFS_PRIVACY_FULL_BACKUP, true)
) {
super.onFullBackup(data)
}
}
}
@@ -3,10 +3,12 @@ package chat.simplex.app
import android.app.Application
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.os.SystemClock.elapsedRealtime
import android.util.Log
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.core.*
@@ -27,6 +29,7 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.SplashView
@@ -40,7 +43,6 @@ import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
class MainActivity: FragmentActivity() {
@@ -73,6 +75,13 @@ class MainActivity: FragmentActivity() {
processIntent(intent, m)
processExternalIntent(intent, m)
}
if (m.controller.appPrefs.privacyProtectScreen.get()) {
Log.d(TAG, "onCreate: set FLAG_SECURE")
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
setContent {
SimpleXTheme {
Surface(
@@ -125,7 +134,15 @@ class MainActivity: FragmentActivity() {
}
override fun onBackPressed() {
super.onBackPressed()
if (
onBackPressedDispatcher.hasEnabledCallbacks() // Has something to do in a backstack
|| Build.VERSION.SDK_INT >= Build.VERSION_CODES.R // Android 11 or above
|| isTaskRoot // there are still other tasks after we reach the main (home) activity
) {
// https://medium.com/mobile-app-development-publication/the-risk-of-android-strandhogg-security-issue-and-how-it-can-be-mitigated-80d2ddb4af06
super.onBackPressed()
}
if (!onBackPressedDispatcher.hasEnabledCallbacks() && vm.chatModel.controller.appPrefs.performLA.get()) {
// When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch
clearAuthState()
@@ -357,22 +374,6 @@ fun MainPage(
.collect {
if (it != null) currentChatId = it
else onComposed()
// Deletes files that were not sent but already stored in files directory.
// Currently, it's voice records only
if (it == null && chatModel.filesToDelete.isNotEmpty()) {
chatModel.filesToDelete.forEach { it.delete() }
chatModel.filesToDelete.clear()
}
}
}
launch {
snapshotFlow { chatModel.sharedContent.value }
.distinctUntilChanged()
.filter { it != null }
.collect {
chatModel.chatId.value = null
currentChatId = null
}
}
}
@@ -386,7 +387,8 @@ fun MainPage(
}
}
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel)
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
}
ModalManager.shared.showInView()
val invitation = chatModel.activeCallInvitation.value
@@ -396,20 +398,31 @@ fun MainPage(
}
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
val userId = getUserIdFromIntent(intent)
when (intent?.action) {
NtfManager.OpenChatAction -> {
val chatId = intent.getStringExtra("chatId")
Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId")
if (chatId != null) {
val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true
if (cInfo != null) withApi { openChat(cInfo, chatModel) }
withBGApi {
if (userId != null && userId != chatModel.currentUser.value?.userId) {
chatModel.controller.changeActiveUser(userId)
}
val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true
if (cInfo != null) openChat(cInfo, chatModel)
}
}
}
NtfManager.ShowChatsAction -> {
Log.d(TAG, "processNotificationIntent: ShowChatsAction")
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
withBGApi {
if (userId != null && userId != chatModel.currentUser.value?.userId) {
chatModel.controller.changeActiveUser(userId)
}
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
}
}
NtfManager.AcceptCallAction -> {
val chatId = intent.getStringExtra("chatId")
@@ -470,7 +483,6 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) {
// TODO open from chat list view
chatModel.appOpenUrl.value = uri
} else {
withUriAction(uri) { linkType ->
@@ -11,8 +11,7 @@ import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.*
import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
@@ -32,12 +31,15 @@ external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
external fun chatRecvMsg(ctrl: ChatCtrl): String
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String
class SimplexApp: Application(), LifecycleEventObserver {
lateinit var chatController: ChatController
var isAppOnForeground: Boolean = false
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() ?: ""
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
val res: DBMigrationResult = kotlin.runCatching {
@@ -88,6 +90,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
context = this
initChatController()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
@@ -95,6 +98,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
withApi {
when (event) {
Lifecycle.Event.ON_START -> {
isAppOnForeground = true
if (chatModel.chatRunning.value == true) {
kotlin.runCatching {
val chats = chatController.apiGetChats()
@@ -103,6 +107,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
}
Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
chatController.showBackgroundServiceNoticeIfNeeded()
}
@@ -111,10 +116,14 @@ class SimplexApp: Application(), LifecycleEventObserver {
* after calling [ChatController.showBackgroundServiceNoticeIfNeeded] notification mode in prefs can be changed.
* It can happen when app was started and a user enables battery optimization while app in background
* */
if (chatModel.chatRunning.value != false && appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
if (chatModel.chatRunning.value != false &&
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
) {
SimplexService.start(applicationContext)
}
}
else -> {}
else -> isAppOnForeground = false
}
}
}
@@ -168,7 +177,18 @@ class SimplexApp: Application(), LifecycleEventObserver {
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
Log.d(TAG, "starting server")
val server = LocalServerSocket(socketName)
var server: LocalServerSocket? = null
for (i in 0..100) {
try {
server = LocalServerSocket(socketName + i)
break
} catch (e: IOException) {
Log.e(TAG, e.stackTraceToString())
}
}
if (server == null) {
throw Error("Unable to setup local server socket. Contact developers")
}
Log.d(TAG, "started server")
s.release()
val receiver = server.accept()
File diff suppressed because it is too large Load Diff
@@ -3,9 +3,11 @@ package chat.simplex.app.model
import android.app.*
import android.content.*
import android.graphics.BitmapFactory
import android.hardware.display.DisplayManager
import android.media.AudioAttributes
import android.net.Uri
import android.util.Log
import android.view.Display
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import chat.simplex.app.*
@@ -23,12 +25,18 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
const val ShowChatsAction: String = "chat.simplex.app.SHOW_CHATS"
// DO NOT change notification channel settings / names
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION"
const val LockScreenCallChannel: String = "chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION"
const val CallChannel: String = "chat.simplex.app.CALL_NOTIFICATION_1"
const val AcceptCallAction: String = "chat.simplex.app.ACCEPT_CALL"
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
const val CallNotificationId: Int = -1
private const val UserIdKey: String = "userId"
private const val ChatIdKey: String = "chatId"
fun getUserIdFromIntent(intent: Intent?): Long? {
val userId = intent?.getLongExtra(UserIdKey, -1L)
return if (userId == -1L || userId == null) null else userId
}
}
private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -37,24 +45,29 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
init {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(NotificationChannel(LockScreenCallChannel, generalGetString(R.string.ntf_channel_calls_lockscreen), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel())
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
}
enum class NotificationAction {
ACCEPT_CONTACT_REQUEST
}
private fun callNotificationChannel(): NotificationChannel {
val callChannel = NotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls), NotificationManager.IMPORTANCE_HIGH)
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
val callChannel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH)
val attrs = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
.build()
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
Log.d(TAG,"callNotificationChannel sound: $soundUri")
callChannel.setSound(soundUri, attrs)
callChannel.enableVibration(true)
// the numbers below are explained here: https://developer.android.com/reference/android/os/Vibrator
// (wait, vibration duration, wait till off, wait till on again = ringtone mp3 duration - vibration duration - ~50ms lost somewhere)
callChannel.vibrationPattern = longArrayOf(250, 250, 0, 2600)
return callChannel
}
@@ -70,8 +83,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
}
fun notifyContactRequestReceived(cInfo: ChatInfo.ContactRequest) {
fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {
notifyMessageReceived(
user = user,
chatId = cInfo.id,
displayName = cInfo.displayName,
msgText = generalGetString(R.string.notification_new_contact_request),
@@ -80,21 +94,22 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
)
}
fun notifyContactConnected(contact: Contact) {
fun notifyContactConnected(user: User, contact: Contact) {
notifyMessageReceived(
user = user,
chatId = contact.id,
displayName = contact.displayName,
msgText = generalGetString(R.string.notification_contact_connected)
)
}
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) {
fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {
if (!cInfo.ntfsEnabled) return
notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
notifyMessageReceived(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
}
fun notifyMessageReceived(chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
fun notifyMessageReceived(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
Log.d(TAG, "notifyMessageReceived $chatId")
val now = Clock.System.now().toEpochMilliseconds()
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
@@ -119,13 +134,14 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
.setColor(0x88FFFF)
.setAutoCancel(true)
.setVibrate(if (actions.isEmpty()) null else longArrayOf(0, 250, 250, 250))
.setContentIntent(chatPendingIntent(OpenChatAction, chatId))
.setContentIntent(chatPendingIntent(OpenChatAction, user.userId, chatId))
.setSilent(if (actions.isEmpty()) recentNotification else false)
for (action in actions) {
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
val actionIntent = Intent(SimplexApp.context, NtfActionReceiver::class.java)
actionIntent.action = action.name
actionIntent.putExtra(UserIdKey, user.userId)
actionIntent.putExtra(ChatIdKey, chatId)
val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags)
val actionButton = when (action) {
@@ -140,7 +156,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
.setGroup(MessageGroup)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
.setGroupSummary(true)
.setContentIntent(chatPendingIntent(ShowChatsAction))
.setContentIntent(chatPendingIntent(ShowChatsAction, user.userId))
.build()
with(NotificationManagerCompat.from(context)) {
@@ -151,24 +167,34 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
fun notifyCallInvitation(invitation: RcvCallInvitation) {
if (isAppOnForeground(context)) return
val keyguardManager = getKeyguardManager(context)
Log.d(TAG,
"notifyCallInvitation pre-requests: " +
"keyguard locked ${keyguardManager.isKeyguardLocked}, " +
"callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " +
"onForeground ${SimplexApp.context.isAppOnForeground}"
)
if (SimplexApp.context.isAppOnForeground) return
val contactId = invitation.contact.id
Log.d(TAG, "notifyCallInvitation $contactId")
val keyguardManager = getKeyguardManager(context)
val image = invitation.contact.image
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val screenOff = displayManager.displays.all { it.state != Display.STATE_ON }
var ntfBuilder =
if (keyguardManager.isDeviceLocked && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
if ((keyguardManager.isKeyguardLocked || screenOff) && appPreferences.callOnLockScreen.get() != CallOnLockScreen.DISABLE) {
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, LockScreenCallChannel)
NotificationCompat.Builder(context, CallChannel)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSilent(true)
} else {
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
NotificationCompat.Builder(context, CallChannel)
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.contact.id))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, contactId))
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, contactId, true))
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
}
val text = generalGetString(
@@ -197,8 +223,11 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
.setLargeIcon(largeIcon)
.setColor(0x88FFFF)
.setAutoCancel(true)
val notification = ntfBuilder.build()
// This makes notification sound and vibration repeat endlessly
notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT
with(NotificationManagerCompat.from(context)) {
notify(CallNotificationId, ntfBuilder.build())
notify(CallNotificationId, notification)
}
}
@@ -206,33 +235,36 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
manager.cancel(CallNotificationId)
}
fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() }
private fun hideSecrets(cItem: ChatItem) : String {
val md = cItem.formattedText
return if (md == null) {
if (cItem.content.text != "") {
cItem.content.text
} else {
if (cItem.content.msgContent is MsgContent.MCVoice) generalGetString(R.string.voice_message) else cItem.file?.fileName ?: ""
}
} else {
return if (md != null) {
var res = ""
for (ft in md) {
res += if (ft.format is Format.Secret) "..." else ft.text
}
res
} else {
cItem.text
}
}
private fun chatPendingIntent(intentAction: String, chatId: String? = null): PendingIntent {
private fun chatPendingIntent(intentAction: String, userId: Long, chatId: String? = null, broadcast: Boolean = false): PendingIntent {
Log.d(TAG, "chatPendingIntent for $intentAction")
val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt()
var intent = Intent(context, MainActivity::class.java)
var intent = Intent(context, if (!broadcast) MainActivity::class.java else NtfActionReceiver::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
.setAction(intentAction)
.putExtra(UserIdKey, userId)
if (chatId != null) intent = intent.putExtra(ChatIdKey, chatId)
return TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent)
getPendingIntent(uniqueInt, PendingIntent.FLAG_IMMUTABLE)
return if (!broadcast) {
TaskStackBuilder.create(context).run {
addNextIntentWithParentStack(intent)
getPendingIntent(uniqueInt, PendingIntent.FLAG_IMMUTABLE)
}
} else {
PendingIntent.getBroadcast(SimplexApp.context, uniqueInt, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}
@@ -242,13 +274,26 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
* */
class NtfActionReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val userId = getUserIdFromIntent(intent)
val chatId = intent?.getStringExtra(ChatIdKey) ?: return
val cInfo = SimplexApp.context.chatModel.getChat(chatId)?.chatInfo
val m = SimplexApp.context.chatModel
when (intent.action) {
NotificationAction.ACCEPT_CONTACT_REQUEST.name -> {
if (cInfo !is ChatInfo.ContactRequest) return
acceptContactRequest(cInfo, SimplexApp.context.chatModel)
SimplexApp.context.chatModel.controller.ntfManager.cancelNotificationsForChat(chatId)
val isCurrentUser = m.currentUser.value?.userId == userId
val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) {
(m.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return
} else {
null
}
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
acceptContactRequest(apiId, cInfo, isCurrentUser, m)
m.controller.ntfManager.cancelNotificationsForChat(chatId)
}
RejectCallAction -> {
val invitation = m.callInvitations[chatId]
if (invitation != null) {
m.callManager.endCall(invitation = invitation)
}
}
else -> {
Log.e(TAG, "Unknown action. Make sure you provide action from NotificationAction enum")
File diff suppressed because it is too large Load Diff
@@ -1,29 +1,21 @@
package chat.simplex.app.views
import android.content.Context
import android.content.res.Configuration
import android.os.SystemClock
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
@@ -31,70 +23,18 @@ import chat.simplex.app.views.helpers.*
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
private val lastSuccessfulAuth: MutableState<Long?> = mutableStateOf(null)
@Composable
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
val lastSuccessfulAuth = remember { lastSuccessfulAuth }
BackHandler(onBack = {
lastSuccessfulAuth.value = null
close()
})
val authorized = remember { !chatModel.controller.appPrefs.performLA.get() }
val context = LocalContext.current
LaunchedEffect(lastSuccessfulAuth.value) {
if (!authorized && !authorizedPreviously(lastSuccessfulAuth)) {
runAuth(lastSuccessfulAuth, context)
}
}
if (authorized || authorizedPreviously(lastSuccessfulAuth)) {
LaunchedEffect(Unit) {
// Update auth each time user visits this screen in authenticated state just to prolong authorized time
lastSuccessfulAuth.value = SystemClock.elapsedRealtime()
}
TerminalLayout(
chatModel.terminalItems,
remember { chatModel.terminalItems },
composeState,
sendCommand = { sendCommand(chatModel, composeState) },
close
)
} else {
Surface(Modifier.fillMaxSize()) {
Column(Modifier.background(MaterialTheme.colors.background)) {
CloseSheetBar(close)
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(R.string.auth_unlock),
icon = Icons.Outlined.Lock,
click = {
runAuth(lastSuccessfulAuth, context)
}
)
}
}
}
}
}
private fun authorizedPreviously(lastSuccessfulAuth: State<Long?>): Boolean =
lastSuccessfulAuth.value?.let { SystemClock.elapsedRealtime() - it < 30_000 } ?: false
private fun runAuth(lastSuccessfulAuth: MutableState<Long?>, context: Context) {
authenticate(
generalGetString(R.string.auth_open_chat_console),
generalGetString(R.string.auth_log_in_using_credential),
context as FragmentActivity,
completed = { laResult ->
lastSuccessfulAuth.value = when (laResult) {
LAResult.Success, LAResult.Unavailable -> SystemClock.elapsedRealtime()
is LAResult.Error, LAResult.Failed -> null
}
}
)
}
private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) {
@@ -102,9 +42,9 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
val prefPerformLA = chatModel.controller.appPrefs.performLA.get()
val s = composeState.value.message
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
val resp = CR.ChatCmdError(ChatError.ChatErrorChat(ChatErrorType.╨бommandError("Failed reading: empty")))
chatModel.terminalItems.add(TerminalItem.cmd(CC.Console(s)))
chatModel.terminalItems.add(TerminalItem.resp(resp))
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.╨бommandError("Failed reading: empty")))
chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s)))
chatModel.addTerminalItem(TerminalItem.resp(resp))
composeState.value = ComposeState(useLinkPreviews = false)
} else {
withApi {
@@ -135,7 +75,21 @@ fun TerminalLayout(
topBar = { CloseSheetBar(close) },
bottomBar = {
Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(composeState, false, sendCommand, ::onMessageChange, { _, _, _ -> }, textStyle)
SendMsgView(
composeState = composeState,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
allowVoiceToContact = {},
sendMessage = sendCommand,
sendLiveMessage = null,
updateLiveMessage = null,
onMessageChange = ::onMessageChange,
textStyle = textStyle
)
}
},
modifier = Modifier.navigationBarsWithImePadding()
@@ -160,7 +114,8 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
DisposableEffect(Unit) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed() } }
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
val context = LocalContext.current
LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item ->
Text(
@@ -171,7 +126,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier
.fillMaxWidth()
.clickable {
ModalManager.shared.showModal {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
}
@@ -38,7 +38,7 @@ fun isValidDisplayName(name: String) : Boolean {
}
@Composable
fun CreateProfilePanel(chatModel: ChatModel) {
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
@@ -72,10 +72,12 @@ fun CreateProfilePanel(chatModel: ChatModel) {
ProfileNameField(fullName)
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
SimpleButton(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
if (chatModel.users.isEmpty()) {
SimpleButton(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
}
Spacer(Modifier.fillMaxWidth().weight(1f))
@@ -83,7 +85,7 @@ fun CreateProfilePanel(chatModel: ChatModel) {
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value) }.padding(8.dp)
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
@@ -105,15 +107,22 @@ fun CreateProfilePanel(chatModel: ChatModel) {
}
}
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) {
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
withApi {
val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
)
chatModel.controller.startChat(user)
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
SimplexService.start(chatModel.controller.appContext)
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
) ?: return@withApi
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
close()
}
}
}
@@ -18,7 +18,7 @@ class CallManager(val chatModel: ChatModel) {
controller.ntfManager.notifyCallInvitation(invitation)
} else {
val contact = invitation.contact
controller.ntfManager.notifyMessageReceived(chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
controller.ntfManager.notifyMessageReceived(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
}
}
}
@@ -297,10 +297,10 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
InfoText(call.callState.text)
val connInfo =
if (call.connectionInfo == null) ""
else " (${call.connectionInfo.text})"
InfoText(call.encryptionStatus + connInfo)
val connInfo = call.connectionInfo
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
InfoText(call.encryptionStatus + connInfoText)
}
}
@@ -480,7 +480,10 @@ fun PreviewActiveCallOverlayVideo() {
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
peerMedia = CallMediaType.Video,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null),
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
)
),
dismiss = {},
toggleAudio = {},
@@ -501,7 +504,10 @@ fun PreviewActiveCallOverlayAudio() {
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
peerMedia = CallMediaType.Audio,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null),
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
)
),
dismiss = {},
toggleAudio = {},
@@ -1,5 +1,6 @@
package chat.simplex.app.views.call
import android.app.Activity
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
@@ -43,8 +44,7 @@ class IncomingCallActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activity = this
setContent { IncomingCallActivityView(vm.chatModel, activity) }
setContent { IncomingCallActivityView(vm.chatModel) }
unlockForIncomingCall()
}
@@ -83,11 +83,12 @@ fun getKeyguardManager(context: Context): KeyguardManager =
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
@Composable
fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
fun IncomingCallActivityView(m: ChatModel) {
val switchingCall = m.switchingCall.value
val invitation = m.activeCallInvitation.value
val call = m.activeCall.value
val showCallView = m.showCallView.value
val activity = LocalContext.current as Activity
LaunchedEffect(invitation, call, switchingCall, showCallView) {
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
Log.d(TAG, "IncomingCallActivityView: finishing activity")
@@ -105,36 +106,43 @@ fun IncomingCallActivityView(m: ChatModel, activity: IncomingCallActivity) {
if (invitation != null) IncomingCallAlertView(invitation, m)
}
} else if (invitation != null) {
IncomingCallLockScreenAlert(invitation, m, activity)
IncomingCallLockScreenAlert(invitation, m)
}
}
}
}
@Composable
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel, activity: IncomingCallActivity) {
fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatModel) {
val cm = chatModel.callManager
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
var callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) }
LaunchedEffect(true) { SoundPlayer.shared.start(cxt, scope, sound = true) }
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
val callOnLockScreen by remember { mutableStateOf(chatModel.controller.appPrefs.callOnLockScreen.get()) }
val context = LocalContext.current
DisposableEffect(Unit) {
onDispose {
// Cancel notification whatever happens next since otherwise sound from notification and from inside the app can co-exist
chatModel.controller.ntfManager.cancelCallNotification()
}
}
IncomingCallLockScreenAlertLayout(
invitation,
callOnLockScreen,
chatModel,
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { chatModel.activeCallInvitation.value = null },
ignoreCall = {
chatModel.activeCallInvitation.value = null
chatModel.controller.ntfManager.cancelCallNotification()
},
acceptCall = { cm.acceptIncomingCall(invitation = invitation) },
openApp = {
SoundPlayer.shared.stop()
var intent = Intent(activity, MainActivity::class.java)
val intent = Intent(context, MainActivity::class.java)
.setAction(OpenChatAction)
.putExtra("userId", invitation.user.userId)
.putExtra("chatId", invitation.contact.id)
activity.startActivity(intent)
activity.finish()
context.startActivity(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getKeyguardManager(activity).requestDismissKeyguard(activity, null)
getKeyguardManager(context).requestDismissKeyguard((context as Activity), null)
}
(context as Activity).finish()
}
)
}
@@ -143,6 +151,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
fun IncomingCallLockScreenAlertLayout(
invitation: RcvCallInvitation,
callOnLockScreen: CallOnLockScreen?,
chatModel: ChatModel,
rejectCall: () -> Unit,
ignoreCall: () -> Unit,
acceptCall: () -> Unit,
@@ -154,7 +163,7 @@ fun IncomingCallLockScreenAlertLayout(
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
IncomingCallInfo(invitation)
IncomingCallInfo(invitation, chatModel)
Spacer(Modifier.fillMaxHeight().weight(1f))
if (callOnLockScreen == CallOnLockScreen.ACCEPT) {
ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
@@ -211,12 +220,14 @@ fun PreviewIncomingCallLockScreenAlert() {
.fillMaxSize()) {
IncomingCallLockScreenAlertLayout(
invitation = RcvCallInvitation(
user = User.sampleData,
contact = Contact.sampleData,
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
sharedKey = null,
callTs = Clock.System.now()
),
callOnLockScreen = null,
chatModel = SimplexApp.context.chatModel,
rejectCall = {},
ignoreCall = {},
acceptCall = {},
@@ -17,9 +17,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Contact
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.usersettings.ProfilePreview
import kotlinx.datetime.Clock
@@ -32,8 +33,12 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
IncomingCallAlertLayout(
invitation,
chatModel,
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = { chatModel.activeCallInvitation.value = null },
ignoreCall = {
chatModel.activeCallInvitation.value = null
chatModel.controller.ntfManager.cancelCallNotification()
},
acceptCall = { cm.acceptIncomingCall(invitation = invitation) }
)
}
@@ -41,13 +46,14 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
@Composable
fun IncomingCallAlertLayout(
invitation: RcvCallInvitation,
chatModel: ChatModel,
rejectCall: () -> Unit,
ignoreCall: () -> Unit,
acceptCall: () -> Unit
) {
val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight
Column(Modifier.fillMaxWidth().background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) {
IncomingCallInfo(invitation)
IncomingCallInfo(invitation, chatModel)
Spacer(Modifier.height(8.dp))
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
Row(Modifier.fillMaxWidth().weight(1f), verticalAlignment = Alignment.CenterVertically) {
@@ -63,9 +69,13 @@ fun IncomingCallAlertLayout(
}
@Composable
fun IncomingCallInfo(invitation: RcvCallInvitation) {
fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
@Composable fun CallIcon(icon: ImageVector, descr: String) = Icon(icon, descr, tint = SimplexGreen)
Row {
Row(verticalAlignment = Alignment.CenterVertically) {
if (chatModel.users.size > 1) {
ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondary)
Spacer(Modifier.width(4.dp))
}
if (invitation.callType.media == CallMediaType.Video) CallIcon(Icons.Filled.Videocam, stringResource(R.string.icon_descr_video_call))
else CallIcon(Icons.Filled.Phone, stringResource(R.string.icon_descr_audio_call))
Spacer(Modifier.width(4.dp))
@@ -98,11 +108,13 @@ fun PreviewIncomingCallAlertLayout() {
SimpleXTheme {
IncomingCallAlertLayout(
invitation = RcvCallInvitation(
user = User.sampleData,
contact = Contact.sampleData,
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
sharedKey = null,
callTs = Clock.System.now()
),
chatModel = SimplexApp.context.chatModel,
rejectCall = {},
ignoreCall = {},
acceptCall = {}
@@ -1,15 +1,19 @@
package chat.simplex.app.views.call
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import androidx.compose.ui.text.toUpperCase
import chat.simplex.app.*
import chat.simplex.app.model.Contact
import chat.simplex.app.model.User
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URI
import java.util.*
import kotlin.collections.ArrayList
data class Call(
val contact: Contact,
@@ -61,39 +65,39 @@ enum class CallState {
}
}
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
@Serializable
sealed class WCallCommand {
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
@Serializable @SerialName("start") class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("end") object End: WCallCommand()
}
@Serializable
sealed class WCallResponse {
@Serializable @SerialName("capabilities") class Capabilities(val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("answer") class Answer(val answer: String, val iceCandidates: String): WCallResponse()
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
@Serializable @SerialName("capabilities") data class Capabilities(val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("answer") data class Answer(val answer: String, val iceCandidates: String): WCallResponse()
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
@Serializable @SerialName("ended") object Ended: WCallResponse()
@Serializable @SerialName("ok") object Ok: WCallResponse()
@Serializable @SerialName("error") class Error(val message: String): WCallResponse()
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
}
@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
@Serializable data class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
@Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
val callTypeText: String get() = generalGetString(when(callType.media) {
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
@@ -103,19 +107,32 @@ sealed class WCallResponse {
CallMediaType.Audio -> R.string.incoming_audio_call
})
}
@Serializable class CallCapabilities(val encryption: Boolean)
@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
val text: String @Composable get() = when {
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
stringResource(R.string.call_connection_peer_to_peer)
localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay ->
stringResource(R.string.call_connection_via_relay)
else ->
"${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}"
@Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
val text: String @Composable get() {
val local = localCandidate?.candidateType
val remote = remoteCandidate?.candidateType
return when {
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
stringResource(R.string.call_connection_peer_to_peer)
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
stringResource(R.string.call_connection_via_relay)
else ->
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
}
}
val protocolText: String get() {
val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown"
val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)"
return if (local == remote) localText else "$localText / $remote"
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
@@ -150,7 +167,7 @@ enum class VideoCamera {
}
@Serializable
class ConnectionState(
data class ConnectionState(
val connectionState: String,
val iceConnectionState: String,
val iceGatheringState: String,
@@ -158,20 +175,22 @@ class ConnectionState(
)
// the servers are expected in this format:
// stun:stun.simplex.im:443
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443
// stun:stun.simplex.im:443?transport=tcp
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
fun parseRTCIceServer(str: String): RTCIceServer? {
var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "turn:")
s = replaceScheme(s, "turns:")
val u = runCatching { URI(s) }.getOrNull()
if (u != null) {
val scheme = u.scheme
val host = u.host
val port = u.port
if (u.path == "" && (scheme == "stun" || scheme == "turn")) {
if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) {
val userInfo = u.userInfo?.split(":")
val query = if (u.query == null || u.query == "") "" else "?${u.query}"
return RTCIceServer(
urls = listOf("$scheme:$host:$port"),
urls = listOf("$scheme:$host:$port$query"),
username = userInfo?.getOrNull(0),
credential = userInfo?.getOrNull(1)
)
@@ -35,9 +35,10 @@ import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import chat.simplex.app.views.usersettings.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
@Composable
fun ChatInfoView(
@@ -46,26 +47,65 @@ fun ChatInfoView(
connStats: ConnectionStats?,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
close: () -> Unit,
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
ChatInfoLayout(
chat,
contact,
connStats,
contactNetworkStatus.value,
customUserProfile,
localAlias,
connectionCode,
developerTools,
onLocalAliasChanged = {
setContactAlias(chat.chatInfo.apiId, it, chatModel)
},
openPreferences = {
ModalManager.shared.showCustomModal { close ->
val user = chatModel.currentUser.value
if (user != null) {
ContactPreferencesView(chatModel, user, contact.contactId, close)
}
}
},
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
switchContactAddress = {
showSwitchContactAddressAlert(chatModel, contact.contactId)
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
VerifyCodeView(
ct.displayName,
connectionCode,
ct.verified,
verify = { code ->
chatModel.controller.apiVerifyContact(ct.contactId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.updateContact(
ct.copy(
activeConn = ct.activeConn.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)
)
r
}
},
close,
)
}
}
}
)
}
@@ -113,13 +153,17 @@ fun ChatInfoLayout(
chat: Chat,
contact: Contact,
connStats: ConnectionStats?,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
developerTools: Boolean,
onLocalAliasChanged: (String) -> Unit,
openPreferences: () -> Unit,
deleteContact: () -> Unit,
clearChat: () -> Unit,
switchContactAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
Column(
Modifier
@@ -144,19 +188,26 @@ fun ChatInfoLayout(
}
SectionSpacer()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
if (developerTools) {
SwitchAddressButton(switchContactAddress)
SectionView {
if (connectionCode != null) {
VerifyCodeButton(contact.verified, verifyClicked)
SectionDivider()
}
ContactPreferencesButton(openPreferences)
}
SectionSpacer()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchContactAddress)
SectionDivider()
if (connStats != null) {
SectionItemView({
AlertManager.shared.showAlertMsg(
generalGetString(R.string.network_status),
chat.serverInfo.networkStatus.statusExplanation
contactNetworkStatus.statusExplanation
)}) {
NetworkStatusRow(chat.serverInfo.networkStatus)
NetworkStatusRow(contactNetworkStatus)
}
val rcvServers = connStats.rcvServers
if (rcvServers != null && rcvServers.isNotEmpty()) {
@@ -196,13 +247,17 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
horizontalAlignment = Alignment.CenterHorizontally
) {
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Text(
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(bottom = 8.dp)
)
Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
if (contact.verified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
}
Text(
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
Text(
cInfo.fullName, style = MaterialTheme.typography.h2,
@@ -264,7 +319,7 @@ fun LocalAliasEditor(
}
@Composable
fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -296,14 +351,14 @@ fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
}
@Composable
fun ServerImage(networkStatus: Chat.NetworkStatus) {
private fun ServerImage(networkStatus: NetworkStatus) {
Box(Modifier.size(18.dp)) {
when (networkStatus) {
is Chat.NetworkStatus.Connected ->
is NetworkStatus.Connected ->
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
is Chat.NetworkStatus.Disconnected ->
is NetworkStatus.Disconnected ->
Icon(Icons.Filled.Pending, stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight)
is Chat.NetworkStatus.Error ->
is NetworkStatus.Error ->
Icon(Icons.Filled.Error, stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight)
else -> Icon(Icons.Outlined.Circle, stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight)
}
@@ -327,6 +382,25 @@ fun SwitchAddressButton(onClick: () -> Unit) {
}
}
@Composable
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
SettingsActionItem(
if (contactVerified) Icons.Outlined.VerifiedUser else Icons.Outlined.Shield,
stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code),
click = onClick,
iconColor = HighOrLowlight,
)
}
@Composable
private fun ContactPreferencesButton(onClick: () -> Unit) {
SettingsActionItem(
Icons.Outlined.ToggleOn,
stringResource(R.string.contact_preferences),
click = onClick
)
}
@Composable
fun ClearChatButton(onClick: () -> Unit) {
SettingsActionItem(
@@ -339,7 +413,7 @@ fun ClearChatButton(onClick: () -> Unit) {
}
@Composable
fun DeleteContactButton(onClick: () -> Unit) {
private fun DeleteContactButton(onClick: () -> Unit) {
SettingsActionItem(
Icons.Outlined.Delete,
stringResource(R.string.button_delete_contact),
@@ -377,18 +451,21 @@ fun PreviewChatInfoLayout() {
ChatInfoLayout(
chat = Chat(
chatInfo = ChatInfo.Direct.sampleData,
chatItems = arrayListOf(),
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
chatItems = arrayListOf()
),
Contact.sampleData,
localAlias = "",
connectionCode = "123",
developerTools = false,
connStats = null,
contactNetworkStatus = NetworkStatus.Connected(),
onLocalAliasChanged = {},
customUserProfile = null,
openPreferences = {},
deleteContact = {},
clearChat = {},
switchContactAddress = {},
verifyClicked = {},
)
}
}
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
@@ -12,8 +13,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.mapSaver
@@ -56,7 +56,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val user = chatModel.currentUser.value
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = rememberSaveable(saver = ComposeState.saver()) {
mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews))
mutableStateOf(
if (chatModel.draftChatId.value == chatId && chatModel.draft.value != null) {
chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
} else {
ComposeState(useLinkPreviews = useLinkPreviews)
}
)
}
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
@@ -77,16 +83,23 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
}
}
launch {
// .toList() is important for making observation working
snapshotFlow { chatModel.chats.toList() }
.distinctUntilChanged()
.collect { chats ->
chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }.let {
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
if (it?.chatInfo != activeChat.value?.chatInfo) {
activeChat.value = it
}}
snapshotFlow {
/**
* It's possible that in some cases concurrent modification can happen on [ChatModel.chats] list.
* In this case only error log will be printed here (no crash).
* TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that
* */
try {
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
} catch (e: ConcurrentModificationException) {
Log.e(TAG, e.stackTraceToString())
null
}
}
.distinctUntilChanged()
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
.filter { it?.chatInfo != activeChat.value?.chatInfo && it != null }
.collect { activeChat.value = it }
}
}
val view = LocalView.current
@@ -119,6 +132,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.chatItems,
searchText,
useLinkPreviews = useLinkPreviews,
linkMode = chatModel.simplexLinkMode.value,
chatModelIncognito = chatModel.incognito.value,
back = {
hideKeyboard(view)
@@ -128,16 +142,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
info = {
hideKeyboard(view)
withApi {
val cInfo = chat.chatInfo
if (cInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(cInfo.apiId)
if (chat.chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)
ModalManager.shared.showModalCloseable(true) { close ->
ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close)
remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
}
}
} else if (cInfo is ChatInfo.Group) {
setGroupMembers(cInfo.groupInfo, chatModel)
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
var groupLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
ModalManager.shared.showModalCloseable(true) { close ->
GroupChatInfoView(chatModel, close)
GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close)
}
}
}
@@ -146,8 +163,21 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
hideKeyboard(view)
withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
} else {
member to null
}
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
GroupMemberInfoView(groupInfo, member, stats, chatModel, close, close)
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
}
}
}
},
@@ -163,17 +193,27 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
deleteMessage = { itemId, mode ->
withApi {
val cInfo = chat.chatInfo
val toItem = chatModel.controller.apiDeleteChatItem(
val r = chatModel.controller.apiDeleteChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = itemId,
mode = mode
)
if (toItem != null) chatModel.removeChatItem(cInfo, toItem.chatItem)
if (r != null) {
val toChatItem = r.toChatItem
if (toChatItem == null) {
chatModel.removeChatItem(cInfo, r.deletedChatItem.chatItem)
} else {
chatModel.upsertChatItem(cInfo, toChatItem.chatItem)
}
}
}
},
receiveFile = { fileId ->
withApi { chatModel.controller.receiveFile(fileId) }
val user = chatModel.currentUser.value
if (user != null) {
withApi { chatModel.controller.receiveFile(user, fileId) }
}
},
joinGroup = { groupId ->
withApi { chatModel.controller.apiJoinGroup(groupId) }
@@ -195,19 +235,24 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
},
acceptFeature = { contact, feature, param ->
withApi {
chatModel.controller.allowFeatureToContact(contact, feature, param)
}
},
addMembers = { groupInfo ->
hideKeyboard(view)
withApi {
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, chatModel, close)
AddGroupMembersView(groupInfo, false, chatModel, close)
}
}
},
markRead = { range, unreadCountAfter ->
chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter)
chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id)
withApi {
withBGApi {
chatModel.controller.apiChatRead(
chat.chatInfo.chatType,
chat.chatInfo.apiId,
@@ -240,6 +285,7 @@ fun ChatLayout(
chatItems: List<ChatItem>,
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
chatModelIncognito: Boolean,
back: () -> Unit,
info: () -> Unit,
@@ -250,6 +296,7 @@ fun ChatLayout(
joinGroup: (Long) -> Unit,
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
addMembers: (GroupInfo) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
@@ -289,8 +336,8 @@ fun ChatLayout(
BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) {
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, markRead, setFloatingButton, onComposed,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
)
}
}
@@ -413,10 +460,15 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
cInfo.displayName, fontWeight = FontWeight.SemiBold,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
Row(verticalAlignment = Alignment.CenterVertically) {
if ((cInfo as? ChatInfo.Direct)?.contact?.verified == true) {
ContactVerifiedShield()
}
Text(
cInfo.displayName, fontWeight = FontWeight.SemiBold,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
}
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.localAlias.isEmpty()) {
Text(
cInfo.fullName,
@@ -427,6 +479,11 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
}
}
@Composable
private fun ContactVerifiedShield() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight)
}
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
val CIListStateSaver = run {
@@ -447,6 +504,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
chatItems: List<ChatItem>,
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
chatModelIncognito: Boolean,
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadPrevMessages: (ChatInfo) -> Unit,
@@ -454,13 +512,14 @@ fun BoxWithConstraintsScope.ChatItemsList(
receiveFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: () -> Unit,
) {
val listState = rememberLazyListState()
val scope = rememberCoroutineScope()
ScrollToBottom(chat.id, listState)
ScrollToBottom(chat.id, listState, chatItems)
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
// Scroll to bottom when search value changes from something to nothing and back
LaunchedEffect(searchValue.value.isEmpty()) {
@@ -479,7 +538,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } }
val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } }
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
val scrollToItem: (Long) -> Unit = { itemId: Long ->
val index = reversedChatItems.indexOfFirst { it.id == itemId }
@@ -498,7 +557,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
itemsIndexed(reversedChatItems) { i, cItem ->
itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem ->
CompositionLocalProvider(
// Makes horizontal and vertical scrolling to coexist nicely.
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
@@ -518,7 +577,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
scope.launch {
if (composeState.value.editing) {
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))
}
}
@@ -559,11 +618,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} else {
Spacer(Modifier.size(42.dp))
}
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
} else {
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
} else { // direct message
@@ -574,7 +633,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent) 12.dp else 76.dp,
).then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
@@ -593,7 +652,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
@Composable
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState) {
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: List<ChatItem>) {
val scope = rememberCoroutineScope()
// Helps to scroll to bottom after moving from Group to Direct chat
// and prevents scrolling to bottom on orientation change
@@ -605,6 +664,23 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState) {
// Don't autoscroll next time until it will be needed
shouldAutoScroll = false to chatId
}
val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() }
/*
* Since we use key with each item in LazyColumn, LazyColumn will not autoscroll to bottom item. We need to do it ourselves.
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
* */
LaunchedEffect(Unit) {
snapshotFlow { chatItems.lastOrNull()?.id }
.distinctUntilChanged()
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
.collect {
if (listState.firstVisibleItemIndex == 0) {
listState.animateScrollToItem(0)
} else {
listState.animateScrollBy(scrollDistance)
}
}
}
}
@Composable
@@ -948,6 +1024,7 @@ fun PreviewChatLayout() {
chatItems = chatItems,
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
chatModelIncognito = false,
back = {},
info = {},
@@ -958,6 +1035,7 @@ fun PreviewChatLayout() {
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
addMembers = { _ -> },
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },
@@ -1005,6 +1083,7 @@ fun PreviewGroupChatLayout() {
chatItems = chatItems,
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
chatModelIncognito = false,
back = {},
info = {},
@@ -1015,6 +1094,7 @@ fun PreviewGroupChatLayout() {
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
addMembers = { _ -> },
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },
@@ -1,8 +1,10 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.app.views.chat
import ComposeVoiceView
import ComposeFileView
import ComposeVoiceView
import android.Manifest
import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
@@ -32,27 +34,25 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.*
import java.io.File
import java.nio.file.Files
@Serializable
sealed class ComposePreview {
@Serializable object NoPreview: ComposePreview()
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
@Serializable class ImagePreview(val images: List<String>): ComposePreview()
@Serializable class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
@Serializable class FilePreview(val fileName: String): ComposePreview()
@Serializable class ImagePreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
@Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview()
}
@Serializable
@@ -62,16 +62,26 @@ sealed class ComposeContextItem {
@Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
}
@Serializable
data class LiveMessage(
val chatItem: ChatItem,
val typedMsg: String,
val sentMsg: String,
val sent: Boolean
)
@Serializable
data class ComposeState(
val message: String = "",
val liveMessage: LiveMessage? = null,
val preview: ComposePreview = ComposePreview.NoPreview,
val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem,
val inProgress: Boolean = false,
val useLinkPreviews: Boolean
) {
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this(
constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this(
editingItem.content.text,
liveMessage,
chatItemPreview(editingItem),
ComposeContextItem.EditingItem(editingItem),
useLinkPreviews = useLinkPreviews
@@ -89,10 +99,13 @@ data class ComposeState(
is ComposePreview.ImagePreview -> true
is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true
else -> message.isNotEmpty()
else -> message.isNotEmpty() || liveMessage != null
}
hasContent && !inProgress
}
val endLiveDisabled: Boolean
get() = liveMessage != null && message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
val linkPreviewAllowed: Boolean
get() =
when (preview) {
@@ -108,6 +121,19 @@ data class ComposeState(
else -> null
}
val attachmentDisabled: Boolean
get() {
if (editing || liveMessage != null) return true
return when (preview) {
ComposePreview.NoPreview -> false
is ComposePreview.CLinkPreview -> false
else -> true
}
}
val empty: Boolean
get() = message.isEmpty() && preview is ComposePreview.NoPreview
companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
save = { json.encodeToString(serializer(), it.value) },
@@ -118,16 +144,24 @@ data class ComposeState(
}
}
sealed class RecordingState {
object NotStarted: RecordingState()
class Started(val filePath: String, val progressMs: Int = 0): RecordingState()
class Finished(val filePath: String, val durationMs: Int): RecordingState()
val filePathNullable: String?
get() = (this as? Started)?.filePath
}
fun chatItemPreview(chatItem: ChatItem): ComposePreview {
val fileName = chatItem.file?.fileName ?: ""
return when (val mc = chatItem.content.msgContent) {
is MsgContent.MCText -> ComposePreview.NoPreview
is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview)
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image))
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = chatItem.file?.fileName ?: "", mc.duration / 1000, true)
is MsgContent.MCFile -> {
val fileName = chatItem.file?.fileName ?: ""
ComposePreview.FilePreview(fileName)
}
// TODO: include correct type
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true)
is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName))
is MsgContent.MCUnknown, null -> ComposePreview.NoPreview
}
}
@@ -148,26 +182,17 @@ fun ComposeView(
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
// attachments
val chosenContent = rememberSaveable { mutableStateOf<List<UploadContent>>(emptyList()) }
val audioSaver = Saver<MutableState<Pair<Uri, Int>?>, Pair<String, Int>> (
save = { it.value.let { if (it == null) null else it.first.toString() to it.second } },
restore = { mutableStateOf(Uri.parse(it.first) to it.second) }
)
val chosenAudio = rememberSaveable(saver = audioSaver) { mutableStateOf(null) }
val chosenFile = rememberSaveable { mutableStateOf<Uri?>(null) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
chosenContent.value = listOf(UploadContent.SimpleImage(uri))
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview)))
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
}
}
val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
if (isGranted) {
cameraLauncher.launch(null)
cameraLauncher.launchWithFallback()
} else {
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
}
@@ -209,8 +234,7 @@ fun ComposeView(
}
if (imagesPreview.isNotEmpty()) {
chosenContent.value = content
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview, content))
}
}
val processPickedFile = { uri: Uri?, text: String? ->
@@ -219,8 +243,7 @@ fun ComposeView(
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) {
chosenFile.value = uri
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
}
} else {
AlertManager.shared.showAlertMsg(
@@ -233,13 +256,14 @@ fun ComposeView(
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) {
AttachmentOption.TakePhoto -> {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
cameraLauncher.launch(null)
cameraLauncher.launchWithFallback()
}
else -> {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
@@ -310,128 +334,160 @@ fun ComposeView(
cancelledLinks.clear()
}
fun checkLinkPreview(): MsgContent {
fun clearState(live: Boolean = false) {
if (live) {
composeState.value = composeState.value.copy(inProgress = false)
} else {
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
resetLinkPreview()
}
recState.value = RecordingState.NotStarted
textStyle.value = smallFont
chatModel.removeLiveDummy()
}
fun deleteUnusedFiles() {
chatModel.filesToDelete.forEach { it.delete() }
chatModel.filesToDelete.clear()
}
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
quotedItemId = quoted,
mc = mc,
live = live
)
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem?.chatItem
}
suspend fun sendMessageAsync(text: String?, live: Boolean): ChatItem? {
val cInfo = chat.chatInfo
val cs = composeState.value
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
val url = parseMessage(cs.message)
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(cs.message, preview = lp)
} else {
MsgContent.MCText(cs.message)
var sent: ChatItem?
val msgText = text ?: cs.message
fun sending() {
composeState.value = composeState.value.copy(inProgress = true)
}
fun checkLinkPreview(): MsgContent {
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
val url = parseMessage(msgText)
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(msgText, preview = lp)
} else {
MsgContent.MCText(msgText)
}
}
else -> MsgContent.MCText(msgText)
}
}
fun updateMsgContent(msgContent: MsgContent): MsgContent {
return when (msgContent) {
is MsgContent.MCText -> checkLinkPreview()
is MsgContent.MCLink -> checkLinkPreview()
is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image)
is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration)
is MsgContent.MCFile -> MsgContent.MCFile(msgText)
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json)
}
}
suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
val oldMsgContent = ei.content.msgContent
if (oldMsgContent != null) {
val updatedItem = chatModel.controller.apiUpdateChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = ei.meta.itemId,
mc = updateMsgContent(oldMsgContent),
live = live
)
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
return updatedItem?.chatItem
}
return null
}
val liveMessage = cs.liveMessage
if (!live) {
if (liveMessage != null) composeState.value = cs.copy(liveMessage = null)
sending()
}
if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem
sent = updateMessage(ei, cInfo, live)
} else if (liveMessage != null && liveMessage.sent) {
sent = updateMessage(liveMessage.chatItem, cInfo, live)
} else {
val msgs: ArrayList<MsgContent> = ArrayList()
val files: ArrayList<String> = ArrayList()
when (val preview = cs.preview) {
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.ImagePreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
}
if (file != null) {
files.add(file)
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
}
}
}
is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath)
val actualFile = File(getAppFilePath(SimplexApp.context, tmpFile.name.replaceAfter(RecorderNative.extension, "")))
withContext(Dispatchers.IO) {
Files.move(tmpFile.toPath(), actualFile.toPath())
}
files.add(actualFile.name)
deleteUnusedFiles()
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
}
is ComposePreview.FilePreview -> {
val file = saveFileFromUri(context, preview.uri)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
}
}
}
else -> MsgContent.MCText(cs.message)
val quotedItemId: Long? = when (cs.contextItem) {
is ComposeContextItem.QuotedItem -> cs.contextItem.chatItem.id
else -> null
}
sent = null
msgs.forEachIndexed { index, content ->
if (index > 0) delay(100)
sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index),
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
)
}
if (sent == null && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview || cs.preview is ComposePreview.VoicePreview)) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
}
}
}
fun updateMsgContent(msgContent: MsgContent): MsgContent {
val cs = composeState.value
return when (msgContent) {
is MsgContent.MCText -> checkLinkPreview()
is MsgContent.MCLink -> checkLinkPreview()
is MsgContent.MCImage -> MsgContent.MCImage(cs.message, image = msgContent.image)
is MsgContent.MCVoice -> MsgContent.MCVoice(cs.message, duration = msgContent.duration)
is MsgContent.MCFile -> MsgContent.MCFile(cs.message)
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = cs.message, json = msgContent.json)
}
}
fun clearState() {
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
textStyle.value = smallFont
chosenContent.value = emptyList()
chosenAudio.value = null
chosenFile.value = null
linkUrl.value = null
prevLinkUrl.value = null
pendingLinkUrl.value = null
cancelledLinks.clear()
clearState(live)
return sent
}
fun sendMessage() {
composeState.value = composeState.value.copy(inProgress = true)
val cInfo = chat.chatInfo
val cs = composeState.value
when (val contextItem = cs.contextItem) {
is ComposeContextItem.EditingItem -> {
val ei = contextItem.chatItem
val oldMsgContent = ei.content.msgContent
if (oldMsgContent != null) {
withApi {
val updatedItem = chatModel.controller.apiUpdateChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = ei.meta.itemId,
mc = updateMsgContent(oldMsgContent)
)
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
clearState()
}
}
}
else -> {
val msgs: ArrayList<MsgContent> = ArrayList()
val files: ArrayList<String> = ArrayList()
when (val preview = cs.preview) {
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(cs.message))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.ImagePreview -> {
chosenContent.value.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
}
if (file != null) {
files.add(file)
msgs.add(MsgContent.MCImage(if (msgs.isEmpty()) cs.message else "", preview.images[index]))
}
}
}
is ComposePreview.VoicePreview -> {
val chosenAudioVal = chosenAudio.value
if (chosenAudioVal != null) {
val file = chosenAudioVal.first.toFile().name
files.add((file))
chatModel.filesToDelete.remove(chosenAudioVal.first.toFile())
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) cs.message else "", chosenAudioVal.second / 1000))
}
}
is ComposePreview.FilePreview -> {
val chosenFileVal = chosenFile.value
if (chosenFileVal != null) {
val file = saveFileFromUri(context, chosenFileVal)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) cs.message else ""))
}
}
}
}
val quotedItemId: Long? = when (contextItem) {
is ComposeContextItem.QuotedItem -> contextItem.chatItem.id
else -> null
}
if (msgs.isNotEmpty()) {
withApi {
msgs.forEachIndexed { index, content ->
if (index > 0) delay(100)
val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = files.getOrNull(index),
quotedItemId = if (index == 0) quotedItemId else null,
mc = content
)
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
}
clearState()
}
} else {
clearState()
}
}
withBGApi {
sendMessageAsync(null, false)
}
}
@@ -450,11 +506,17 @@ fun ComposeView(
fun onAudioAdded(filePath: String, durationMs: Int, finished: Boolean) {
val file = File(filePath)
chosenAudio.value = file.toUri() to durationMs
chatModel.filesToDelete.add(file)
composeState.value = composeState.value.copy(preview = ComposePreview.VoicePreview(filePath, durationMs, finished))
}
fun allowVoiceToContact() {
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
withApi {
chatModel.controller.allowFeatureToContact(contact, ChatFeature.Voice)
}
}
fun cancelLinkPreview() {
val uri = composeState.value.linkPreview?.uri
if (uri != null) {
@@ -466,17 +528,70 @@ fun ComposeView(
fun cancelImages() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenContent.value = emptyList()
}
fun cancelVoice() {
val filePath = recState.value.filePathNullable
recState.value = RecordingState.NotStarted
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenContent.value = emptyList()
withBGApi {
RecorderNative.stopRecording?.invoke()
AudioPlayer.stop(filePath)
filePath?.let { File(it).delete() }
}
}
fun cancelFile() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenFile.value = null
}
fun truncateToWords(s: String): String {
var acc = ""
val word = StringBuilder()
for (c in s) {
if (c.isLetter() || c.isDigit()) {
word.append(c)
} else {
acc = acc + word.toString() + c
word.clear()
}
}
return acc
}
suspend fun sendLiveMessage() {
val cs = composeState.value
val typedMsg = cs.message
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
val ci = sendMessageAsync(typedMsg, live = true)
if (ci != null) {
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))
}
}
fun liveMessageToSend(lm: LiveMessage, t: String): String? {
val s = if (t != lm.typedMsg) truncateToWords(t) else t
return if (s != lm.sentMsg) s else null
}
suspend fun updateLiveMessage() {
val typedMsg = composeState.value.message
val liveMessage = composeState.value.liveMessage
if (liveMessage != null) {
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
if (sentMsg != null) {
val ci = sendMessageAsync(sentMsg, live = true)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
}
} else if (liveMessage.typedMsg != typedMsg) {
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
}
}
}
@Composable
@@ -518,6 +633,9 @@ fun ComposeView(
}
LaunchedEffect(chatModel.sharedContent.value) {
// Important. If it's null, don't do anything, chat is not closed yet but will be after a moment
if (chatModel.chatId.value == null) return@LaunchedEffect
when (val shared = chatModel.sharedContent.value) {
is SharedContent.Text -> onMessageChange(shared.text)
is SharedContent.Images -> processPickedImage(shared.uris, shared.text)
@@ -538,27 +656,95 @@ fun ComposeView(
modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
val attachEnabled = !composeState.value.editing && composeState.value.preview !is ComposePreview.VoicePreview
IconButton(showChooseAttachment, enabled = attachEnabled) {
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) {
Icon(
Icons.Filled.AttachFile,
contentDescription = stringResource(R.string.attach),
tint = if (attachEnabled) MaterialTheme.colors.primary else HighOrLowlight,
tint = if (!composeState.value.attachmentDisabled) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
)
}
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
LaunchedEffect(allowedVoiceByPrefs) {
if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
// Voice was disabled right when this user records it, just cancel it
cancelVoice()
}
}
val needToAllowVoiceToContact = remember(chat.chatInfo) {
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) {
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
contactPreference.allow == FeatureAllowed.YES
}
}
LaunchedEffect(Unit) {
snapshotFlow { recState.value }
.distinctUntilChanged()
.collect {
when(it) {
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
is RecordingState.Finished -> onAudioAdded(it.filePath, it.durationMs, true)
is RecordingState.NotStarted -> {}
}
}
}
fun clearCurrentDraft() {
if (chatModel.draftChatId.value == chat.id) {
chatModel.draft.value = null
chatModel.draftChatId.value = null
}
}
val activity = LocalContext.current as Activity
DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation
onDispose {
if (orientation == activity.resources.configuration.orientation) {
val cs = composeState.value
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage()
resetLinkPreview()
clearCurrentDraft()
deleteUnusedFiles()
} else if (!composeState.value.empty) {
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
}
chatModel.draft.value = composeState.value
chatModel.draftChatId.value = chat.id
} else {
clearCurrentDraft()
deleteUnusedFiles()
}
chatModel.removeLiveDummy()
}
}
}
SendMsgView(
composeState,
allowVoiceRecord = true,
showVoiceRecordIcon = true,
recState,
chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
needToAllowVoiceToContact,
allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
sendMessage = {
sendMessage()
resetLinkPreview()
},
::onMessageChange,
::onAudioAdded,
textStyle
sendLiveMessage = ::sendLiveMessage,
updateLiveMessage = ::updateLiveMessage,
cancelLiveMessage = {
composeState.value = composeState.value.copy(liveMessage = null)
chatModel.removeLiveDummy()
},
onMessageChange = ::onMessageChange,
textStyle = textStyle
)
}
}
@@ -14,42 +14,49 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.durationText
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.AudioInfoUpdater
import chat.simplex.app.views.chat.item.SentColorLight
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
fun ComposeVoiceView(filePath: String, durationMs: Int, finished: Boolean, cancelEnabled: Boolean, cancelVoice: () -> Unit) {
fun ComposeVoiceView(
filePath: String,
recordedDurationMs: Int,
finishedRecording: Boolean,
cancelEnabled: Boolean,
cancelVoice: () -> Unit
) {
BoxWithConstraints(Modifier
.fillMaxWidth()
) {
val audioPlaying = rememberSaveable { mutableStateOf(false) }
val audioInfo = rememberSaveable(saver = ProgressAndDuration.Saver) {
mutableStateOf(ProgressAndDuration(durationMs = durationMs))
}
LaunchedEffect(durationMs) {
audioInfo.value = audioInfo.value.copy(durationMs = durationMs)
}
val progress = rememberSaveable { mutableStateOf(0) }
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
val progressBarWidth = remember { Animatable(0f) }
LaunchedEffect(durationMs, finished) {
snapshotFlow { audioInfo.value }
LaunchedEffect(recordedDurationMs, finishedRecording) {
snapshotFlow { progress.value }
.distinctUntilChanged()
.collect {
val number = if (audioPlaying.value) audioInfo.value.progressMs else if (!finished) durationMs else 0
val new = if (audioPlaying.value || finished)
((number.toDouble() / durationMs) * maxWidth.value).dp
else
(((number.toDouble()) / MAX_VOICE_MILLIS_FOR_SENDING) * maxWidth.value).dp
progressBarWidth.animateTo(new.value, audioProgressBarAnimationSpec())
val startTime = when {
finishedRecording -> progress.value
else -> recordedDurationMs
}
val endTime = when {
finishedRecording -> duration.value
audioPlaying.value -> recordedDurationMs
else -> MAX_VOICE_MILLIS_FOR_SENDING
}
val to = ((startTime.toDouble() / endTime) * maxWidth.value).dp
progressBarWidth.animateTo(to.value, audioProgressBarAnimationSpec())
}
}
Spacer(
Modifier
.requiredWidth(progressBarWidth.value.dp)
.padding(top = 58.dp)
.height(2.dp)
.height(3.dp)
.background(MaterialTheme.colors.primary)
)
Row(
@@ -60,33 +67,35 @@ fun ComposeVoiceView(filePath: String, durationMs: Int, finished: Boolean, cance
.background(SentColorLight),
verticalAlignment = Alignment.CenterVertically
) {
val play = play@{
audioPlaying.value = AudioPlayer.start(filePath, audioInfo.value.progressMs) {
audioPlaying.value = false
}
}
val pause = {
audioInfo.value = ProgressAndDuration(AudioPlayer.pause(), audioInfo.value.durationMs)
audioPlaying.value = false
}
AudioInfoUpdater(filePath, audioPlaying, audioInfo)
IconButton({ if (!audioPlaying.value) play() else pause() }, enabled = finished) {
IconButton(
onClick = {
if (!audioPlaying.value) {
AudioPlayer.play(filePath, audioPlaying, progress, duration, false)
} else {
AudioPlayer.pause(audioPlaying, progress)
}
},
enabled = finishedRecording) {
Icon(
if (audioPlaying.value) Icons.Filled.Pause else Icons.Filled.PlayArrow,
stringResource(R.string.icon_descr_file),
Modifier
.padding(start = 4.dp, end = 2.dp)
.size(36.dp),
tint = if (finished) MaterialTheme.colors.primary else HighOrLowlight
tint = if (finishedRecording) MaterialTheme.colors.primary else HighOrLowlight
)
}
val numberInText = remember(durationMs, audioInfo.value) {
derivedStateOf { if (audioPlaying.value) audioInfo.value.progressMs / 1000 else durationMs / 1000 }
val numberInText = remember(recordedDurationMs, progress.value) {
derivedStateOf {
when {
finishedRecording && progress.value == 0 && !audioPlaying.value -> duration.value / 1000
finishedRecording -> progress.value / 1000
else -> recordedDurationMs / 1000
}
}
}
val text = "%02d:%02d".format(numberInText.value / 60, numberInText.value % 60)
Text(
text,
durationText(numberInText.value),
fontSize = 18.sp,
color = HighOrLowlight,
)
@@ -0,0 +1,232 @@
package chat.simplex.app.views.chat
import InfoRow
import SectionDivider
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggle
@Composable
fun ContactPreferencesView(
m: ChatModel,
user: User,
contactId: Long,
close: () -> Unit,
) {
val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
val ct = contact.value ?: return
var featuresAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(ct.contactId, prefs)
if (toContact != null) {
m.updateContact(toContact)
currentFeaturesAllowed = featuresAllowed
}
afterSave()
}
}
ModalView(
close = {
if (featuresAllowed == currentFeaturesAllowed) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
ContactPreferencesLayout(
featuresAllowed,
currentFeaturesAllowed,
user,
ct,
applyPrefs = { prefs ->
featuresAllowed = prefs
},
reset = {
featuresAllowed = currentFeaturesAllowed
},
savePrefs = ::savePrefs,
)
}
}
@Composable
private fun ContactPreferencesLayout(
featuresAllowed: ContactFeaturesAllowed,
currentFeaturesAllowed: ContactFeaturesAllowed,
user: User,
contact: Contact,
applyPrefs: (ContactFeaturesAllowed) -> Unit,
reset: () -> Unit,
savePrefs: () -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.contact_preferences))
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl ?: 86400))
}
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
}
SectionSpacer()
val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) }
FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) {
applyPrefs(featuresAllowed.copy(fullDelete = it))
}
SectionSpacer()
val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) }
FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) {
applyPrefs(featuresAllowed.copy(voice = it))
}
SectionSpacer()
ResetSaveButtons(
reset = reset,
save = savePrefs,
disabled = featuresAllowed == currentFeaturesAllowed
)
}
}
@Composable
private fun FeatureSection(
feature: ChatFeature,
userDefault: FeatureAllowed,
pref: ContactUserPreference,
allowFeature: State<ContactFeatureAllowed>,
onSelected: (ContactFeatureAllowed) -> Unit
) {
val enabled = FeatureEnabled.enabled(
feature.asymmetric,
user = SimpleChatPreference(allow = allowFeature.value.allowed),
contact = pref.contactPreference
)
SectionView(
feature.text.uppercase(),
icon = feature.iconFilled,
iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red,
leadingIcon = true,
) {
SectionItemView {
ExposedDropDownSettingRow(
generalGetString(R.string.chat_preferences_you_allow),
ContactFeatureAllowed.values(userDefault).map { it to it.text },
allowFeature,
icon = null,
onSelected = onSelected
)
}
SectionDivider()
InfoRow(
generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text
)
}
SectionTextFooter(feature.enabledDescription(enabled))
}
@Composable
private fun TimedMessagesFeatureSection(
featuresAllowed: ContactFeaturesAllowed,
pref: ContactUserPreferenceTimed,
allowFeature: State<Boolean>,
onTTLUpdated: (Int?) -> Unit,
onSelected: (Boolean, Int?) -> Unit
) {
val enabled = FeatureEnabled.enabled(
ChatFeature.TimedMessages.asymmetric,
user = TimedMessagesPreference(allow = if (allowFeature.value) FeatureAllowed.YES else FeatureAllowed.NO),
contact = pref.contactPreference
)
SectionView(
ChatFeature.TimedMessages.text.uppercase(),
icon = ChatFeature.TimedMessages.iconFilled,
iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red,
leadingIcon = true,
) {
SectionItemView {
PreferenceToggle(
generalGetString(R.string.chat_preferences_you_allow),
checked = allowFeature.value,
) { allow ->
onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
}
}
SectionDivider()
InfoRow(
generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text
)
SectionDivider()
if (featuresAllowed.timedMessagesAllowed) {
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
TimedMessagesTTLPicker(ttl, onTTLUpdated)
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
}
}
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
}
@Composable
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(reset, disabled = disabled) {
Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
SectionDivider()
SectionItemView(save, disabled = disabled) {
Text(stringResource(R.string.save_and_notify_contact), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
}
}
@Composable
fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) {
val ttlValues = TimedMessagesPreference.ttlValues
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
SectionItemView {
ExposedDropDownSettingRow(
generalGetString(R.string.delete_after),
values.map { it to TimedMessagesPreference.ttlText(it) },
selection,
onSelected = onSelected
)
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_contact),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -14,8 +14,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.CIDirection
import chat.simplex.app.model.ChatItem
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.*
@@ -53,6 +52,7 @@ fun ContextItemView(
MarkdownText(
contextItem.text, contextItem.formattedText,
sender = contextItem.memberDisplayName, senderBold = true, maxLines = 3,
linkMode = SimplexLinkMode.DESCRIPTION,
modifier = Modifier.fillMaxWidth(),
)
}
@@ -0,0 +1,53 @@
package chat.simplex.app.views.chat
import android.Manifest
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ScanCodeLayout(verifyCode, close)
}
@Composable
private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
Column(
Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.scan_code), false)
Box(
Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1F)
.padding(bottom = DEFAULT_PADDING)
) {
QRCodeScanner { text ->
verifyCode(text) {
if (it) {
close()
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.incorrect_code)
)
}
}
}
}
Text(stringResource(R.string.scan_code_from_contacts_app))
}
}
@@ -6,11 +6,15 @@ import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.text.InputType
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.*
import android.widget.EditText
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
@@ -21,15 +25,17 @@ import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.*
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.inputmethod.EditorInfoCompat
@@ -37,154 +43,124 @@ import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.*
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.ChatItem
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.*
import java.io.*
@Composable
fun SendMsgView(
composeState: MutableState<ComposeState>,
allowVoiceRecord: Boolean,
showVoiceRecordIcon: Boolean,
recState: MutableState<RecordingState>,
isDirectChat: Boolean,
liveMessageAlertShown: SharedPreference<Boolean>,
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
allowVoiceToContact: () -> Unit,
sendMessage: () -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null,
updateLiveMessage: (suspend () -> Unit)? = null,
cancelLiveMessage: (() -> Unit)? = null,
onMessageChange: (String) -> Unit,
onAudioAdded: (String, Int, Boolean) -> Unit,
textStyle: MutableState<TextStyle>
) {
Column(Modifier.padding(vertical = 8.dp)) {
Box {
val cs = composeState.value
val attachEnabled = !composeState.value.editing
val filePath = rememberSaveable { mutableStateOf(null as String?) }
var recordingTimeRange by rememberSaveable(saver = LongRange.saver) { mutableStateOf(0L..0L) } // since..to
val showVoiceButton = ((cs.message.isEmpty() || recordingTimeRange.first > 0L) && allowVoiceRecord && attachEnabled && cs.preview is ComposePreview.NoPreview) || filePath.value != null
Box(if (recordingTimeRange.first == 0L)
Modifier
else
Modifier.clickable(false, onClick = {})
) {
NativeKeyboard(composeState, textStyle, onMessageChange)
Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, onMessageChange)
// Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview) {
Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { }))
}
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
}
Box(Modifier.align(Alignment.BottomEnd)) {
val sendButtonSize = remember { Animatable(36f) }
val sendButtonAlpha = remember { Animatable(1f) }
val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO))
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
// Making LiveMessage alive when screen orientation was changed
if (cs.liveMessage != null && sendLiveMessage != null && updateLiveMessage != null) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
}
}
Box(Modifier.align(Alignment.BottomEnd)) {
val icon = if (cs.editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
if (cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.VoicePreview || cs.preview is ComposePreview.FilePreview)) {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp)
} else if (!showVoiceButton) {
IconButton(sendMessage, Modifier.size(36.dp), enabled = cs.sendEnabled()) {
Icon(
icon,
stringResource(R.string.icon_descr_send_message),
tint = Color.White,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
.clip(CircleShape)
.background(color)
)
}
} else {
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.RECORD_AUDIO,
)
)
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
val recordingInProgress: State<Boolean> = remember { rec.recordingInProgress }
var now by remember { mutableStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) {
while (isActive) {
now = System.currentTimeMillis()
if (recordingTimeRange.first != 0L && recordingInProgress.value && composeState.value.preview is ComposePreview.VoicePreview) {
filePath.value?.let { onAudioAdded(it, (now - recordingTimeRange.first).toInt(), false) }
}
delay(100)
}
}
val stopRecordingAndAddAudio: () -> Unit = {
rec.stop()
recordingTimeRange = recordingTimeRange.first..System.currentTimeMillis()
filePath.value?.let { onAudioAdded(it, (recordingTimeRange.last - recordingTimeRange.first).toInt(), true) }
}
val startStopRecording: () -> Unit = {
when {
showProgress -> ProgressIndicator()
showVoiceButton -> {
Row(verticalAlignment = Alignment.CenterVertically) {
val stopRecOnNextClick = remember { mutableStateOf(false) }
when {
!permissionsState.allPermissionsGranted -> permissionsState.launchMultiplePermissionRequest()
recordingInProgress.value -> stopRecordingAndAddAudio()
filePath.value == null -> {
recordingTimeRange = System.currentTimeMillis()..0L
filePath.value = rec.start(stopRecordingAndAddAudio)
filePath.value?.let { onAudioAdded(it, (now - recordingTimeRange.first).toInt(), false) }
needToAllowVoiceToContact || !allowedVoiceByPrefs -> {
DisallowedVoiceButton {
if (needToAllowVoiceToContact) {
showNeedToAllowVoiceAlert(allowVoiceToContact)
} else {
showDisabledVoiceAlert(isDirectChat)
}
}
}
!permissionsState.allPermissionsGranted ->
VoiceButtonWithoutPermission { permissionsState.launchMultiplePermissionRequest() }
else ->
RecordVoiceView(recState, stopRecOnNextClick)
}
if (sendLiveMessage != null
&& updateLiveMessage != null
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem) {
Spacer(Modifier.width(10.dp))
StartLiveMessageButton {
if (composeState.value.preview is ComposePreview.NoPreview) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
}
}
}
}
var stopRecOnNextClick by remember { mutableStateOf(false) }
val context = LocalContext.current
DisposableEffect(stopRecOnNextClick) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
if (stopRecOnNextClick) {
// Lock orientation to current orientation because screen rotation will break the recording
activity.requestedOrientation = if (activity.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
// Unlock orientation
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
}
val cleanUp = { remove: Boolean ->
rec.stop()
if (remove) filePath.value?.let { File(it).delete() }
filePath.value = null
stopRecOnNextClick = false
recordingTimeRange = 0L..0L
}
LaunchedEffect(cs.preview) {
if (cs.preview !is ComposePreview.VoicePreview && filePath.value != null) {
// Pressed on X icon in preview
cleanUp(true)
}
}
val interactionSource = interactionSourceWithTapDetection(
onPress = {
if (filePath.value == null) startStopRecording()
},
onClick = {
if (!recordingInProgress.value && filePath.value != null) {
sendMessage()
cleanUp(false)
} else if (stopRecOnNextClick) {
stopRecordingAndAddAudio()
stopRecOnNextClick = false
} else {
// tapped and didn't hold a finger
stopRecOnNextClick = true
}
},
onCancel = startStopRecording,
onRelease = startStopRecording
)
val sendButtonModifier = if (recordingTimeRange.last != 0L)
Modifier.clip(CircleShape).background(color)
else
Modifier
IconButton({}, Modifier.size(36.dp), enabled = !cs.inProgress, interactionSource = interactionSource) {
Icon(
if (recordingTimeRange.last != 0L) Icons.Outlined.ArrowUpward else if (stopRecOnNextClick) Icons.Default.Stop else Icons.Default.Mic,
stringResource(R.string.icon_descr_record_voice_message),
tint = if (recordingTimeRange.last != 0L) Color.White else if (!cs.inProgress) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
.then(sendButtonModifier)
)
}
DisposableEffect(Unit) {
onDispose {
rec.stop()
}
cs.liveMessage?.sent == false && cs.message.isEmpty() -> {
CancelLiveMessageButton {
cancelLiveMessage?.invoke()
}
}
else -> {
val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) Icons.Filled.Check else Icons.Outlined.ArrowUpward
val disabled = !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled
if (cs.liveMessage == null &&
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null
) {
var showDropdown by rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false },
Modifier.width(220.dp),
) {
ItemAction(
generalGetString(R.string.send_live_message),
Icons.Filled.Bolt,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown = false
}
)
}
} else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
}
}
}
@@ -196,6 +172,7 @@ fun SendMsgView(
private fun NativeKeyboard(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
onMessageChange: (String) -> Unit
) {
val cs = composeState.value
@@ -206,19 +183,15 @@ private fun NativeKeyboard(
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
var showKeyboard by remember { mutableStateOf(false) }
LaunchedEffect(cs.contextItem) {
when (cs.contextItem) {
is ComposeContextItem.QuotedItem -> {
delay(100)
showKeyboard = true
}
is ComposeContextItem.EditingItem -> {
// Keyboard will not show up if we try to show it too fast
delay(300)
showKeyboard = true
}
if (cs.contextItem is ComposeContextItem.QuotedItem) {
delay(100)
showKeyboard = true
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
// Keyboard will not show up if we try to show it too fast
delay(300)
showKeyboard = true
}
}
@@ -230,6 +203,7 @@ private fun NativeKeyboard(
) {
super.setOnReceiveContentListener(mimeTypes, listener)
}
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
val connection = super.onCreateInputConnection(editorInfo)
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
@@ -276,6 +250,7 @@ private fun NativeKeyboard(
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false
}
showDeleteTextButton.value = it.lineCount >= 4
}
if (composeState.value.preview is ComposePreview.VoicePreview) {
Text(
@@ -287,6 +262,296 @@ private fun NativeKeyboard(
}
}
@Composable
private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) {
IconButton(
{ composeState.value = composeState.value.copy(message = "") },
Modifier.align(Alignment.TopEnd).size(36.dp)
) {
Icon(Icons.Filled.Close, null, Modifier.padding(7.dp).size(36.dp), tint = HighOrLowlight)
}
}
@Composable
private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) {
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
DisposableEffect(Unit) { onDispose { rec.stop() } }
val stopRecordingAndAddAudio: () -> Unit = {
recState.value.filePathNullable?.let {
recState.value = RecordingState.Finished(it, rec.stop())
}
}
if (stopRecOnNextClick.value) {
LaunchedEffect(recState.value) {
if (recState.value is RecordingState.NotStarted) {
stopRecOnNextClick.value = false
}
}
// Lock orientation to current orientation because screen rotation will break the recording
LockToCurrentOrientationUntilDispose()
StopRecordButton(stopRecordingAndAddAudio)
} else {
val startRecording: () -> Unit = {
recState.value = RecordingState.Started(
filePath = rec.start { progress: Int?, finished: Boolean ->
val state = recState.value
if (state is RecordingState.Started && progress != null) {
recState.value = if (!finished)
RecordingState.Started(state.filePath, progress)
else
RecordingState.Finished(state.filePath, progress)
}
},
)
}
val interactionSource = interactionSourceWithTapDetection(
onPress = { if (recState.value is RecordingState.NotStarted) startRecording() },
onClick = {
if (stopRecOnNextClick.value) {
stopRecordingAndAddAudio()
} else {
// tapped and didn't hold a finger
stopRecOnNextClick.value = true
}
},
onCancel = stopRecordingAndAddAudio,
onRelease = stopRecordingAndAddAudio
)
RecordVoiceButton(interactionSource)
}
}
@Composable
private fun DisallowedVoiceButton(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
Icons.Outlined.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message),
tint = HighOrLowlight,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
@Composable
private fun VoiceButtonWithoutPermission(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
Icons.Filled.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(34.dp)
.padding(4.dp)
)
}
}
@Composable
private fun LockToCurrentOrientationUntilDispose() {
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context as Activity
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_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
android.view.Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
// Unlock orientation
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
}
}
@Composable
private fun StopRecordButton(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
Icons.Filled.Stop,
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
@Composable
private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
IconButton({}, Modifier.size(36.dp), interactionSource = interactionSource) {
Icon(
Icons.Filled.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(34.dp)
.padding(4.dp)
)
}
}
@Composable
private fun ProgressIndicator() {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp)
}
@Composable
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,
sizeDp: Animatable<Float, AnimationVector1D>,
alpha: Animatable<Float, AnimationVector1D>,
enabled: Boolean,
sendMessage: () -> Unit,
onLongClick: (() -> Unit)? = null
) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier.requiredSize(36.dp)
.combinedClickable(
onClick = sendMessage,
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
icon,
stringResource(R.string.icon_descr_send_message),
tint = Color.White,
modifier = Modifier
.size(sizeDp.value.dp)
.padding(4.dp)
.alpha(alpha.value)
.clip(CircleShape)
.background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight)
.padding(3.dp)
)
}
}
@Composable
private fun StartLiveMessageButton(onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier.requiredSize(36.dp)
.clickable(
onClick = onClick,
enabled = true,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Filled.Bolt,
stringResource(R.string.icon_descr_send_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
private fun startLiveMessage(
scope: CoroutineScope,
send: suspend () -> Unit,
update: suspend () -> Unit,
sendButtonSize: Animatable<Float, AnimationVector1D>,
sendButtonAlpha: Animatable<Float, AnimationVector1D>,
composeState: MutableState<ComposeState>,
liveMessageAlertShown: SharedPreference<Boolean>
) {
fun run() {
scope.launch {
while (composeState.value.liveMessage != null) {
sendButtonSize.animateTo(if (sendButtonSize.value == 36f) 32f else 36f, tween(700, 50))
}
sendButtonSize.snapTo(36f)
}
scope.launch {
while (composeState.value.liveMessage != null) {
sendButtonAlpha.animateTo(if (sendButtonAlpha.value == 1f) 0.75f else 1f, tween(700, 50))
}
sendButtonAlpha.snapTo(1f)
}
scope.launch {
delay(3000)
while (composeState.value.liveMessage != null) {
update()
delay(3000)
}
}
}
fun start() = withBGApi {
if (composeState.value.liveMessage == null) {
send()
}
run()
}
if (liveMessageAlertShown.state.value) {
start()
} else {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.live_message),
text = generalGetString(R.string.send_live_message_desc),
confirmText = generalGetString(R.string.send_verb),
onConfirm = {
liveMessageAlertShown.set(true)
start()
})
}
}
private fun showNeedToAllowVoiceAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.allow_voice_messages_question),
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
confirmText = generalGetString(R.string.allow_verb),
dismissText = generalGetString(R.string.cancel_verb),
onConfirm = onConfirm,
)
}
private fun showDisabledVoiceAlert(isDirectChat: Boolean) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.voice_messages_prohibited),
text = generalGetString(
if (isDirectChat)
R.string.ask_your_contact_to_enable_voice
else
R.string.only_group_owners_can_enable_voice
)
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -300,10 +565,15 @@ fun PreviewSendMsgView() {
SimpleXTheme {
SendMsgView(
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
allowVoiceRecord = false,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> },
textStyle = textStyle
)
}
@@ -323,10 +593,15 @@ fun PreviewSendMsgViewEditing() {
SimpleXTheme {
SendMsgView(
composeState = remember { mutableStateOf(composeStateEditing) },
allowVoiceRecord = false,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> },
textStyle = textStyle
)
}
@@ -342,14 +617,19 @@ fun PreviewSendMsgViewEditing() {
fun PreviewSendMsgViewInProgress() {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt"), inProgress = true, useLinkPreviews = true)
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt", getAppFileUri("test.txt")), inProgress = true, useLinkPreviews = true)
SimpleXTheme {
SendMsgView(
composeState = remember { mutableStateOf(composeStateInProgress) },
allowVoiceRecord = false,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> },
textStyle = textStyle
)
}
@@ -0,0 +1,137 @@
package chat.simplex.app.views.chat
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun VerifyCodeView(
displayName: String,
connectionCode: String?,
connectionVerified: Boolean,
verify: suspend (String?) -> Pair<Boolean, String>?,
close: () -> Unit,
) {
if (connectionCode != null) {
VerifyCodeLayout(
displayName,
connectionCode,
connectionVerified,
verifyCode = { newCode, cb ->
withBGApi {
val res = verify(newCode)
if (res != null) {
val (verified) = res
cb(verified)
if (verified) close()
}
}
}
)
}
}
@Composable
private fun VerifyCodeLayout(
displayName: String,
connectionCode: String,
connectionVerified: Boolean,
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.security_code), false)
val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
if (connectionVerified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 4.dp).size(22.dp), tint = HighOrLowlight)
Text(String.format(stringResource(R.string.is_verified), displayName))
} else {
Text(String.format(stringResource(R.string.is_not_verified), displayName))
}
}
SectionView {
QRCode(connectionCode, Modifier.aspectRatio(1f))
}
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.weight(2f))
SelectionContainer(Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING_HALF)) {
Text(
splitCode,
fontFamily = FontFamily.Monospace,
fontSize = 18.sp,
maxLines = 20
)
}
val context = LocalContext.current
Box(Modifier.weight(1f)) {
IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
Icon(Icons.Filled.Share, null, tint = MaterialTheme.colors.primary)
}
}
Spacer(Modifier.weight(1f))
}
Text(
generalGetString(R.string.to_verify_compare),
Modifier.padding(bottom = DEFAULT_PADDING)
)
Row(
Modifier.padding(bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
if (connectionVerified) {
SimpleButton(generalGetString(R.string.clear_verification), Icons.Outlined.Shield) {
verifyCode(null) {}
}
} else {
SimpleButton(generalGetString(R.string.scan_code), Icons.Outlined.QrCode) {
ModalManager.shared.showModal {
ScanCodeView(verifyCode) { }
}
}
SimpleButton(generalGetString(R.string.mark_code_verified), Icons.Outlined.VerifiedUser) {
verifyCode(connectionCode) { verified ->
if (!verified) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.incorrect_code)
)
}
}
}
}
}
}
}
private fun splitToParts(s: String, length: Int): String {
if (length >= s.length) return s
return (0..(s.length - 1) / length)
.map { s.drop(it * length).take(length) }
.joinToString(separator = "\n")
}
@@ -31,17 +31,23 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable
fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
val selectedContacts = remember { mutableStateListOf<Long>() }
val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) }
var allowModifyMembers by remember { mutableStateOf(true) }
BackHandler(onBack = close)
AddGroupMembersLayout(
groupInfo = groupInfo,
creatingGroup = creatingGroup,
contactsToAdd = getContactsToAdd(chatModel),
selectedContacts = selectedContacts,
selectedRole = selectedRole,
allowModifyMembers = allowModifyMembers,
openPreferences = {
ModalManager.shared.showCustomModal { close ->
GroupPreferencesView(chatModel, groupInfo.id, close)
}
},
inviteMembers = {
allowModifyMembers = false
withApi {
@@ -59,6 +65,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () ->
clearSelection = { selectedContacts.clear() },
addContact = { contactId -> if (contactId !in selectedContacts) selectedContacts.add(contactId) },
removeContact = { contactId -> selectedContacts.removeIf { it == contactId } },
close = close,
)
}
@@ -79,14 +86,17 @@ fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
@Composable
fun AddGroupMembersLayout(
groupInfo: GroupInfo,
creatingGroup: Boolean,
contactsToAdd: List<Contact>,
selectedContacts: List<Long>,
selectedRole: MutableState<GroupMemberRole>,
allowModifyMembers: Boolean,
openPreferences: () -> Unit,
inviteMembers: () -> Unit,
clearSelection: () -> Unit,
addContact: (Long) -> Unit,
removeContact: (Long) -> Unit,
close: () -> Unit,
) {
Column(
Modifier
@@ -120,18 +130,28 @@ fun AddGroupMembersLayout(
}
} else {
SectionView {
if (creatingGroup) {
SectionItemView(openPreferences) {
Text(stringResource(R.string.set_group_preferences))
}
SectionDivider()
}
SectionItemView {
RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
}
SectionDivider()
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers)
if (creatingGroup && selectedContacts.isEmpty()) {
SkipInvitingButton(close)
} else {
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers)
}
}
SectionCustomFooter {
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
}
SectionSpacer()
SectionView {
SectionView(stringResource(R.string.select_contacts)) {
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
}
SectionSpacer()
@@ -170,6 +190,17 @@ fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
)
}
@Composable
fun SkipInvitingButton(onClick: () -> Unit) {
SettingsActionItem(
Icons.Outlined.Check,
stringResource(R.string.skip_inviting_button),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
)
}
@Composable
fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelection: () -> Unit) {
Row(
@@ -288,14 +319,17 @@ fun PreviewAddGroupMembersLayout() {
SimpleXTheme {
AddGroupMembersLayout(
groupInfo = GroupInfo.sampleData,
creatingGroup = false,
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
selectedContacts = remember { mutableStateListOf() },
selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) },
allowModifyMembers = true,
openPreferences = {},
inviteMembers = {},
clearSelection = {},
addContact = {},
removeContact = {}
removeContact = {},
close = {},
)
}
}
@@ -4,7 +4,9 @@ import InfoRow
import SectionDivider
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -22,15 +24,17 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.chatlist.cantInviteIncognitoAlert
import chat.simplex.app.views.chatlist.setGroupMembers
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.*
@Composable
fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
@@ -43,32 +47,56 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
.sortedBy { it.displayName.lowercase() },
developerTools,
groupLink,
addMembers = {
withApi {
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, chatModel, close)
AddGroupMembersView(groupInfo, false, chatModel, close)
}
}
},
showMemberInfo = { member ->
withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
} else {
member to null
}
ModalManager.shared.showModalCloseable(true) { closeCurrent ->
GroupMemberInfoView(groupInfo, member, stats, chatModel, closeCurrent) { closeCurrent(); close() }
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
closeCurrent()
close()
}
}
}
}
},
editGroupProfile = {
ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
},
openPreferences = {
ModalManager.shared.showCustomModal { close ->
GroupPreferencesView(
chatModel,
chat.id,
close
)
}
},
deleteGroup = { deleteGroupDialog(chat.chatInfo, groupInfo, chatModel, close) },
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
manageGroupLink = {
withApi {
val groupLink = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink) }
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) }
}
}
)
@@ -117,9 +145,11 @@ fun GroupChatInfoLayout(
groupInfo: GroupInfo,
members: List<GroupMember>,
developerTools: Boolean,
groupLink: String?,
addMembers: () -> Unit,
showMemberInfo: (GroupMember) -> Unit,
editGroupProfile: () -> Unit,
openPreferences: () -> Unit,
deleteGroup: () -> Unit,
clearChat: () -> Unit,
leaveGroup: () -> Unit,
@@ -139,9 +169,25 @@ fun GroupChatInfoLayout(
}
SectionSpacer()
SectionView {
if (groupInfo.canEdit) {
SectionItemView(editGroupProfile) { EditGroupProfileButton() }
SectionDivider()
}
GroupPreferencesButton(openPreferences)
}
SectionTextFooter(stringResource(R.string.only_group_owners_can_change_prefs))
SectionSpacer()
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
SectionItemView(manageGroupLink) { GroupLinkButton() }
SectionItemView(manageGroupLink) {
if (groupLink == null) {
CreateGroupLinkButton()
} else {
GroupLinkButton()
}
}
SectionDivider()
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
SectionItemView(onAddMembersClick) {
@@ -160,10 +206,6 @@ fun GroupChatInfoLayout(
}
SectionSpacer()
SectionView {
if (groupInfo.canEdit) {
SectionItemView(editGroupProfile) { EditGroupProfileButton() }
SectionDivider()
}
ClearChatButton(clearChat)
if (groupInfo.canDelete) {
SectionDivider()
@@ -188,7 +230,7 @@ fun GroupChatInfoLayout(
}
@Composable
fun GroupChatInfoHeader(cInfo: ChatInfo) {
private fun GroupChatInfoHeader(cInfo: ChatInfo) {
Column(
Modifier.padding(horizontal = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -212,7 +254,16 @@ fun GroupChatInfoHeader(cInfo: ChatInfo) {
}
@Composable
fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
private fun GroupPreferencesButton(onClick: () -> Unit) {
SettingsActionItem(
Icons.Outlined.ToggleOn,
stringResource(R.string.group_preferences),
click = onClick
)
}
@Composable
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
Row(
Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
@@ -228,7 +279,7 @@ fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
}
@Composable
fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
Column {
members.forEachIndexed { index, member ->
SectionItemView({ showMemberInfo(member) }, minHeight = 50.dp) {
@@ -242,7 +293,7 @@ fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Uni
}
@Composable
fun MemberRow(member: GroupMember, user: Boolean = false) {
private fun MemberRow(member: GroupMember, user: Boolean = false) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -254,10 +305,15 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
) {
ProfileImage(size = 46.dp, member.image)
Column {
Text(
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (member.memberIncognito) Indigo else Color.Unspecified
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
MemberVerifiedShield()
}
Text(
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (member.memberIncognito) Indigo else Color.Unspecified
)
}
val s = member.memberStatus.shortText
val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s
Text(
@@ -277,7 +333,12 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
}
@Composable
fun GroupLinkButton() {
private fun MemberVerifiedShield() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 3.dp).size(16.dp), tint = HighOrLowlight)
}
@Composable
private fun GroupLinkButton() {
Row(
Modifier
.fillMaxSize(),
@@ -286,10 +347,27 @@ fun GroupLinkButton() {
Icon(
Icons.Outlined.Link,
stringResource(R.string.group_link),
tint = MaterialTheme.colors.primary
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.group_link), color = MaterialTheme.colors.primary)
Text(stringResource(R.string.group_link))
}
}
@Composable
private fun CreateGroupLinkButton() {
Row(
Modifier
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Outlined.AddLink,
stringResource(R.string.create_group_link),
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.create_group_link))
}
}
@@ -303,15 +381,15 @@ fun EditGroupProfileButton() {
Icon(
Icons.Outlined.Edit,
stringResource(R.string.button_edit_group_profile),
tint = MaterialTheme.colors.primary
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.button_edit_group_profile), color = MaterialTheme.colors.primary)
Text(stringResource(R.string.button_edit_group_profile))
}
}
@Composable
fun LeaveGroupButton() {
private fun LeaveGroupButton() {
Row(
Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
@@ -327,7 +405,7 @@ fun LeaveGroupButton() {
}
@Composable
fun DeleteGroupButton() {
private fun DeleteGroupButton() {
Row(
Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
@@ -349,13 +427,13 @@ fun PreviewGroupChatInfoLayout() {
GroupChatInfoLayout(
chat = Chat(
chatInfo = ChatInfo.Direct.sampleData,
chatItems = arrayListOf(),
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
chatItems = arrayListOf()
),
groupInfo = GroupInfo.sampleData,
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
developerTools = false,
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
groupLink = null,
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
)
}
}
@@ -1,10 +1,12 @@
package chat.simplex.app.views.chat.group
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -15,22 +17,32 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.GroupInfo
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?) {
var groupLink by remember { mutableStateOf(connReqContact) }
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current
fun createLink() {
creatingLink = true
withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
onGroupLinkUpdated(groupLink)
creatingLink = false
}
}
LaunchedEffect(Unit) {
if (groupLink == null && !creatingLink) {
createLink()
}
}
GroupLinkLayout(
groupLink = groupLink,
createLink = {
withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
}
},
creatingLink,
createLink = ::createLink,
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
deleteLink = {
AlertManager.shared.showAlertMsg(
@@ -42,17 +54,22 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) {
groupLink = null
onGroupLinkUpdated(null)
}
}
}
)
}
)
if (creatingLink) {
ProgressIndicator()
}
}
@Composable
fun GroupLinkLayout(
groupLink: String?,
creatingLink: Boolean,
createLink: () -> Unit,
share: () -> Unit,
deleteLink: () -> Unit
@@ -74,7 +91,7 @@ fun GroupLinkLayout(
verticalArrangement = Arrangement.SpaceEvenly
) {
if (groupLink == null) {
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, click = createLink)
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
} else {
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
Row(
@@ -99,3 +116,18 @@ fun GroupLinkLayout(
}
}
@Composable
fun ProgressIndicator() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
}
}
@@ -5,6 +5,7 @@ import SectionDivider
import SectionItemView
import SectionSpacer
import SectionView
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -12,6 +13,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -21,19 +23,20 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.SimplexServers
import chat.simplex.app.views.chat.SwitchAddressButton
import chat.simplex.app.views.chatlist.openChat
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import kotlinx.datetime.Clock
@Composable
fun GroupMemberInfoView(
groupInfo: GroupInfo,
member: GroupMember,
connStats: ConnectionStats?,
connectionCode: String?,
chatModel: ChatModel,
close: () -> Unit,
closeAll: () -> Unit, // Close all open windows up to ChatView
@@ -49,22 +52,27 @@ fun GroupMemberInfoView(
connStats,
newRole,
developerTools,
openDirectChat = {
connectionCode,
getContactChat = { chatModel.getContactChat(it) },
knownDirectChat = {
withApi {
val oldChat = chatModel.getContactChat(member.memberContactId ?: return@withApi)
if (oldChat != null) {
openChat(oldChat.chatInfo, chatModel)
} else {
var newChat = chatModel.controller.apiGetChat(ChatType.Direct, member.memberContactId) ?: return@withApi
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
newChat = newChat.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
chatModel.addChat(newChat)
chatModel.chatItems.clear()
chatModel.chatId.value = newChat.id
}
chatModel.chatItems.clear()
chatModel.chatItems.addAll(it.chatItems)
chatModel.chatId.value = it.chatInfo.id
closeAll()
}
},
newDirectChat = {
withApi {
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
if (c != null) {
chatModel.addChat(c)
chatModel.chatItems.clear()
chatModel.chatId.value = c.id
closeAll()
}
}
},
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
onRoleSelected = {
if (it == newRole.value) return@GroupMemberInfoLayout
@@ -85,6 +93,32 @@ fun GroupMemberInfoView(
},
switchMemberAddress = {
switchMemberAddress(chatModel, groupInfo, member)
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
VerifyCodeView(
mem.displayName,
connectionCode,
mem.verified,
verify = { code ->
chatModel.controller.apiVerifyGroupMember(mem.groupId, mem.groupMemberId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.upsertGroupMember(
groupInfo,
mem.copy(
activeConn = mem.activeConn?.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)
)
r
}
},
close,
)
}
}
}
)
}
@@ -114,10 +148,14 @@ fun GroupMemberInfoLayout(
connStats: ConnectionStats?,
newRole: MutableState<GroupMemberRole>,
developerTools: Boolean,
openDirectChat: () -> Unit,
connectionCode: String?,
getContactChat: (Long) -> Chat?,
knownDirectChat: (Chat) -> Unit,
newDirectChat: (Long) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
Column(
Modifier
@@ -133,10 +171,29 @@ fun GroupMemberInfoLayout(
}
SectionSpacer()
SectionView {
OpenChatButton(openDirectChat)
if (member.memberActive) {
val contactId = member.memberContactId
if (contactId != null) {
SectionView {
val chat = getContactChat(contactId)
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
OpenChatButton(onClick = { knownDirectChat(chat) })
if (connectionCode != null) {
SectionDivider()
}
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { newDirectChat(contactId) })
if (connectionCode != null) {
SectionDivider()
}
}
if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked)
}
}
SectionSpacer()
}
}
SectionSpacer()
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
@@ -159,12 +216,10 @@ fun GroupMemberInfoLayout(
}
}
SectionSpacer()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
if (developerTools) {
SwitchAddressButton(switchMemberAddress)
SectionDivider()
}
if (connStats != null) {
if (connStats != null) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchMemberAddress)
SectionDivider()
val rcvServers = connStats.rcvServers
val sndServers = connStats.sndServers
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
@@ -179,8 +234,8 @@ fun GroupMemberInfoLayout(
}
}
}
SectionSpacer()
}
SectionSpacer()
if (member.canBeRemoved(groupInfo)) {
SectionView {
@@ -207,12 +262,17 @@ fun GroupMemberInfoHeader(member: GroupMember) {
horizontalAlignment = Alignment.CenterHorizontally
) {
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Text(
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
}
Text(
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (member.fullName != "" && member.fullName != member.displayName) {
Text(
member.fullName, style = MaterialTheme.typography.h2,
@@ -302,10 +362,14 @@ fun PreviewGroupMemberInfoLayout() {
connStats = null,
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
developerTools = false,
openDirectChat = {},
connectionCode = "123",
getContactChat = { Chat.sampleData },
knownDirectChat = {},
newDirectChat = {},
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},
verifyClicked = {},
)
}
}
@@ -0,0 +1,183 @@
package chat.simplex.app.views.chat.group
import InfoRow
import SectionDivider
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.TimedMessagesTTLPicker
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
@Composable
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
val groupInfo = remember { derivedStateOf { (m.getChat(chatId)?.chatInfo as? ChatInfo.Group)?.groupInfo } }
val gInfo = groupInfo.value ?: return
var preferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(gInfo.fullGroupPreferences) }
var currentPreferences by rememberSaveable(gInfo, stateSaver = serializableSaver()) { mutableStateOf(preferences) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val gInfo = m.controller.apiUpdateGroup(gInfo.groupId, gp)
if (gInfo != null) {
m.updateGroup(gInfo)
currentPreferences = preferences
}
afterSave()
}
}
ModalView(
close = {
if (preferences == currentPreferences) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
GroupPreferencesLayout(
preferences,
currentPreferences,
gInfo,
applyPrefs = { prefs ->
preferences = prefs
},
reset = {
preferences = currentPreferences
},
savePrefs = ::savePrefs,
)
}
}
@Composable
private fun GroupPreferencesLayout(
preferences: FullGroupPreferences,
currentPreferences: FullGroupPreferences,
groupInfo: GroupInfo,
applyPrefs: (FullGroupPreferences) -> Unit,
reset: () -> Unit,
savePrefs: () -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.group_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl ?: 86400)))
}
FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable ->
if (enable == GroupFeatureEnabled.ON) {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400)))
} else {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl)))
}
}
SectionSpacer()
val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) }
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it)))
}
SectionSpacer()
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) }
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
}
SectionSpacer()
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
}
if (groupInfo.canEdit) {
SectionSpacer()
ResetSaveButtons(
reset = reset,
save = savePrefs,
disabled = preferences == currentPreferences
)
}
}
}
@Composable
private fun FeatureSection(
feature: GroupFeature,
enableFeature: State<GroupFeatureEnabled>,
groupInfo: GroupInfo,
preferences: FullGroupPreferences,
onTTLUpdated: (Int?) -> Unit,
onSelected: (GroupFeatureEnabled) -> Unit
) {
SectionView {
val on = enableFeature.value == GroupFeatureEnabled.ON
val icon = if (on) feature.iconFilled else feature.icon
val iconTint = if (on) SimplexGreen else HighOrLowlight
val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON
if (groupInfo.canEdit) {
SectionItemView {
PreferenceToggleWithIcon(
feature.text,
icon,
iconTint,
enableFeature.value == GroupFeatureEnabled.ON,
) { checked ->
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
}
}
if (timedOn) {
SectionDivider()
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
TimedMessagesTTLPicker(ttl, onTTLUpdated)
}
} else {
InfoRow(
feature.text,
enableFeature.value.text,
icon = icon,
iconTint = iconTint,
)
if (timedOn) {
SectionDivider()
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
}
}
}
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit))
}
@Composable
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(reset, disabled = disabled) {
Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
SectionDivider()
SectionItemView(save, disabled = disabled) {
Text(stringResource(R.string.save_and_notify_group_members), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -136,7 +136,13 @@ fun GroupProfileLayout(
if (enabled) {
Text(
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable { saveProfile(GroupProfile(displayName.value, fullName.value, profileImage.value)) },
modifier = Modifier.clickable {
saveProfile(groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
))
},
color = MaterialTheme.colors.primary
)
} else {
@@ -24,7 +24,7 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat
Modifier
.padding(horizontal = 4.dp)
.padding(bottom = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
@Composable fun ConnectingCallIcon() = Icon(Icons.Outlined.SettingsPhone, stringResource(R.string.icon_descr_call_connecting), tint = Color.Green)
@Composable fun ConnectingCallIcon() = Icon(Icons.Outlined.SettingsPhone, stringResource(R.string.icon_descr_call_connecting), tint = SimplexGreen)
when (status) {
CICallStatus.Pending -> if (sent) {
Icon(Icons.Outlined.Call, stringResource(R.string.icon_descr_call_pending_sent))
@@ -38,7 +38,7 @@ fun CICallItemView(cInfo: ChatInfo, cItem: ChatItem, status: CICallStatus, durat
CICallStatus.Progress -> Icon(Icons.Filled.PhoneInTalk, stringResource(R.string.icon_descr_call_progress), tint = SimplexGreen)
CICallStatus.Ended -> Row {
Icon(Icons.Outlined.CallEnd, stringResource(R.string.icon_descr_call_ended), tint = HighOrLowlight, modifier = Modifier.padding(end = 4.dp))
Text(status.duration(duration), color = HighOrLowlight)
Text(durationText(duration), color = HighOrLowlight)
}
CICallStatus.Error -> {}
}
@@ -0,0 +1,34 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
@Composable
fun CIChatFeatureView(
chatItem: ChatItem,
feature: Feature,
iconColor: Color,
icon: ImageVector? = null
) {
Row(
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(icon ?: feature.iconFilled, feature.text, Modifier.size(18.dp), tint = iconColor)
Text(
chatEventText(chatItem),
Modifier,
// this is important. Otherwise, aligning will be bad because annotated string has a Span with size 12.sp
fontSize = 12.sp
)
}
}
@@ -18,32 +18,38 @@ import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun CIEventView(ci: ChatItem) {
fun withGroupEventStyle(builder: AnnotatedString.Builder, text: String) {
return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) }
@Composable
fun chatEventTextView(text: AnnotatedString) {
Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp))
}
Surface {
Row(
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.Bottom
verticalAlignment = Alignment.CenterVertically
) {
Text(
buildAnnotatedString {
val memberDisplayName = ci.memberDisplayName
if (memberDisplayName != null) {
withGroupEventStyle(this, memberDisplayName)
val memberDisplayName = ci.memberDisplayName
if (memberDisplayName != null) {
chatEventTextView(
buildAnnotatedString {
withStyle(chatEventStyle) { append(memberDisplayName) }
append(" ")
}
withGroupEventStyle(this, ci.content.text)
append(" ")
withGroupEventStyle(this, ci.timestampText)
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
)
}.plus(chatEventText(ci))
)
} else {
chatEventTextView(chatEventText(ci))
}
}
}
}
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)
fun chatEventText(ci: ChatItem): AnnotatedString =
buildAnnotatedString {
withStyle(chatEventStyle) { append(ci.content.text + " " + ci.timestampText) }
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -0,0 +1,54 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun CIFeaturePreferenceView(
chatItem: ChatItem,
contact: Contact?,
feature: ChatFeature,
allowed: FeatureAllowed,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
) {
Row(
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(feature.icon, feature.text, Modifier.size(18.dp), tint = HighOrLowlight)
if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) {
val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
val setParam = feature == ChatFeature.TimedMessages && contact.mergedPreferences.timedMessages.userPreference.pref.ttl == null
val acceptTextId = if (setParam) R.string.accept_feature_set_1_day else R.string.accept_feature
val param = if (setParam) 86400 else null
val annotatedText = buildAnnotatedString {
withStyle(chatEventStyle) { append(chatItem.content.text + " ") }
withAnnotation(tag = "Accept", annotation = "Accept") {
withStyle(acceptStyle) { append(generalGetString(acceptTextId) + " ") }
}
withStyle(chatEventStyle) { append(chatItem.timestampText) }
}
fun accept(offset: Int): Boolean = annotatedText.getStringAnnotations(tag = "Accept", start = offset, end = offset).isNotEmpty()
ClickableText(
annotatedText,
onClick = { if (accept(it)) { acceptFeature(contact, feature, param) } },
shouldConsumeEvent = ::accept
)
} else {
Text(chatItem.content.text + " " + chatItem.timestampText,
fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)
}
}
}
@@ -205,6 +205,6 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
fun PreviewCIFileFramedItemView(@PreviewParameter(ChatItemProvider::class) chatItem: ChatItem) {
val showMenu = remember { mutableStateOf(false) }
SimpleXTheme {
FramedItemView(ChatInfo.Direct.sampleData, chatItem, showMenu = showMenu, receiveFile = {})
FramedItemView(ChatInfo.Direct.sampleData, chatItem, linkMode = SimplexLinkMode.DESCRIPTION, showMenu = showMenu, receiveFile = {})
}
}
@@ -0,0 +1,46 @@
package chat.simplex.app.views.chat.item
import SectionSpacer
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable
fun CIInvalidJSONView(json: String) {
Row(Modifier
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
.padding(horizontal = 10.dp, vertical = 6.dp)
) {
Text(stringResource(R.string.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)
}
}
@Composable
fun InvalidJSONView(json: String) {
Column {
Spacer(Modifier.height(DEFAULT_PADDING))
SectionView {
val context = LocalContext.current
SettingsActionItem(Icons.Outlined.Share, generalGetString(R.string.share_verb), click = {
shareText(context, json)
})
}
Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) {
Text(json)
}
}
}
@@ -3,61 +3,85 @@ package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Timer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import kotlinx.datetime.Clock
@Composable
fun CIMetaView(chatItem: ChatItem, metaColor: Color = HighOrLowlight) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (!chatItem.isDeletedContent) {
if (chatItem.meta.itemEdited) {
Icon(
Icons.Filled.Edit,
modifier = Modifier.height(12.dp).padding(end = 1.dp),
contentDescription = stringResource(R.string.icon_descr_edited),
tint = metaColor,
)
}
CIStatusView(chatItem.meta.itemStatus, metaColor)
fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = HighOrLowlight) {
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
if (chatItem.isDeletedContent) {
Text(
chatItem.timestampText,
color = metaColor,
fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp)
)
} else {
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor)
}
Text(
chatItem.timestampText,
color = metaColor,
fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp)
)
}
}
@Composable
// changing this function requires updating reserveSpaceForMeta
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
if (meta.itemEdited) {
StatusIconText(Icons.Outlined.Edit, color)
Spacer(Modifier.width(3.dp))
}
if (meta.disappearing) {
StatusIconText(Icons.Outlined.Timer, color)
val ttl = meta.itemTimed?.ttl
if (ttl != chatTTL) {
Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
}
Spacer(Modifier.width(4.dp))
}
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
if (statusIcon != null) {
val (icon, statusColor) = statusIcon
StatusIconText(icon, statusColor)
Spacer(Modifier.width(4.dp))
} else if (!meta.disappearing) {
StatusIconText(Icons.Filled.Circle, Color.Transparent)
Spacer(Modifier.width(4.dp))
}
Text(meta.timestampText, color = color, fontSize = 13.sp)
}
// the conditions in this function should match CIMetaText
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
val iconSpace = " "
var res = ""
if (meta.itemEdited) res += iconSpace
if (meta.itemTimed != null) {
res += iconSpace
val ttl = meta.itemTimed.ttl
if (ttl != chatTTL) {
res += TimedMessagesPreference.shortTtlText(ttl)
}
}
if (meta.statusIcon(HighOrLowlight) != null || !meta.disappearing) {
res += iconSpace
}
return res + meta.timestampText
}
@Composable
fun CIStatusView(status: CIStatus, metaColor: Color = HighOrLowlight) {
when (status) {
is CIStatus.SndSent -> {
Icon(Icons.Filled.Check, stringResource(R.string.icon_descr_sent_msg_status_sent), Modifier.height(12.dp), tint = metaColor)
}
is CIStatus.SndErrorAuth -> {
Icon(Icons.Filled.Close, stringResource(R.string.icon_descr_sent_msg_status_unauthorized_send), Modifier.height(12.dp), tint = Color.Red)
}
is CIStatus.SndError -> {
Icon(Icons.Filled.WarningAmber, stringResource(R.string.icon_descr_sent_msg_status_send_failed), Modifier.height(12.dp), tint = Color.Yellow)
}
is CIStatus.RcvNew -> {
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = MaterialTheme.colors.primary)
}
else -> {}
}
private fun StatusIconText(icon: ImageVector, color: Color) {
Icon(icon, null, Modifier.height(12.dp), tint = color)
}
@Preview
@@ -66,7 +90,8 @@ fun PreviewCIMetaView() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
)
),
null
)
}
@@ -77,7 +102,8 @@ fun PreviewCIMetaViewUnread() {
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
status = CIStatus.RcvNew()
)
),
null
)
}
@@ -87,8 +113,9 @@ fun PreviewCIMetaViewSendFailed() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
status = CIStatus.SndError(AgentErrorType.CMD(CommandErrorType.SYNTAX()))
)
status = CIStatus.SndError("CMD SYNTAX")
),
null
)
}
@@ -98,7 +125,8 @@ fun PreviewCIMetaViewSendNoAuth() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
)
),
null
)
}
@@ -108,7 +136,8 @@ fun PreviewCIMetaViewSendSent() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent()
)
),
null
)
}
@@ -119,7 +148,8 @@ fun PreviewCIMetaViewEdited() {
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
itemEdited = true
)
),
null
)
}
@@ -131,7 +161,8 @@ fun PreviewCIMetaViewEditedUnread() {
1, CIDirection.DirectRcv(), Clock.System.now(), "hello",
itemEdited = true,
status=CIStatus.RcvNew()
)
),
null
)
}
@@ -143,7 +174,8 @@ fun PreviewCIMetaViewEditedSent() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
itemEdited = true,
status=CIStatus.SndSent()
)
),
null
)
}
@@ -151,6 +183,7 @@ fun PreviewCIMetaViewEditedSent() {
@Composable
fun PreviewCIMetaViewDeletedContent() {
CIMetaView(
chatItem = ChatItem.getDeletedContentSampleData()
chatItem = ChatItem.getDeletedContentSampleData(),
null
)
}
@@ -1,5 +1,6 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -18,26 +19,26 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
@Composable
fun CIVoiceView(
durationSec: Int,
providedDurationSec: Int,
file: CIFile?,
edited: Boolean,
sent: Boolean,
hasText: Boolean,
ci: ChatItem,
metaColor: Color
timedMessagesTTL: Int?,
longClick: () -> Unit,
) {
Row(
Modifier.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 6.dp),
Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = 6.dp, end = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (file != null) {
@@ -45,86 +46,27 @@ fun CIVoiceView(
val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(context, file) }
var brokenAudio by rememberSaveable(file.filePath) { mutableStateOf(false) }
val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) }
val audioInfo = remember(file.filePath) {
file.audioInfo.value = file.audioInfo.value.copy(durationMs = durationSec * 1000)
file.audioInfo
}
val play = play@{
audioPlaying.value = AudioPlayer.start(filePath ?: return@play, audioInfo.value.progressMs) {
// If you want to preserve the position after switching a track, remove this line
audioInfo.value = ProgressAndDuration(0, audioInfo.value.durationMs)
audioPlaying.value = false
}
val progress = rememberSaveable(file.filePath) { mutableStateOf(0) }
val duration = rememberSaveable(file.filePath) { mutableStateOf(providedDurationSec * 1000) }
val play = {
AudioPlayer.play(filePath, audioPlaying, progress, duration, true)
brokenAudio = !audioPlaying.value
}
val pause = {
audioInfo.value = ProgressAndDuration(AudioPlayer.pause(), audioInfo.value.durationMs)
audioPlaying.value = false
AudioPlayer.pause(audioPlaying, progress)
}
AudioInfoUpdater(filePath, audioPlaying, audioInfo)
val time = if (audioPlaying.value) audioInfo.value.progressMs else audioInfo.value.durationMs
val minWidth = with(LocalDensity.current) { 45.sp.toDp() }
val text = String.format("%02d:%02d", time / 1000 / 60, time / 1000 % 60)
if (hasText) {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, audioInfo, brokenAudio, play, pause)
Text(
text,
Modifier
.padding(start = 12.dp, end = 5.dp)
.widthIn(min = minWidth),
color = HighOrLowlight,
fontSize = 16.sp,
textAlign = TextAlign.Start,
maxLines = 1
)
} else {
if (sent) {
Row {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.height(56.dp))
Text(
text,
Modifier
.padding(end = 12.dp)
.widthIn(min = minWidth),
color = HighOrLowlight,
fontSize = 16.sp,
maxLines = 1
)
}
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, audioInfo, brokenAudio, play, pause)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci, metaColor)
}
}
}
} else {
Row {
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, audioInfo, brokenAudio, play, pause)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci, metaColor)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text,
Modifier
.padding(start = 12.dp)
.widthIn(min = minWidth),
color = HighOrLowlight,
fontSize = 16.sp,
maxLines = 1
)
Spacer(Modifier.height(56.dp))
}
val text = remember {
derivedStateOf {
val time = when {
audioPlaying.value || progress.value != 0 -> progress.value
else -> duration.value
}
durationText(time / 1000)
}
}
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick)
} else {
VoiceMsgIndicator(null, false, sent, hasText, null, false, {}, {})
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
val metaReserve = if (edited)
" "
else
@@ -134,6 +76,73 @@ fun CIVoiceView(
}
}
@Composable
private fun VoiceLayout(
file: CIFile,
ci: ChatItem,
text: State<String>,
audioPlaying: State<Boolean>,
progress: State<Int>,
duration: State<Int>,
brokenAudio: Boolean,
sent: Boolean,
hasText: Boolean,
timedMessagesTTL: Int?,
play: () -> Unit,
pause: () -> Unit,
longClick: () -> Unit
) {
when {
hasText -> {
Spacer(Modifier.width(6.dp))
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
DurationText(text, PaddingValues(start = 12.dp))
}
sent -> {
Row {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.height(56.dp))
DurationText(text, PaddingValues(end = 12.dp))
}
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci, timedMessagesTTL)
}
}
}
}
else -> {
Row {
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci, timedMessagesTTL)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
DurationText(text, PaddingValues(start = 12.dp))
Spacer(Modifier.height(56.dp))
}
}
}
}
}
@Composable
private fun DurationText(text: State<String>, padding: PaddingValues) {
val minWidth = with(LocalDensity.current) { 45.sp.toDp() }
Text(
text.value,
Modifier
.padding(padding)
.widthIn(min = minWidth),
color = HighOrLowlight,
fontSize = 16.sp,
maxLines = 1
)
}
@Composable
private fun PlayPauseButton(
audioPlaying: Boolean,
@@ -144,17 +153,21 @@ private fun PlayPauseButton(
enabled: Boolean,
error: Boolean,
play: () -> Unit,
pause: () -> Unit
pause: () -> Unit,
longClick: () -> Unit
) {
Surface(
onClick = { if (!audioPlaying) play() else pause() },
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = if (sent) SentColorLight else ReceivedColorLight,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(
Modifier
.defaultMinSize(minWidth = 56.dp, minHeight = 56.dp),
.defaultMinSize(minWidth = 56.dp, minHeight = 56.dp)
.combinedClickable(
onClick = { if (!audioPlaying) play() else pause() },
onLongClick = longClick
),
contentAlignment = Alignment.Center
) {
Icon(
@@ -173,17 +186,19 @@ private fun VoiceMsgIndicator(
audioPlaying: Boolean,
sent: Boolean,
hasText: Boolean,
audioInfo: State<ProgressAndDuration>?,
progress: State<Int>?,
duration: State<Int>?,
error: Boolean,
play: () -> Unit,
pause: () -> Unit
pause: () -> Unit,
longClick: () -> Unit
) {
val strokeWidth = with(LocalDensity.current){ 3.dp.toPx() }
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
val strokeColor = MaterialTheme.colors.primary
if (file != null && file.loaded && audioInfo != null) {
val angle = 360f * (audioInfo.value.progressMs.toDouble() / audioInfo.value.durationMs).toFloat()
if (file != null && file.loaded && progress != null && duration != null) {
val angle = 360f * (progress.value.toDouble() / duration.value).toFloat()
if (hasText) {
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.drawRingModifier(angle, strokeColor, strokeWidth)) {
IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) {
Icon(
imageVector = if (audioPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescription = null,
@@ -192,12 +207,13 @@ private fun VoiceMsgIndicator(
)
}
} else {
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause)
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
}
} else {
if (file?.fileStatus == CIFileStatus.RcvInvitation
|| file?.fileStatus == CIFileStatus.RcvTransfer
|| file?.fileStatus == CIFileStatus.RcvAccepted) {
|| file?.fileStatus == CIFileStatus.RcvAccepted
) {
Box(
Modifier
.size(56.dp)
@@ -207,7 +223,7 @@ private fun VoiceMsgIndicator(
ProgressIndicator()
}
} else {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {})
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
}
}
}
@@ -242,26 +258,3 @@ private fun ProgressIndicator() {
strokeWidth = 4.dp
)
}
@Composable
fun AudioInfoUpdater(
filePath: String?,
audioPlaying: MutableState<Boolean>,
audioInfo: MutableState<ProgressAndDuration>
) {
LaunchedEffect(filePath) {
if (filePath != null && audioInfo.value.durationMs == 0) {
audioInfo.value = ProgressAndDuration(audioInfo.value.progressMs, AudioPlayer.duration(filePath))
}
}
LaunchedEffect(audioPlaying.value) {
while (isActive && audioPlaying.value) {
audioInfo.value = AudioPlayer.progressAndDurationOrEnded()
if (audioInfo.value.progressMs == audioInfo.value.durationMs) {
audioInfo.value = ProgressAndDuration(0, audioInfo.value.durationMs)
audioPlaying.value = false
}
delay(50)
}
}
}
@@ -20,12 +20,15 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
import kotlinx.datetime.Clock
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
@Composable
fun ChatItemView(
cInfo: ChatInfo,
@@ -34,18 +37,25 @@ fun ChatItemView(
imageProvider: (() -> ImageGalleryProvider)? = null,
showMember: Boolean = false,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val sent = cItem.chatDir.sent
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
val showMenu = remember { mutableStateOf(false) }
val revealed = remember { mutableStateOf(false) }
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
val onLinkLongClick = { _: String -> showMenu.value = true }
val live = composeState.value.liveMessage != null
Box(
modifier = Modifier
.padding(bottom = 4.dp)
@@ -58,7 +68,7 @@ fun ChatItemView(
showMsgDeliveryErrorAlert(generalGetString(R.string.message_delivery_error_desc))
}
is CIStatus.SndError -> {
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError.string}")
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError}")
}
else -> {}
}
@@ -68,26 +78,36 @@ fun ChatItemView(
.clip(RoundedCornerShape(18.dp))
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
) {
@Composable fun ContentItem() {
if (cItem.file == null && cItem.quotedItem == null && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem)
@Composable
fun framedItemView() {
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
}
fun deleteMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.delete_message_cannot_be_undone_warning)
} else {
val onLinkLongClick = { _: String -> showMenu.value = true }
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, showMenu, receiveFile, onLinkLongClick, scrollToItem)
generalGetString(R.string.delete_message_mark_deleted_warning)
}
}
@Composable
fun MsgContentItemDropdownMenu() {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
showMenu.value = false
})
if (!cItem.meta.itemDeleted && !live) {
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
showMenu.value = false
})
}
ItemAction(stringResource(R.string.share_verb), Icons.Outlined.Share, onClick = {
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
when {
@@ -114,40 +134,78 @@ 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 = {
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false
})
}
ItemAction(
stringResource(R.string.delete_verb),
Icons.Outlined.Delete,
onClick = {
showMenu.value = false
deleteMessageAlertDialog(cItem, deleteMessage = deleteMessage)
},
color = Color.Red
)
if (cItem.meta.itemDeleted && revealed.value) {
ItemAction(
stringResource(R.string.hide_verb),
Icons.Outlined.VisibilityOff,
onClick = {
revealed.value = false
showMenu.value = false
}
)
}
if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
}
@Composable fun DeletedItem() {
DeletedItemView(cItem, showMember = showMember)
@Composable
fun MarkedDeletedItemDropdownMenu() {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
ItemAction(
stringResource(R.string.delete_verb),
Icons.Outlined.Delete,
stringResource(R.string.reveal_verb),
Icons.Outlined.Visibility,
onClick = {
revealed.value = true
showMenu.value = false
deleteMessageAlertDialog(cItem, deleteMessage = deleteMessage)
},
color = Color.Red
}
)
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@Composable
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted && !revealed.value) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
MarkedDeletedItemDropdownMenu()
} else if (cItem.quotedItem == null && !cItem.meta.itemDeleted && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL)
MsgContentItemDropdownMenu()
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
MsgContentItemDropdownMenu()
} else {
framedItemView()
MsgContentItemDropdownMenu()
}
} else {
framedItemView()
MsgContentItemDropdownMenu()
}
}
@Composable fun DeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@@ -162,18 +220,48 @@ fun ChatItemView(
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
is CIContent.SndGroupEventContent -> CIEventView(cItem)
is CIContent.RcvConnEventContent -> CIEventView(cItem)
is CIContent.SndConnEventContent -> CIEventView(cItem)
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.RcvChatPreference -> {
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
}
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, HighOrLowlight, icon = c.feature.icon,)
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
}
}
}
}
@Composable
fun DeleteItemAction(
cItem: ChatItem,
showMenu: MutableState<Boolean>,
questionText: String,
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
ItemAction(
stringResource(R.string.delete_verb),
Icons.Outlined.Delete,
onClick = {
showMenu.value = false
deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
},
color = Color.Red
)
}
@Composable
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) {
DropdownMenuItem(onClick) {
@@ -191,10 +279,10 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
}
}
fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteMode) -> Unit) {
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialogButtons(
title = generalGetString(R.string.delete_message__question),
text = generalGetString(R.string.delete_message_cannot_be_undone_warning),
text = questionText,
buttons = {
Row(
Modifier
@@ -235,12 +323,14 @@ fun PreviewChatItemView() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> }
)
}
}
@@ -253,12 +343,14 @@ fun PreviewChatItemViewDeletedContent() {
ChatInfo.Direct.sampleData,
ChatItem.getDeletedContentSampleData(),
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> }
)
}
}
@@ -18,7 +18,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
Surface(
shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight,
@@ -35,7 +35,7 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci)
CIMetaView(ci, timedMessagesTTL)
}
}
}
@@ -49,7 +49,8 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
fun PreviewDeletedItemView() {
SimpleXTheme {
DeletedItemView(
ChatItem.getDeletedContentSampleData()
ChatItem.getDeletedContentSampleData(),
null
)
}
}
@@ -15,13 +15,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp)
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp)
@Composable
fun EmojiItemView(chatItem: ChatItem) {
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) {
Column(
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
EmojiText(chatItem.content.text)
CIMetaView(chatItem)
CIMetaView(chatItem, timedMessagesTTL)
}
}
@@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.InsertDriveFile
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -14,19 +14,21 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.*
import androidx.compose.ui.util.fastMap
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ChatItemLinkView
import chat.simplex.app.views.helpers.base64ToBitmap
import chat.simplex.app.views.helpers.*
import kotlinx.datetime.Clock
import kotlin.math.min
val SentColorLight = Color(0x1E45B8FF)
val ReceivedColorLight = Color(0x20B1B0B5)
@@ -40,12 +42,14 @@ fun FramedItemView(
uriHandler: UriHandler? = null,
imageProvider: (() -> ImageGalleryProvider)? = null,
showMember: Boolean = false,
linkMode: SimplexLinkMode,
showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit,
onLinkLongClick: (link: String) -> Unit = {},
scrollToItem: (Long) -> Unit = {},
) {
val sent = ci.chatDir.sent
val chatTTL = chatInfo.timedMessagesTTL
fun membership(): GroupMember? {
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
@@ -57,13 +61,41 @@ fun FramedItemView(
Modifier.padding(vertical = 6.dp, horizontal = 12.dp),
contentAlignment = Alignment.TopStart
) {
val text = if (qi.content is MsgContent.MCVoice && qi.text.isEmpty())
qi.content.toTextWithDuration(true)
else
qi.text
MarkdownText(
text, qi.formattedText, sender = qi.sender(membership()), senderBold = true, maxLines = 3,
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface)
qi.text, qi.formattedText, sender = qi.sender(membership()), senderBold = true, maxLines = 3,
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface),
linkMode = linkMode
)
}
}
@Composable
fun FramedItemHeader(caption: String, italic: Boolean, icon: ImageVector? = null) {
Row(
Modifier
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight)
.fillMaxWidth()
.padding(start = 8.dp)
.padding(end = 12.dp)
.padding(top = 6.dp)
.padding(bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
if (icon != null) {
Icon(
icon,
caption,
Modifier.size(18.dp),
tint = if (isInDarkTheme()) FileDark else FileLight
)
}
Text(
buildAnnotatedString {
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = HighOrLowlight)) {
append(caption)
}
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
)
}
}
@@ -97,7 +129,7 @@ fun FramedItemView(
ciQuotedMsgView(qi)
}
Icon(
if (qi.content is MsgContent.MCFile) Icons.Filled.InsertDriveFile else Icons.Filled.PlayArrow,
if (qi.content is MsgContent.MCFile) Icons.Filled.InsertDriveFile else Icons.Filled.Mic,
if (qi.content is MsgContent.MCFile) stringResource(R.string.icon_descr_file) else stringResource(R.string.voice_message),
Modifier
.padding(top = 6.dp, end = 4.dp)
@@ -110,7 +142,16 @@ fun FramedItemView(
}
}
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVoice) && ci.content.text.isEmpty() && ci.quotedItem == null
@Composable
fun ciFileView(ci: ChatItem, text: String) {
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
if (text != "" || ci.meta.isLive) {
CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler)
}
}
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
Box(Modifier
.clip(RoundedCornerShape(18.dp))
.background(
@@ -124,8 +165,13 @@ fun FramedItemView(
Box(contentAlignment = Alignment.BottomEnd) {
Column(Modifier.width(IntrinsicSize.Max)) {
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
if (ci.meta.itemDeleted) {
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
} else if (ci.meta.isLive) {
FramedItemHeader(stringResource(R.string.live), false)
}
ci.quotedItem?.let { ciQuoteView(it) }
if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) {
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
Column(
Modifier
@@ -141,37 +187,36 @@ fun FramedItemView(
when (val mc = ci.content.msgContent) {
is MsgContent.MCImage -> {
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "") {
if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White
} else {
CIMarkdownText(ci, showMember, uriHandler)
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}
}
is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, mc.text != "" || ci.quotedItem != null, ci, metaColor)
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
if (mc.text != "") {
CIMarkdownText(ci, showMember, uriHandler)
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}
}
is MsgContent.MCFile -> {
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
if (mc.text != "") {
CIMarkdownText(ci, showMember, uriHandler)
is MsgContent.MCFile -> ciFileView(ci, mc.text)
is MsgContent.MCUnknown ->
if (ci.file == null) {
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
} else {
ciFileView(ci, mc.text)
}
}
is MsgContent.MCLink -> {
ChatItemLinkView(mc.preview)
CIMarkdownText(ci, showMember, uriHandler, onLinkLongClick)
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
}
else -> CIMarkdownText(ci, showMember, uriHandler, onLinkLongClick)
else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
}
}
}
}
if (ci.content.msgContent !is MsgContent.MCVoice || ci.content.text.isNotEmpty()) {
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
CIMetaView(ci, metaColor)
}
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
CIMetaView(ci, chatTTL, metaColor)
}
}
}
@@ -180,20 +225,24 @@ fun FramedItemView(
@Composable
fun CIMarkdownText(
ci: ChatItem,
chatTTL: Int?,
showMember: Boolean,
linkMode: SimplexLinkMode,
uriHandler: UriHandler?,
onLinkLongClick: (link: String) -> Unit = {}
) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
MarkdownText(
ci.content.text, ci.formattedText, if (showMember) ci.memberDisplayName else null,
metaText = ci.timestampText, edited = ci.meta.itemEdited,
text, if (text.isEmpty()) emptyList() else ci.formattedText, if (showMember) ci.memberDisplayName else null,
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick
)
}
}
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
const val MAX_SAFE_WIDTH_HEIGHT = 100_000
@Composable
fun PriorityLayout(
@@ -212,9 +261,15 @@ fun PriorityLayout(
if (it.layoutId == priorityLayoutId)
imagePlaceable!!
else
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: constraints.maxWidth)) }
// Limit width for every other element to width of important element and height for a sum of all elements
layout(imagePlaceable?.measuredWidth ?: placeables.maxOf { it.width }, placeables.sumOf { it.height }) {
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: min(MAX_SAFE_WIDTH_HEIGHT, constraints.maxWidth))) }
/**
* Limit width for every other element to width of important element and height for a sum of all elements.
*
* min(MAX_SAFE_WIDTH_HEIGHT, ...) is here because of exception (related to width of long text):
* java.lang.IllegalArgumentException: Can't represent a size of 324314 in Constraints
* at androidx.compose.ui.unit.Constraints$Companion.bitsNeedForSize(Constraints.kt:403)
* */
layout(imagePlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH_HEIGHT, placeables.maxOf { it.width }), min(MAX_SAFE_WIDTH_HEIGHT, placeables.sumOf { it.height })) {
var y = 0
placeables.forEach {
it.place(0, y)
@@ -238,6 +293,7 @@ fun PreviewTextItemViewSnd(@PreviewParameter(EditedProvider::class) edited: Bool
ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = edited,
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -254,6 +310,7 @@ fun PreviewTextItemViewRcv(@PreviewParameter(EditedProvider::class) edited: Bool
ChatItem.getSampleData(
1, CIDirection.DirectRcv(), Clock.System.now(), "hello", itemEdited = edited
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -274,6 +331,7 @@ fun PreviewTextItemViewLong(@PreviewParameter(EditedProvider::class) edited: Boo
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
itemEdited = edited
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -295,6 +353,7 @@ fun PreviewTextItemViewQuote(@PreviewParameter(EditedProvider::class) edited: Bo
quotedItem = CIQuote.getSample(1, Clock.System.now(), "hi", chatDir = CIDirection.DirectRcv()),
itemEdited = edited
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -316,6 +375,7 @@ fun PreviewTextItemViewEmoji(@PreviewParameter(EditedProvider::class) edited: Bo
quotedItem = CIQuote.getSample(1, Clock.System.now(), "Lorem ipsum dolor sit amet", chatDir = CIDirection.DirectRcv()),
itemEdited = edited
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -344,6 +404,7 @@ fun PreviewQuoteWithTextAndImage(@PreviewParameter(EditedProvider::class) edited
quotedItem = ciQuote,
itemEdited = edited
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -372,6 +433,7 @@ fun PreviewQuoteWithLongTextAndImage(@PreviewParameter(EditedProvider::class) ed
quotedItem = ciQuote,
itemEdited = edited
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -399,6 +461,7 @@ fun PreviewQuoteWithLongTextAndFile(@PreviewParameter(EditedProvider::class) edi
quotedItem = ciQuote,
itemEdited = edited
),
linkMode = SimplexLinkMode.DESCRIPTION,
showMenu = showMenu,
receiveFile = {}
)
@@ -77,12 +77,14 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
val image = provider.getImage(index)
if (image == null) {
// No such image. Let's shrink total pages size or scroll to start of the list of pages to remove blank page automatically
scope.launch {
when (settledCurrentPage) {
index - 1 -> provider.totalImagesSize.value = settledCurrentPage + 1
index + 1 -> {
provider.scrollToStart()
pagerState.scrollToPage(0)
SideEffect {
scope.launch {
when (settledCurrentPage) {
index - 1 -> provider.totalImagesSize.value = settledCurrentPage + 1
index + 1 -> {
provider.scrollToStart()
pagerState.scrollToPage(0)
}
}
}
}
@@ -22,7 +22,7 @@ import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
Surface(
Modifier.clickable(onClick = {
AlertManager.shared.showAlertMsg(
@@ -45,7 +45,7 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci)
CIMetaView(ci, timedMessagesTTL)
}
}
}
@@ -59,7 +59,8 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
fun IntegrityErrorItemViewPreview() {
SimpleXTheme {
IntegrityErrorItemView(
ChatItem.getDeletedContentSampleData()
ChatItem.getDeletedContentSampleData(),
null
)
}
}
@@ -0,0 +1,58 @@
package chat.simplex.app.views.chat.item
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
Surface(
shape = RoundedCornerShape(18.dp),
color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight,
) {
Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
buildAnnotatedString {
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci, timedMessagesTTL)
}
}
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode"
)
@Composable
fun PreviewMarkedDeletedItemView() {
SimpleXTheme {
DeletedItemView(
ChatItem.getSampleData(itemDeleted = true),
null
)
}
}
@@ -1,20 +1,30 @@
package chat.simplex.app.views.chat.item
import android.app.Activity
import android.content.ActivityNotFoundException
import android.util.Log
import androidx.annotation.IntRange
import androidx.compose.foundation.text.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.core.text.BidiFormatter
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.detectGesture
import kotlinx.coroutines.*
val reserveTimestampStyle = SpanStyle(color = Color.Transparent)
val boldFont = SpanStyle(fontWeight = FontWeight.Medium)
@@ -36,42 +46,101 @@ fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolea
}
}
private val noTyping: AnnotatedString = AnnotatedString(" ")
private val typingIndicators: List<AnnotatedString> = listOf(
typing(FontWeight.Black) + typing() + typing(),
typing(FontWeight.Bold) + typing(FontWeight.Black) + typing(),
typing() + typing(FontWeight.Bold) + typing(FontWeight.Black),
typing() + typing() + typing(FontWeight.Bold)
)
private fun typingIndicator(recent: Boolean, @IntRange (from = 0, to = 4) typingIdx: Int): AnnotatedString = buildAnnotatedString {
pushStyle(SpanStyle(color = HighOrLowlight, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp))
append(if (recent) typingIndicators[typingIdx] else noTyping)
}
private fun typing(w: FontWeight = FontWeight.Light): AnnotatedString =
AnnotatedString(".", SpanStyle(fontWeight = w))
@Composable
fun MarkdownText (
text: String,
text: CharSequence,
formattedText: List<FormattedText>? = null,
sender: String? = null,
metaText: String? = null,
edited: Boolean = false,
meta: CIMeta? = null,
chatTTL: Int? = null,
style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp),
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
uriHandler: UriHandler? = null,
senderBold: Boolean = false,
modifier: Modifier = Modifier,
linkMode: SimplexLinkMode,
inlineContent: Map<String, InlineTextContent>? = null,
onLinkLongClick: (link: String) -> Unit = {}
) {
val textLayoutDirection = remember (text) {
if (BidiFormatter.getInstance().isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
}
val reserve = when {
textLayoutDirection != LocalLayoutDirection.current && metaText != null -> "\n"
edited -> " "
else -> " "
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
"\n"
} else if (meta != null) {
reserveSpaceForMeta(meta, chatTTL)
} else {
" "
}
val scope = rememberCoroutineScope()
CompositionLocalProvider(
LocalLayoutDirection provides if (textLayoutDirection != LocalLayoutDirection.current)
if (LocalLayoutDirection.current == LayoutDirection.Ltr) LayoutDirection.Rtl else LayoutDirection.Ltr
else
LocalLayoutDirection.current
) {
var timer: Job? by remember { mutableStateOf(null) }
var typingIdx by rememberSaveable { mutableStateOf(0) }
fun stopTyping() {
timer?.cancel()
timer = null
}
fun switchTyping() {
if (meta != null && meta.isLive && meta.recent) {
timer = timer ?: scope.launch {
while (isActive) {
typingIdx = (typingIdx + 1) % typingIndicators.size
delay(250)
}
}
} else {
stopTyping()
}
}
if (meta?.isLive == true) {
val activity = LocalContext.current as Activity
LaunchedEffect(meta.recent, meta.isLive) {
switchTyping()
}
DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation
onDispose {
if (orientation == activity.resources.configuration.orientation) {
stopTyping()
}
}
}
}
if (formattedText == null) {
val annotatedText = buildAnnotatedString {
appendSender(this, sender, senderBold)
append(text)
if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
if (text is String) append(text)
else if (text is AnnotatedString) append(text)
if (meta?.isLive == true) {
append(typingIndicator(meta.recent, typingIdx))
}
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
}
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent ?: mapOf())
} else {
var hasLinks = false
val annotatedText = buildAnnotatedString {
@@ -79,22 +148,29 @@ fun MarkdownText (
for (ft in formattedText) {
if (ft.format == null) append(ft.text)
else {
val link = ft.link
val link = ft.link(linkMode)
if (link != null) {
hasLinks = true
val ftStyle = ft.format.style
val ftStyle = if (ft.format is Format.SimplexLink && !ft.format.trustedUri && linkMode == SimplexLinkMode.BROWSER) {
SpanStyle(color = Color.Red, textDecoration = TextDecoration.Underline)
} else {
ft.format.style
}
withAnnotation(tag = "URL", annotation = link) {
withStyle(ftStyle) { append(ft.text) }
withStyle(ftStyle) { append(ft.viewText(linkMode)) }
}
} else {
withStyle(ft.format.style) { append(ft.text) }
}
}
}
if (meta?.isLive == true) {
append(typingIndicator(meta.recent, typingIdx))
}
// With RTL language set globally links looks bad sometimes, better to add a new line to bo sure everything looks good
/*if (metaText != null && hasLinks && LocalLayoutDirection.current == LayoutDirection.Rtl)
withStyle(reserveTimestampStyle) { append("\n" + metaText) }
else */if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
}
if (hasLinks && uriHandler != null) {
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,
@@ -104,7 +180,15 @@ fun MarkdownText (
},
onClick = { offset ->
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
.firstOrNull()?.let { annotation -> uriHandler.openUri(annotation.item) }
.firstOrNull()?.let { annotation ->
try {
uriHandler.openUri(annotation.item)
} catch (e: ActivityNotFoundException) {
// It can happen, for example, when you click on a text 0.00001 but don't have any app that can catch
// `tel:` scheme in url installed on a device (no phone app or contacts, maybe)
Log.e(TAG, "Open url: ${e.stackTraceToString()}")
}
}
},
shouldConsumeEvent = { offset ->
annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any()
@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.annotatedStringResource
import chat.simplex.app.views.usersettings.MarkdownHelpView
import chat.simplex.app.views.usersettings.simplexTeamUri
val bold = SpanStyle(fontWeight = FontWeight.Bold)
@@ -76,6 +77,15 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Text(annotatedStringResource(R.string.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(R.string.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
}
Column(
Modifier.padding(vertical = 24.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2)
MarkdownHelpView()
}
}
}
@@ -11,15 +11,20 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.chat.group.deleteGroupDialog
import chat.simplex.app.views.chat.group.leaveGroupDialog
import chat.simplex.app.views.chat.item.InvalidJSONView
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.ContactConnectionInfoView
@@ -33,22 +38,25 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
}
val stopped = chatModel.chatRunning.value == false
val linkMode by remember { chatModel.controller.appPrefs.simplexLinkMode.state }
LaunchedEffect(chat.id) {
showMenu.value = false
delay(500L)
}
when (chat.chatInfo) {
is ChatInfo.Direct ->
is ChatInfo.Direct -> {
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped) },
chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) },
click = { directChatAction(chat.chatInfo, chatModel) },
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
showMenu,
stopped
)
}
is ChatInfo.Group ->
ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped) },
chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) },
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) },
showMenu,
@@ -74,6 +82,18 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
showMenu,
stopped
)
is ChatInfo.InvalidJSON ->
ChatListNavLinkLayout(
chatLinkPreview = {
InvalidDataView()
},
click = {
ModalManager.shared.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
},
dropdownMenuItems = null,
showMenu,
stopped
)
}
}
@@ -281,7 +301,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
if (chatModel.incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.Check,
color = if (chatModel.incognito.value) Indigo else MaterialTheme.colors.onBackground,
onClick = {
acceptContactRequest(chatInfo, chatModel)
acceptContactRequest(chatInfo.apiId, chatInfo, true, chatModel)
showMenu.value = false
}
)
@@ -319,6 +339,29 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
)
}
@Composable
private fun InvalidDataView() {
Row {
ProfileImage(72.dp, null, Icons.Filled.AccountCircle, HighOrLowlight)
Column(
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1F)
) {
Text(
stringResource(R.string.invalid_data),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3,
fontWeight = FontWeight.Bold,
color = Color.Red
)
val height = with(LocalDensity.current) { 46.sp.toDp() }
Spacer(Modifier.height(height))
}
}
}
fun markChatRead(c: Chat, chatModel: ChatModel) {
var chat = c
withApi {
@@ -366,16 +409,16 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
title = generalGetString(R.string.accept_connection_request__question),
text = generalGetString(R.string.if_you_choose_to_reject_the_sender_will_not_be_notified),
confirmText = if (chatModel.incognito.value) generalGetString(R.string.accept_contact_incognito_button) else generalGetString(R.string.accept_contact_button),
onConfirm = { acceptContactRequest(contactRequest, chatModel) },
onConfirm = { acceptContactRequest(contactRequest.apiId, contactRequest, true, chatModel) },
dismissText = generalGetString(R.string.reject_contact_button),
onDismiss = { rejectContactRequest(contactRequest, chatModel) }
)
}
fun acceptContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
fun acceptContactRequest(apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
withApi {
val contact = chatModel.controller.apiAcceptContactRequest(contactRequest.apiId)
if (contact != null) {
val contact = chatModel.controller.apiAcceptContactRequest(apiId)
if (contact != null && isCurrentUser && contactRequest != null) {
val chat = Chat(ChatInfo.Direct(contact), listOf())
chatModel.replaceChat(contactRequest.id, chat)
}
@@ -584,9 +627,13 @@ fun PreviewChatListNavLinkDirect() {
),
chatStats = Chat.ChatStats()
),
null,
null,
false,
null,
stopped = false
null,
stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION
)
},
click = {},
@@ -621,9 +668,13 @@ fun PreviewChatListNavLinkGroup() {
),
chatStats = Chat.ChatStats()
),
null,
null,
false,
null,
stopped = false
null,
stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION
)
},
click = {},
@@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -13,41 +14,60 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.*
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.NewChatSheet
import chat.simplex.app.views.onboarding.WhatsNewView
import chat.simplex.app.views.onboarding.shouldShowWhatsNew
import chat.simplex.app.views.usersettings.SettingsView
import chat.simplex.app.views.usersettings.simplexTeamUri
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@Composable
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
val newChatSheetState by rememberSaveable(stateSaver = NewChatSheetState.saver()) { mutableStateOf(MutableStateFlow(NewChatSheetState.GONE)) }
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val showNewChatSheet = {
newChatSheetState.value = NewChatSheetState.VISIBLE
newChatSheetState.value = AnimatedViewState.VISIBLE
}
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
if (animated) newChatSheetState.value = NewChatSheetState.HIDING
else newChatSheetState.value = NewChatSheetState.GONE
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
else newChatSheetState.value = AnimatedViewState.GONE
}
LaunchedEffect(Unit) {
if (shouldShowWhatsNew(chatModel)) {
delay(1000L)
ModalManager.shared.showCustomModal { close -> WhatsNewView(close = close) }
}
}
LaunchedEffect(chatModel.clearOverlays.value) {
if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false)
}
LaunchedEffect(chatModel.appOpenUrl.value) {
val url = chatModel.appOpenUrl.value
if (url != null) {
chatModel.appOpenUrl.value = null
connectIfOpenedViaUri(url, chatModel)
}
}
var searchInList by rememberSaveable { mutableStateOf("") }
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val switchingUsers = rememberSaveable { mutableStateOf(false) }
Scaffold(
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, stopped) { searchInList = it.trim() } },
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
scaffoldState = scaffoldState,
drawerContent = { SettingsView(chatModel, setPerformLA) },
floatingActionButton = {
@@ -80,7 +100,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
) {
if (chatModel.chats.isNotEmpty()) {
ChatList(chatModel, search = searchInList)
} else {
} else if (!switchingUsers.value) {
Box(Modifier.fillMaxSize()) {
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
OnboardingButtons(showNewChatSheet)
@@ -94,6 +114,17 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (searchInList.isEmpty()) {
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
}
UserPicker(chatModel, userPickerState, switchingUsers) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
}
if (switchingUsers.value) {
Box(
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
contentAlignment = Alignment.Center
) {
ProgressIndicator()
}
}
}
@Composable
@@ -139,7 +170,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
}
@Composable
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
var showSearch by rememberSaveable { mutableStateOf(false) }
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
if (showSearch) {
@@ -172,10 +203,23 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
val scope = rememberCoroutineScope()
DefaultTopAppBar(
navigationButton = {
if (showSearch)
if (showSearch) {
NavigationButtonBack(hideSearchOnBack)
else
} else if (chatModel.users.isEmpty()) {
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
} else {
val users by remember { derivedStateOf { chatModel.users.toList() } }
val allRead = users
.filter { !it.user.activeUser }
.all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
if (users.size == 1) {
scope.launch { drawerState.open() }
} else {
userPickerState.value = AnimatedViewState.VISIBLE
}
}
}
},
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -202,6 +246,47 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
Divider(Modifier.padding(top = AppBarHeight))
}
@Composable
private fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
IconButton(onClick = onButtonClicked) {
Box {
ProfileImage(
image = image,
size = 37.dp
)
if (!allRead) {
unreadBadge()
}
}
}
}
@Composable
private fun BoxScope.unreadBadge(text: String? = "") {
Text(
text ?: "",
color = MaterialTheme.colors.onPrimary,
fontSize = 6.sp,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.badgeLayout()
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp)
.align(Alignment.TopEnd)
)
}
@Composable
private fun ProgressIndicator() {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
}
private var lazyListState = 0 to 0
@Composable
@@ -4,17 +4,20 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -22,11 +25,22 @@ import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.ComposePreview
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.views.helpers.*
@Composable
fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean) {
fun ChatPreviewView(
chat: Chat,
chatModelDraft: ComposeState?,
chatModelDraftChatId: ChatId?,
chatModelIncognito: Boolean,
currentUserProfileDisplayName: String?,
contactNetworkStatus: NetworkStatus?,
stopped: Boolean,
linkMode: SimplexLinkMode
) {
val cInfo = chat.chatInfo
@Composable
@@ -63,11 +77,58 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
)
}
@Composable
fun VerifiedIcon() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight)
}
fun messageDraft(draft: ComposeState): Pair<AnnotatedString, Map<String, InlineTextContent>> {
fun attachment(): Pair<ImageVector, String?>? =
when (draft.preview) {
is ComposePreview.FilePreview -> Icons.Filled.InsertDriveFile to draft.preview.fileName
is ComposePreview.ImagePreview -> Icons.Outlined.Image to null
is ComposePreview.VoicePreview -> Icons.Filled.PlayArrow to durationText(draft.preview.durationMs / 1000)
else -> null
}
val attachment = attachment()
val text = buildAnnotatedString {
appendInlineContent(id = "editIcon")
append(" ")
if (attachment != null) {
appendInlineContent(id = "attachmentIcon")
if (attachment.second != null) {
append(attachment.second as String)
}
append(" ")
}
append(draft.message)
}
val inlineContent: Map<String, InlineTextContent> = mapOf(
"editIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(Icons.Outlined.EditNote, null, tint = MaterialTheme.colors.primary)
},
"attachmentIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(attachment?.first ?: Icons.Outlined.EditNote, null, tint = HighOrLowlight)
}
)
return text to inlineContent
}
@Composable
fun chatPreviewTitle() {
when (cInfo) {
is ChatInfo.Direct ->
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight)
Row(verticalAlignment = Alignment.CenterVertically) {
if (cInfo.contact.verified) {
VerifiedIcon()
}
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight)
}
is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary)
@@ -82,15 +143,30 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
fun chatPreviewText(chatModelIncognito: Boolean) {
val ci = chat.chatItems.lastOrNull()
if (ci != null) {
val (text: CharSequence, inlineTextContent) = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
!ci.meta.itemDeleted -> ci.text to null
else -> generalGetString(R.string.marked_deleted_description) to null
}
val formattedText = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
!ci.meta.itemDeleted -> ci.formattedText
else -> null
}
MarkdownText(
ci.text,
ci.formattedText,
sender = if (cInfo is ChatInfo.Group && !ci.chatDir.sent) ci.memberDisplayName else null,
text,
formattedText,
sender = when {
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName
else -> null
},
linkMode = linkMode,
senderBold = true,
metaText = null,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
inlineContent = inlineTextContent,
modifier = Modifier.fillMaxWidth(),
)
} else {
@@ -178,7 +254,7 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
Modifier.padding(top = 52.dp),
contentAlignment = Alignment.Center
) {
ChatStatusImage(chat)
ChatStatusImage(contactNetworkStatus)
}
}
}
@@ -201,10 +277,9 @@ fun unreadCountStr(n: Int): String {
}
@Composable
fun ChatStatusImage(chat: Chat) {
val s = chat.serverInfo.networkStatus
val descr = s.statusString
if (s is Chat.NetworkStatus.Error) {
fun ChatStatusImage(s: NetworkStatus?) {
val descr = s?.statusString
if (s is NetworkStatus.Error) {
Icon(
Icons.Outlined.ErrorOutline,
contentDescription = descr,
@@ -212,7 +287,7 @@ fun ChatStatusImage(chat: Chat) {
modifier = Modifier
.size(19.dp)
)
} else if (s !is Chat.NetworkStatus.Connected) {
} else if (s !is NetworkStatus.Connected) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
@@ -232,6 +307,6 @@ fun ChatStatusImage(chat: Chat) {
@Composable
fun PreviewChatPreviewView() {
SimpleXTheme {
ChatPreviewView(Chat.sampleData, false, "", stopped = false)
ChatPreviewView(Chat.sampleData, null, null, false, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
}
}
@@ -29,7 +29,7 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
stopped
)
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection -> {}
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {}
}
}
@@ -0,0 +1,198 @@
package chat.simplex.app.views.chatlist
import SectionItemViewSpaceBetween
import android.util.Log
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, openSettings: () -> Unit) {
val scope = rememberCoroutineScope()
var newChat by remember { mutableStateOf(userPickerState.value) }
val users by remember { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } }
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
LaunchedEffect(Unit) {
launch {
userPickerState.collect {
newChat = it
launch {
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE
}
}
}
}
LaunchedEffect(Unit) {
snapshotFlow { newChat.isVisible() }
.distinctUntilChanged()
.filter { it }
.collect {
try {
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
var same = users.size == updatedUsers.size
if (same) {
for (i in 0 until minOf(users.size, updatedUsers.size)) {
val prev = updatedUsers[i].user
val next = users[i].user
if (prev.userId != next.userId || prev.activeUser != next.activeUser || prev.chatViewName != next.chatViewName || prev.image != next.image) {
same = false
break
}
}
}
if (!same) {
chatModel.users.clear()
chatModel.users.addAll(updatedUsers)
}
} catch (e: Exception) {
Log.e(TAG, "Error updating users ${e.stackTraceToString()}")
}
}
}
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density }
Box(Modifier
.fillMaxSize()
.offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else xOffset, 0) }
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
.padding(bottom = 10.dp, top = 10.dp)
.graphicsLayer {
alpha = animatedFloat.value
translationY = (animatedFloat.value - 1) * xOffset
}
) {
Column(
Modifier
.widthIn(min = 220.dp)
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
) {
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
openSettings()
userPickerState.value = AnimatedViewState.GONE
}) {
userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) {
chatModel.chats.clear()
scope.launch {
val job = launch {
delay(500)
switchingUsers.value = true
}
chatModel.controller.changeActiveUser(u.user.userId)
job.cancel()
switchingUsers.value = false
}
}
}
Divider(Modifier.requiredHeight(1.dp))
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
}
}
SettingsPickerItem {
openSettings()
userPickerState.value = AnimatedViewState.GONE
}
}
}
}
@Composable
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = 46.dp)
.combinedClickable(
onClick = if (u.activeUser) openSettings else onClick,
onLongClick = onLongClick,
interactionSource = remember { MutableInteractionSource() },
indication = if (!u.activeUser) LocalIndication.current else null
)
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
ProfileImage(
image = u.image,
size = 54.dp
)
Text(
u.displayName,
modifier = Modifier
.padding(start = 8.dp, end = 8.dp),
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
if (u.activeUser) {
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
} else if (unreadCount > 0) {
Row {
Text(
unreadCountStr(unreadCount),
color = MaterialTheme.colors.onPrimary,
fontSize = 11.sp,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp),
textAlign = TextAlign.Center,
maxLines = 1
)
Spacer(Modifier.width(2.dp))
}
} else {
Box(Modifier.size(20.dp))
}
}
}
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
Text(
text,
color = MaterialTheme.colors.onBackground,
)
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
}
}
@@ -27,6 +27,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
@@ -84,8 +86,11 @@ fun DatabaseView(
chatArchiveTime,
chatLastStart,
chatDbDeleted.value,
m.controller.appPrefs.privacyFullBackup,
appFilesCountAndSize,
chatItemTTL,
m.currentUser.value,
m.users,
startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) },
stopChatAlert = { stopChatAlert(m, runChat, context) },
exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
@@ -126,14 +131,17 @@ fun DatabaseLayout(
chatDbChanged: Boolean,
useKeyChain: Boolean,
chatDbEncrypted: Boolean?,
initialRandomDBPassphrase: Preference<Boolean>,
initialRandomDBPassphrase: SharedPreference<Boolean>,
importArchiveLauncher: ManagedActivityResultLauncher<String, Uri?>,
chatArchiveName: MutableState<String?>,
chatArchiveTime: MutableState<Instant?>,
chatLastStart: MutableState<Instant?>,
chatDbDeleted: Boolean,
privacyFullBackup: SharedPreference<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
chatItemTTL: MutableState<ChatItemTTL>,
currentUser: User?,
users: List<UserInfo>,
startChat: () -> Unit,
stopChatAlert: () -> Unit,
exportArchive: () -> Unit,
@@ -146,10 +154,27 @@ fun DatabaseLayout(
val operationsDisabled = !stopped || progressIndicator
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 48.dp),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.your_chat_database))
SectionView(stringResource(R.string.messages_section_title).uppercase()) {
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
}
SectionTextFooter(
remember(currentUser?.displayName) {
buildAnnotatedString {
append(generalGetString(R.string.messages_section_description) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(currentUser?.displayName ?: "")
}
append(".")
}
}
)
SectionSpacer()
SectionView(stringResource(R.string.run_chat_section)) {
RunChatSetting(runChat, stopped, chatDbDeleted, startChat, stopChatAlert)
}
@@ -165,6 +190,8 @@ fun DatabaseLayout(
disabled = operationsDisabled
)
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.Backup, stringResource(R.string.full_backup), privacyFullBackup)
SectionDivider()
SettingsActionItem(
Icons.Outlined.IosShare,
stringResource(R.string.export_database),
@@ -220,16 +247,14 @@ fun DatabaseLayout(
)
SectionSpacer()
SectionView(stringResource(R.string.data_section)) {
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
SectionDivider()
SectionView(stringResource(R.string.files_and_media_section).uppercase()) {
val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0
SectionItemView(
deleteAppFilesAndMedia,
disabled = deleteFilesDisabled
) {
Text(
stringResource(R.string.delete_files_and_media),
stringResource(if (users.size > 1) R.string.delete_files_and_media_for_all_users else R.string.delete_files_and_media_all),
color = if (deleteFilesDisabled) HighOrLowlight else Color.Red
)
}
@@ -683,14 +708,17 @@ fun PreviewDatabaseLayout() {
chatDbChanged = false,
useKeyChain = false,
chatDbEncrypted = false,
initialRandomDBPassphrase = Preference({ true }, {}),
initialRandomDBPassphrase = SharedPreference({ true }, {}),
importArchiveLauncher = rememberGetContentLauncher {},
chatArchiveName = remember { mutableStateOf("dummy_archive") },
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },
chatLastStart = remember { mutableStateOf(Clock.System.now()) },
chatDbDeleted = false,
privacyFullBackup = SharedPreference({ true }, {}),
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) },
currentUser = User.sampleData,
users = listOf(UserInfo.sampleData),
startChat = {},
stopChatAlert = {},
exportArchive = {},
@@ -1,11 +1,20 @@
package chat.simplex.app.views.helpers
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Dialog
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.ui.theme.*
class AlertManager {
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
@@ -35,6 +44,36 @@ class AlertManager {
}
}
fun showAlertDialogButtonsColumn(
title: String,
text: AnnotatedString? = null,
buttons: @Composable () -> Unit,
) {
showAlert {
Dialog(onDismissRequest = this::hideAlert) {
Column(Modifier.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)) {
Text(title,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = if (text == null) DEFAULT_PADDING else DEFAULT_PADDING_HALF),
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
)
if (text != null) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
Text(
text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
fontSize = 14.sp,
)
}
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
buttons()
}
}
}
}
}
fun showAlertDialog(
title: String,
text: String? = null,
@@ -67,6 +106,41 @@ class AlertManager {
}
}
fun showAlertDialogStacked(
title: String,
text: String? = null,
confirmText: String = generalGetString(R.string.ok),
onConfirm: (() -> Unit)? = null,
dismissText: String = generalGetString(R.string.cancel_verb),
onDismiss: (() -> Unit)? = null,
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = { Text(title) },
text = alertText,
buttons = {
Column(
Modifier.fillMaxWidth().padding(horizontal = 8.dp).padding(top = 16.dp, bottom = 2.dp),
horizontalAlignment = Alignment.End
) {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
},
)
}
}
fun showAlertMsg(
title: String, text: String? = null,
confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null
@@ -102,4 +176,4 @@ class AlertManager {
companion object {
val shared = AlertManager()
}
}
}
@@ -11,7 +11,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.*
@Composable
fun CloseSheetBar(close: () -> Unit) {
fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) {
Column(
Modifier
.fillMaxWidth()
@@ -20,9 +20,15 @@ fun CloseSheetBar(close: () -> Unit) {
) {
Row(
Modifier
.width(TitleInsetWithIcon - AppBarHorizontalPadding)
.padding(top = 4.dp), // Like in DefaultAppBar
content = { NavigationButtonBack(close) }
content = {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
NavigationButtonBack(close)
Row {
endButtons()
}
}
}
)
}
}
@@ -30,7 +36,7 @@ fun CloseSheetBar(close: () -> Unit) {
@Composable
fun AppBarTitle(title: String, withPadding: Boolean = true) {
val padding = if (withPadding)
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING )
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
else
PaddingValues(bottom = DEFAULT_PADDING)
Text(
@@ -53,6 +53,15 @@ fun NavigationButtonBack(onButtonClicked: () -> Unit) {
}
}
@Composable
fun ShareButton(onButtonClicked: () -> Unit) {
IconButton(onButtonClicked) {
Icon(
Icons.Outlined.Share, stringResource(R.string.share_verb), tint = MaterialTheme.colors.primary
)
}
}
@Composable
fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
IconButton(onClick = onButtonClicked) {
@@ -1,9 +1,13 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.app.views.helpers
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.runtime.saveable.Saver
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
sealed class SharedContent {
data class Text(val text: String): SharedContent()
@@ -11,7 +15,7 @@ sealed class SharedContent {
data class File(val text: String, val uri: Uri): SharedContent()
}
enum class NewChatSheetState {
enum class AnimatedViewState {
VISIBLE, HIDING, GONE;
fun isVisible(): Boolean {
return this == VISIBLE
@@ -23,7 +27,7 @@ enum class NewChatSheetState {
return this == GONE
}
companion object {
fun saver(): Saver<MutableStateFlow<NewChatSheetState>, *> = Saver(
fun saver(): Saver<MutableStateFlow<AnimatedViewState>, *> = Saver(
save = { it.value.toString() },
restore = {
MutableStateFlow(valueOf(it))
@@ -32,7 +36,16 @@ enum class NewChatSheetState {
}
}
sealed class UploadContent {
data class SimpleImage(val uri: Uri): UploadContent()
data class AnimatedImage(val uri: Uri): UploadContent()
@Serializer(forClass = Uri::class)
object UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uri) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString())
}
@Serializable
sealed class UploadContent {
@Serializable data class SimpleImage(val uri: Uri): UploadContent()
@Serializable data class AnimatedImage(val uri: Uri): UploadContent()
}
@@ -10,9 +10,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
@@ -27,7 +30,7 @@ fun <T> ExposedDropDownSettingRow(
onSelected: (T) -> Unit
) {
Row(
Modifier.fillMaxWidth(),
Modifier.fillMaxWidth().padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var expanded by remember { mutableStateOf(false) }
@@ -40,9 +43,7 @@ fun <T> ExposedDropDownSettingRow(
tint = iconTint
)
}
Text(title, color = if (enabled.value) Color.Unspecified else HighOrLowlight)
Spacer(Modifier.fillMaxWidth().weight(1f))
Text(title, Modifier.weight(1f), color = if (enabled.value) Color.Unspecified else HighOrLowlight)
ExposedDropdownMenuBox(
expanded = expanded,
@@ -55,8 +56,10 @@ fun <T> ExposedDropDownSettingRow(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
val maxWidth = with(LocalDensity.current){ 180.sp.toDp() }
Text(
values.first { it.first == selection.value }.second + (if (label != null) " $label" else ""),
Modifier.widthIn(max = maxWidth),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = HighOrLowlight
@@ -3,6 +3,7 @@ package chat.simplex.app.views.helpers
import android.Manifest
import android.app.Activity
import android.content.*
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.graphics.*
import android.net.Uri
@@ -32,11 +33,9 @@ import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.json
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.chat.PickFromGallery
import chat.simplex.app.views.newchat.ActionButton
import kotlinx.serialization.builtins.*
import kotlinx.serialization.decodeFromString
import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.math.min
@@ -177,6 +176,25 @@ fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLaunche
fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivityResultLauncher<String, List<Uri>> =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetMultipleContents(), cb)
fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
try {
launch(null)
} catch (e: ActivityNotFoundException) {
// No Activity found to handle Intent android.media.action.IMAGE_CAPTURE
// Means, no system camera app (Android 11+ requirement)
// https://developer.android.com/about/versions/11/behavior-changes-11#media-capture
Log.e(TAG, "Camera launcher: " + e.stackTraceToString())
try {
// Try to open any camera just to capture an image, will not be returned like with previous intent
SimplexApp.context.startActivity(Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).also { it.addFlags(FLAG_ACTIVITY_NEW_TASK) })
} catch (e: ActivityNotFoundException) {
// No camera apps available at all
Log.e(TAG, "Camera launcher2: " + e.stackTraceToString())
}
}
}
@Composable
fun GetImageBottomSheet(
imageBitmap: MutableState<Uri?>,
@@ -204,7 +222,7 @@ fun GetImageBottomSheet(
}
val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
if (isGranted) {
cameraLauncher.launch(null)
cameraLauncher.launchWithFallback()
hideBottomSheet()
} else {
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
@@ -228,7 +246,7 @@ fun GetImageBottomSheet(
ActionButton(null, stringResource(R.string.use_camera_button), icon = Icons.Outlined.PhotoCamera) {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
cameraLauncher.launch(null)
cameraLauncher.launchWithFallback()
hideBottomSheet()
}
else -> {
@@ -20,12 +20,13 @@ fun ModalView(
close: () -> Unit,
background: Color = MaterialTheme.colors.background,
modifier: Modifier = Modifier,
endButtons: @Composable RowScope.() -> Unit = {},
content: @Composable () -> Unit,
) {
BackHandler(onBack = close)
Surface(Modifier.fillMaxSize()) {
Column(Modifier.background(background)) {
CloseSheetBar(close)
CloseSheetBar(close, endButtons)
Box(modifier) { content() }
}
}
@@ -37,9 +38,9 @@ class ModalManager {
private val toRemove = mutableSetOf<Int>()
private var oldViewChanging = AtomicBoolean(false)
fun showModal(settings: Boolean = false, content: @Composable () -> Unit) {
fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
showCustomModal { close ->
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, content = content)
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, endButtons = endButtons, content = content)
}
}
@@ -1,45 +1,36 @@
package chat.simplex.app.views.helpers
import android.app.Application
import android.content.Context
import android.media.*
import android.media.AudioManager.AudioPlaybackCallback
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
import android.os.Build
import android.util.Log
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatItem
import chat.simplex.app.views.helpers.AudioPlayer.duration
import kotlinx.coroutines.*
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
interface Recorder {
val recordingInProgress: MutableState<Boolean>
fun start(onStop: () -> Unit): String
fun stop()
fun cancel(filePath: String, recordingInProgress: MutableState<Boolean>)
}
data class ProgressAndDuration(
val progressMs: Int = 0,
val durationMs: Int = 0
) {
companion object {
val Saver
get() = Saver<MutableState<ProgressAndDuration>, Pair<Int, Int>>(
save = { it.value.progressMs to it.value.durationMs },
restore = { mutableStateOf(ProgressAndDuration(it.first, it.second)) }
)
}
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
fun stop(): Int
}
class RecorderNative(private val recordedBytesLimit: Long): Recorder {
companion object {
// Allows to stop the recorder from outside without having the recorder in a variable
var stopRecording: (() -> Unit)? = null
const val extension = "m4a"
}
override val recordingInProgress = mutableStateOf(false)
private var recorder: MediaRecorder? = null
private var progressJob: Job? = null
private var filePath: String? = null
private var recStartedAt: Long? = null
private fun initRecorder() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(SimplexApp.context)
@@ -47,9 +38,8 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
MediaRecorder()
}
override fun start(onStop: () -> Unit): String {
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
AudioPlayer.stop()
recordingInProgress.value = true
val rec: MediaRecorder
recorder = initRecorder().also { rec = it }
rec.setAudioSource(MediaRecorder.AudioSource.MIC)
@@ -58,28 +48,39 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
rec.setAudioChannels(1)
rec.setAudioSamplingRate(16000)
rec.setAudioEncodingBitRate(16000)
rec.setMaxDuration(-1)
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
rec.setMaxFileSize(recordedBytesLimit)
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val filePath = getAppFilePath(SimplexApp.context, uniqueCombine(SimplexApp.context, getAppFilePath(SimplexApp.context, "voice_${timestamp}.m4a")))
rec.setOutputFile(filePath)
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
val fileToSave = File.createTempFile(generateNewFileName(SimplexApp.context, "voice", "${extension}_"), ".tmp", tmpDir)
fileToSave.deleteOnExit()
val path = fileToSave.absolutePath
filePath = path
rec.setOutputFile(path)
rec.prepare()
rec.start()
rec.setOnInfoListener { mr, what, extra ->
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
stop()
onStop()
recStartedAt = System.currentTimeMillis()
progressJob = CoroutineScope(Dispatchers.Default).launch {
while(isActive) {
onProgressUpdate(progress(), false)
delay(50)
}
}.apply {
invokeOnCompletion {
onProgressUpdate(realDuration(path), true)
}
}
stopRecording = { stop(); onStop() }
return filePath
rec.setOnInfoListener { _, what, _ ->
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED || what == MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
stop()
}
}
stopRecording = { stop() }
return path
}
override fun stop() {
if (!recordingInProgress.value) return
override fun stop(): Int {
val path = filePath ?: return 0
stopRecording = null
recordingInProgress.value = false
recorder?.metrics?.
runCatching {
recorder?.stop()
}
@@ -87,16 +88,25 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
recorder?.reset()
}
runCatching {
// release all resources
recorder?.release()
}
// Await coroutine finishes in order to send real duration to it's listener
runBlocking {
progressJob?.cancelAndJoin()
}
progressJob = null
filePath = null
recorder = null
return (realDuration(path) ?: 0).also { recStartedAt = null }
}
override fun cancel(filePath: String, recordingInProgress: MutableState<Boolean>) {
stop()
runCatching { File(filePath).delete() }.getOrElse { Log.d(TAG, "Unable to delete a file: ${it.stackTraceToString()}") }
}
private fun progress(): Int? = recStartedAt?.let { (System.currentTimeMillis() - it).toInt() }
/**
* Return real duration from [AudioPlayer] if it's possible (should always be possible).
* As a fallback, return internally counted duration
* */
private fun realDuration(path: String): Int? = duration(path) ?: progress()
}
object AudioPlayer {
@@ -107,6 +117,17 @@ object AudioPlayer {
.setUsage(AudioAttributes.USAGE_MEDIA)
.build()
)
(SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager)
.registerAudioPlaybackCallback(object: AudioPlaybackCallback() {
override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) {
if (configs?.any { it.audioAttributes.usage == AudioAttributes.USAGE_VOICE_COMMUNICATION } == true) {
// In a process of making a call
RecorderNative.stopRecording?.invoke()
stop()
}
super.onPlaybackConfigChanged(configs)
}
}, null)
}
private val helperPlayer: MediaPlayer = MediaPlayer().apply {
setAudioAttributes(
@@ -116,55 +137,79 @@ object AudioPlayer {
.build()
)
}
// Filepath: String, onStop: () -> Unit
private val currentlyPlaying: MutableState<Pair<String, () -> Unit>?> = mutableStateOf(null)
// Filepath: String, onProgressUpdate
private val currentlyPlaying: MutableState<Pair<String, (position: Int?, state: TrackState) -> Unit>?> = mutableStateOf(null)
private var progressJob: Job? = null
fun start(filePath: String, seek: Int? = null, onStop: () -> Unit): Boolean {
enum class TrackState {
PLAYING, PAUSED, REPLACED
}
// Returns real duration of the track
private fun start(filePath: String, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
if (!File(filePath).exists()) {
Log.e(TAG, "No such file: $filePath")
return false
return null
}
RecorderNative.stopRecording?.invoke()
val current = currentlyPlaying.value
if (current == null || current.first != filePath) {
stopListener()
player.reset()
// Notify prev audio listener about stop
current?.second?.invoke()
runCatching {
player.setDataSource(filePath)
}.onFailure {
Log.e(TAG, it.stackTraceToString())
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), it.message)
return false
return null
}
runCatching { player.prepare() }.onFailure {
// Can happen when audio file is broken
Log.e(TAG, it.stackTraceToString())
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), it.message)
return false
return null
}
}
if (seek != null) player.seekTo(seek)
player.start()
// Repeated calls to play/pause on the same track will not recompose all dependent views
if (currentlyPlaying.value?.first != filePath) {
currentlyPlaying.value = filePath to onStop
currentlyPlaying.value = filePath to onProgressUpdate
progressJob = CoroutineScope(Dispatchers.Default).launch {
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
while(isActive && player.isPlaying) {
// Even when current position is equal to duration, the player has isPlaying == true for some time,
// so help to make the playback stopped in UI immediately
if (player.currentPosition == player.duration) {
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
break
}
delay(50)
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
}
/*
* Since coroutine is still NOT canceled, means player ended (no stop/no pause). But in some cases
* the player can show position != duration even if they actually equal.
* Let's say to a listener that the position == duration in case of coroutine finished without cancel
* */
if (isActive) {
onProgressUpdate(player.duration, TrackState.PAUSED)
}
onProgressUpdate(null, TrackState.PAUSED)
}
return true
return player.duration
}
fun pause(): Int {
private fun pause(): Int {
progressJob?.cancel()
progressJob = null
player.pause()
return player.currentPosition
}
fun stop() {
if (!player.isPlaying) return
// Notify prev audio listener about stop
currentlyPlaying.value?.second?.invoke()
currentlyPlaying.value = null
if (currentlyPlaying.value == null) return
player.stop()
stopListener()
}
fun stop(item: ChatItem) = stop(item.file?.fileName)
@@ -176,16 +221,59 @@ object AudioPlayer {
}
}
/**
* If player starts playing at 2637 ms in a track 2816 ms long (these numbers are just an example),
* it will stop immediately after start but will not change currentPosition, so it will not be equal to duration.
* However, it sets isPlaying to false. Let's do it ourselves in order to prevent endless waiting loop
* */
fun progressAndDurationOrEnded(): ProgressAndDuration =
ProgressAndDuration(if (player.isPlaying) player.currentPosition else player.duration, player.duration)
private fun stopListener() {
val afterCoroutineCancel: CompletionHandler = {
// Notify prev audio listener about stop
currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED)
currentlyPlaying.value = null
}
/** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be:
* [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.REPLACED] (in this order)
* */
if (progressJob != null) {
progressJob?.invokeOnCompletion(afterCoroutineCancel)
} else {
afterCoroutineCancel(null)
}
progressJob?.cancel()
progressJob = null
}
fun duration(filePath: String): Int {
var res = 0
fun play(
filePath: String?,
audioPlaying: MutableState<Boolean>,
progress: MutableState<Int>,
duration: MutableState<Int>,
resetOnEnd: Boolean,
) {
if (progress.value == duration.value) {
progress.value = 0
}
val realDuration = start(filePath ?: return, progress.value) { pro, state ->
if (pro != null) {
progress.value = pro
}
if (pro == null || pro == duration.value) {
audioPlaying.value = false
if (pro == duration.value) {
progress.value = if (resetOnEnd) 0 else duration.value
} else if (state == TrackState.REPLACED) {
progress.value = 0
}
}
}
audioPlaying.value = realDuration != null
// Update to real duration instead of what was received in ChatInfo
realDuration?.let { duration.value = it }
}
fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
pro.value = pause()
audioPlaying.value = false
}
fun duration(filePath: String): Int? {
var res: Int? = null
kotlin.runCatching {
helperPlayer.setDataSource(filePath)
helperPlayer.prepare()
@@ -1,4 +1,5 @@
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
@@ -10,6 +11,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.app.ui.theme.*
@@ -31,6 +34,28 @@ fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(),
}
}
@Composable
fun SectionView(
title: String,
icon: ImageVector,
iconTint: Color = HighOrLowlight,
leadingIcon: Boolean = false,
padding: PaddingValues = PaddingValues(),
content: (@Composable ColumnScope.() -> Unit)
) {
Column {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
Row(Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), verticalAlignment = Alignment.CenterVertically) {
if (leadingIcon) Icon(icon, null, Modifier.padding(end = 4.dp).size(iconSize), tint = iconTint)
Text(title, color = HighOrLowlight, style = MaterialTheme.typography.body2, fontSize = 12.sp)
if (!leadingIcon) Icon(icon, null, Modifier.padding(start = 4.dp).size(iconSize), tint = iconTint)
}
Surface(color = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background) {
Column(Modifier.padding(padding).fillMaxWidth()) { content() }
}
}
}
@Composable
fun <T> SectionViewSelectable(
title: String?,
@@ -56,7 +81,12 @@ fun <T> SectionViewSelectable(
}
@Composable
fun SectionItemView(click: (() -> Unit)? = null, minHeight: Dp = 46.dp, disabled: Boolean = false, content: (@Composable RowScope.() -> Unit)) {
fun SectionItemView(
click: (() -> Unit)? = null,
minHeight: Dp = 46.dp,
disabled: Boolean = false,
content: (@Composable RowScope.() -> Unit)
) {
val modifier = Modifier
.fillMaxWidth()
.sizeIn(minHeight = minHeight)
@@ -71,6 +101,7 @@ fun SectionItemView(click: (() -> Unit)? = null, minHeight: Dp = 46.dp, disabled
@Composable
fun SectionItemViewSpaceBetween(
click: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
minHeight: Dp = 46.dp,
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
disabled: Boolean = false,
@@ -80,7 +111,7 @@ fun SectionItemViewSpaceBetween(
.fillMaxWidth()
.sizeIn(minHeight = minHeight)
Row(
if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding),
if (click == null || disabled) modifier.padding(padding) else modifier.combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -129,6 +160,11 @@ fun <T> SectionItemWithValue(
@Composable
fun SectionTextFooter(text: String) {
SectionTextFooter(AnnotatedString(text))
}
@Composable
fun SectionTextFooter(text: AnnotatedString) {
Text(
text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
@@ -158,9 +194,13 @@ fun SectionSpacer() {
}
@Composable
fun InfoRow(title: String, value: String) {
fun InfoRow(title: String, value: String, icon: ImageVector? = null, iconTint: Color? = null) {
SectionItemViewSpaceBetween {
Text(title)
Row {
val iconSize = with(LocalDensity.current) { 21.sp.toDp() }
if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: HighOrLowlight)
Text(title)
}
Text(value, color = HighOrLowlight)
}
}
@@ -28,6 +28,22 @@ fun SimpleButton(text: String, icon: ImageVector,
}
}
@Composable
fun SimpleButton(
text: String, icon: ImageVector,
color: Color = MaterialTheme.colors.primary,
disabled: Boolean,
click: () -> Unit
) {
SimpleButtonFrame(click, disabled = disabled) {
Icon(
icon, text, tint = if (disabled) HighOrLowlight else color,
modifier = Modifier.padding(end = 8.dp)
)
Text(text, style = MaterialTheme.typography.caption, color = if (disabled) HighOrLowlight else color)
}
}
@Composable
fun SimpleButtonIconEnded(
text: String,
@@ -12,21 +12,28 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun TextEditor(modifier: Modifier, text: MutableState<String>) {
fun TextEditor(
modifier: Modifier,
text: MutableState<String>,
border: Boolean = true,
fontSize: TextUnit = 14.sp,
background: Color = MaterialTheme.colors.background,
onChange: ((String) -> Unit)? = null
) {
BasicTextField(
value = text.value,
onValueChange = { text.value = it },
onValueChange = { text.value = it; onChange?.invoke(it) },
textStyle = TextStyle(
fontFamily = FontFamily.Monospace, fontSize = 14.sp,
fontFamily = FontFamily.Monospace, fontSize = fontSize,
color = MaterialTheme.colors.onBackground
),
keyboardOptions = KeyboardOptions.Default.copy(
@@ -37,17 +44,17 @@ fun TextEditor(modifier: Modifier, text: MutableState<String>) {
cursorBrush = SolidColor(HighOrLowlight),
decorationBox = { innerTextField ->
Surface(
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
shape = if (border) RoundedCornerShape(10.dp) else RectangleShape,
border = if (border) BorderStroke(1.dp, MaterialTheme.colors.secondary) else null
) {
Row(
Modifier.background(MaterialTheme.colors.background),
Modifier.background(background),
verticalAlignment = Alignment.Top
) {
Box(
Modifier
.weight(1f)
.padding(vertical = 5.dp, horizontal = 7.dp)
.padding(vertical = 5.dp, horizontal = if (border) 7.dp else DEFAULT_PADDING)
) {
innerTextField()
}
@@ -1,7 +1,5 @@
package chat.simplex.app.views.helpers
import android.R.attr.factor
import android.R.color
import android.content.Context
import android.content.res.Resources
import android.graphics.*
@@ -28,11 +26,13 @@ import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.text.HtmlCompat
import chat.simplex.app.*
import chat.simplex.app.model.CIFile
import chat.simplex.app.model.json
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
@@ -43,6 +43,9 @@ fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalSc
fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job =
scope.launch { withContext(Dispatchers.Main, action) }
fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job =
CoroutineScope(Dispatchers.Default).launch(block = action)
enum class KeyboardState {
Opened, Closed
}
@@ -225,7 +228,7 @@ const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
const val MAX_VOICE_MILLIS_FOR_SENDING: Long = 43_000 // approximately is ok
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
const val MAX_FILE_SIZE: Long = 8000000
@@ -241,6 +244,11 @@ fun getAppFilePath(context: Context, fileName: String): String {
return "${getAppFilesDirectory(context)}/$fileName"
}
fun getAppFileUri(fileName: String): Uri {
return Uri.parse("${getAppFilesDirectory(SimplexApp.context)}/$fileName")
}
fun getLoadedFilePath(context: Context, file: CIFile?): String? {
return if (file?.filePath != null && file.loaded) {
val filePath = getAppFilePath(context, file.filePath)
@@ -328,8 +336,7 @@ fun saveImage(context: Context, image: Bitmap): String? {
return try {
val ext = if (image.hasAlpha()) "png" else "jpg"
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
val fileToSave = generateNewFileName(context, "IMG", ext)
val file = File(getAppFilePath(context, fileToSave))
val output = FileOutputStream(file)
dataResized.writeTo(output)
@@ -352,8 +359,7 @@ fun saveAnimImage(context: Context, uri: Uri): String? {
}
// Just in case the image has a strange extension
if (ext.length < 3 || ext.length > 4) ext = "gif"
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
val fileToSave = generateNewFileName(context, "IMG", ext)
val file = File(getAppFilePath(context, fileToSave))
val output = FileOutputStream(file)
context.contentResolver.openInputStream(uri)!!.use { input ->
@@ -387,15 +393,23 @@ fun saveFileFromUri(context: Context, uri: Uri): String? {
}
}
fun generateNewFileName(context: Context, prefix: String, ext: String): String {
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("GMT")
val timestamp = sdf.format(Date())
return uniqueCombine(context, "${prefix}_$timestamp.$ext")
}
fun uniqueCombine(context: Context, fileName: String): String {
fun tryCombine(fileName: String, n: Int): String {
val name = File(fileName).nameWithoutExtension
val ext = File(fileName).extension
val orig = File(fileName)
val name = orig.nameWithoutExtension
val ext = orig.extension
fun tryCombine(n: Int): String {
val suffix = if (n == 0) "" else "_$n"
val f = "$name$suffix.$ext"
return if (File(getAppFilePath(context, f)).exists()) tryCombine(fileName, n + 1) else f
return if (File(getAppFilePath(context, f)).exists()) tryCombine(n + 1) else f
}
return tryCombine(fileName, 0)
return tryCombine(0)
}
fun formatBytes(bytes: Long): String {
@@ -462,3 +476,9 @@ val LongRange.Companion.saver
save = { it.value.first to it.value.last },
restore = { mutableStateOf(it.first..it.second) }
)
/* Make sure that T class has @Serializable annotation */
inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
save = { json.encodeToString(it) },
restore = { json.decodeFromString(it) }
)
@@ -47,7 +47,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
setGroupMembers(groupInfo, chatModel)
close.invoke()
ModalManager.shared.showModalCloseable(true) { close ->
AddGroupMembersView(groupInfo, chatModel, close)
AddGroupMembersView(groupInfo, true, chatModel, close)
}
}
}
@@ -134,7 +134,13 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
CreateGroupButton(MaterialTheme.colors.primary, Modifier
.clickable { createGroup(GroupProfile(displayName.value, fullName.value, profileImage.value)) }
.clickable {
createGroup(GroupProfile(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
))
}
.padding(8.dp))
} else {
CreateGroupButton(HighOrLowlight, Modifier.padding(8.dp))
@@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<NewChatSheetState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
NewChatSheetLayout(
newChatSheetState,
@@ -63,7 +63,7 @@ private val icons = listOf(Icons.Outlined.AddLink, Icons.Outlined.QrCode, Icons.
@Composable
private fun NewChatSheetLayout(
newChatSheetState: StateFlow<NewChatSheetState>,
newChatSheetState: StateFlow<AnimatedViewState>,
stopped: Boolean,
addContact: () -> Unit,
connectViaLink: () -> Unit,
@@ -216,7 +216,7 @@ fun ActionButton(
private fun PreviewNewChatSheet() {
SimpleXTheme {
NewChatSheetLayout(
MutableStateFlow(NewChatSheetState.VISIBLE),
MutableStateFlow(AnimatedViewState.VISIBLE),
stopped = false,
addContact = {},
connectViaLink = {},
@@ -16,11 +16,12 @@ import kotlinx.coroutines.launch
enum class OnboardingStage {
Step1_SimpleXInfo,
Step2_CreateProfile,
Step3_SetNotificationsMode,
OnboardingComplete
}
@Composable
fun CreateProfile(chatModel: ChatModel) {
fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
@@ -33,7 +34,10 @@ fun CreateProfile(chatModel: ChatModel) {
.background(color = MaterialTheme.colors.background)
.padding(20.dp)
) {
CreateProfilePanel(chatModel)
CreateProfilePanel(chatModel, close)
LaunchedEffect(Unit) {
setLastVersionDefault(chatModel)
}
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
@@ -0,0 +1,68 @@
package chat.simplex.app.views.onboarding
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationsMode
import chat.simplex.app.views.usersettings.changeNotificationsMode
@Composable
fun SetNotificationsMode(m: ChatModel) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(20.dp)
) {
AppBarTitle(stringResource(R.string.onboarding_notifications_mode_title), false)
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
Text(stringResource(R.string.onboarding_notifications_mode_subtitle))
Spacer(Modifier.padding(DEFAULT_PADDING_HALF))
NotificationButton(currentMode, NotificationsMode.OFF, R.string.onboarding_notifications_mode_off, R.string.onboarding_notifications_mode_off_desc)
NotificationButton(currentMode, NotificationsMode.PERIODIC, R.string.onboarding_notifications_mode_periodic, R.string.onboarding_notifications_mode_periodic_desc)
NotificationButton(currentMode, NotificationsMode.SERVICE, R.string.onboarding_notifications_mode_service, R.string.onboarding_notifications_mode_service_desc)
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage) {
changeNotificationsMode(currentMode.value, m)
}
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}
}
@Composable
private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mode: NotificationsMode, @StringRes title: Int, @StringRes description: Int) {
TextButton(
onClick = { currentMode.value = mode },
border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight.copy(alpha = 0.5f)),
shape = RoundedCornerShape(15.dp),
) {
Column(Modifier.padding(bottom = 6.dp).padding(horizontal = 8.dp)) {
Text(
stringResource(title),
style = MaterialTheme.typography.h2,
fontWeight = FontWeight.Medium,
color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier.padding(bottom = 4.dp)
)
Text(annotatedStringResource(description), color = MaterialTheme.colors.onBackground, lineHeight = 24.sp)
}
}
Spacer(Modifier.height(DEFAULT_PADDING))
}
@@ -25,7 +25,6 @@ import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ModalManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
@@ -105,14 +104,14 @@ private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: I
@Composable
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
if (user == null) {
ActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
} else {
ActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
}
}
@Composable
private fun ActionButton(
fun OnboardingActionButton(
@StringRes labelId: Int,
onboarding: OnboardingStage?,
onboardingStage: MutableState<OnboardingStage?>,
@@ -0,0 +1,301 @@
package chat.simplex.app.views.onboarding
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) }
@Composable
fun featureDescription(icon: ImageVector, titleId: Int, descrId: Int, link: String?) {
@Composable
fun linkButton(link: String) {
val uriHandler = LocalUriHandler.current
Icon(
Icons.Outlined.OpenInNew, stringResource(titleId), tint = MaterialTheme.colors.primary,
modifier = Modifier
.clickable { uriHandler.openUri(link) }
)
}
Column(
horizontalAlignment = Alignment.Start
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(icon, stringResource(titleId), tint = HighOrLowlight)
Text(
generalGetString(titleId),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3,
fontWeight = FontWeight.Medium
)
if (link != null) {
linkButton(link)
}
}
Text(generalGetString(descrId))
}
}
@Composable
fun pagination() {
Row(
Modifier
.padding(bottom = 16.dp)
) {
if (currentVersion.value > 0) {
val prev = currentVersion.value - 1
Surface(shape = RoundedCornerShape(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.clickable { currentVersion.value = prev }
.padding(8.dp)
) {
Icon(Icons.Outlined.ArrowBackIosNew, "previous", tint = MaterialTheme.colors.primary)
Text(versionDescriptions[prev].version, color = MaterialTheme.colors.primary)
}
}
}
Spacer(Modifier.fillMaxWidth().weight(1f))
if (currentVersion.value < versionDescriptions.lastIndex) {
val next = currentVersion.value + 1
Surface(shape = RoundedCornerShape(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.clickable { currentVersion.value = next }
.padding(8.dp)
) {
Text(versionDescriptions[next].version, color = MaterialTheme.colors.primary)
Icon(Icons.Outlined.ArrowForwardIos, "next", tint = MaterialTheme.colors.primary)
}
}
}
}
}
val v = versionDescriptions[currentVersion.value]
ModalView(close = close) {
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
String.format(generalGetString(R.string.new_in_version), v.version),
Modifier
.fillMaxWidth()
.padding(DEFAULT_PADDING),
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1,
fontWeight = FontWeight.Normal,
color = HighOrLowlight
)
v.features.forEach { feature ->
featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link)
}
if (!viaSettings) {
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(
Modifier.fillMaxWidth(), contentAlignment = Alignment.Center
) {
Text(
generalGetString(R.string.ok),
modifier = Modifier.clickable(onClick = close),
style = MaterialTheme.typography.h3,
color = MaterialTheme.colors.primary
)
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}
Spacer(Modifier.fillMaxHeight().weight(1f))
pagination()
}
}
}
private data class FeatureDescription(
val icon: ImageVector,
val titleId: Int,
val descrId: Int,
val link: String? = null
)
private data class VersionDescription(
val version: String,
val features: List<FeatureDescription>
)
private val versionDescriptions: List<VersionDescription> = listOf(
VersionDescription(
version = "v4.2",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.VerifiedUser,
titleId = R.string.v4_2_security_assessment,
descrId = R.string.v4_2_security_assessment_desc,
link = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"
),
FeatureDescription(
icon = Icons.Outlined.Group,
titleId = R.string.v4_2_group_links,
descrId = R.string.v4_2_group_links_desc
),
FeatureDescription(
icon = Icons.Outlined.Check,
titleId = R.string.v4_2_auto_accept_contact_requests,
descrId = R.string.v4_2_auto_accept_contact_requests_desc
),
)
),
VersionDescription(
version = "v4.3",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.Mic,
titleId = R.string.v4_3_voice_messages,
descrId = R.string.v4_3_voice_messages_desc
),
FeatureDescription(
icon = Icons.Outlined.DeleteForever,
titleId = R.string.v4_3_irreversible_message_deletion,
descrId = R.string.v4_3_irreversible_message_deletion_desc
),
FeatureDescription(
icon = Icons.Outlined.WifiTethering,
titleId = R.string.v4_3_improved_server_configuration,
descrId = R.string.v4_3_improved_server_configuration_desc
),
FeatureDescription(
icon = Icons.Outlined.VisibilityOff,
titleId = R.string.v4_3_improved_privacy_and_security,
descrId = R.string.v4_3_improved_privacy_and_security_desc
),
)
),
VersionDescription(
version = "v4.4",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.Timer,
titleId = R.string.v4_4_disappearing_messages,
descrId = R.string.v4_4_disappearing_messages_desc
),
FeatureDescription(
icon = Icons.Outlined.Pending,
titleId = R.string.v4_4_live_messages,
descrId = R.string.v4_4_live_messages_desc
),
FeatureDescription(
icon = Icons.Outlined.VerifiedUser,
titleId = R.string.v4_4_verify_connection_security,
descrId = R.string.v4_4_verify_connection_security_desc
),
FeatureDescription(
icon = Icons.Outlined.Translate,
titleId = R.string.v4_4_french_interface,
descrId = R.string.v4_4_french_interface_descr
)
)
),
VersionDescription(
version = "v4.5",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.ManageAccounts,
titleId = R.string.v4_5_multiple_chat_profiles,
descrId = R.string.v4_5_multiple_chat_profiles_descr
),
FeatureDescription(
icon = Icons.Outlined.EditNote,
titleId = R.string.v4_5_message_draft,
descrId = R.string.v4_5_message_draft_descr
),
FeatureDescription(
icon = Icons.Outlined.SafetyDivider,
titleId = R.string.v4_5_transport_isolation,
descrId = R.string.v4_5_transport_isolation_descr,
link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation"
),
FeatureDescription(
icon = Icons.Outlined.Task,
titleId = R.string.v4_5_private_filenames,
descrId = R.string.v4_5_private_filenames_descr
),
FeatureDescription(
icon = Icons.Outlined.Battery2Bar,
titleId = R.string.v4_5_reduced_battery_usage,
descrId = R.string.v4_5_reduced_battery_usage_descr
),
FeatureDescription(
icon = Icons.Outlined.Translate,
titleId = R.string.v4_5_italian_interface,
descrId = R.string.v4_5_italian_interface_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
)
)
private val lastVersion = versionDescriptions.last().version
fun setLastVersionDefault(m: ChatModel) {
m.controller.appPrefs.whatsNewVersion.set(lastVersion)
}
fun shouldShowWhatsNew(m: ChatModel): Boolean {
val v = m.controller.appPrefs.whatsNewVersion.get()
setLastVersionDefault(m)
return v != lastVersion
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewWhatsNewView() {
SimpleXTheme {
WhatsNewView(
viaSettings = true,
close = {}
)
}
}
@@ -33,6 +33,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) }
val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) }
val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) }
val networkTCPKeepIdle: MutableState<Int>
val networkTCPKeepIntvl: MutableState<Int>
@@ -48,10 +49,6 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
}
fun buildCfg(): NetCfg {
val socksProxy = currentCfg.value.socksProxy
val tcpConnectTimeout = networkTCPConnectTimeout.value
val tcpTimeout = networkTCPTimeout.value
val smpPingInterval = networkSMPPingInterval.value
val enableKeepAlive = networkEnableKeepAlive.value
val tcpKeepAlive = if (enableKeepAlive) {
val keepIdle = networkTCPKeepIdle.value
@@ -62,11 +59,15 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
null
}
return NetCfg(
socksProxy = socksProxy,
tcpConnectTimeout = tcpConnectTimeout,
tcpTimeout = tcpTimeout,
socksProxy = currentCfg.value.socksProxy,
hostMode = currentCfg.value.hostMode,
requiredHostMode = currentCfg.value.requiredHostMode,
sessionMode = currentCfg.value.sessionMode,
tcpConnectTimeout = networkTCPConnectTimeout.value,
tcpTimeout = networkTCPTimeout.value,
tcpKeepAlive = tcpKeepAlive,
smpPingInterval = smpPingInterval
smpPingInterval = networkSMPPingInterval.value,
smpPingCount = networkSMPPingCount.value
)
}
@@ -74,6 +75,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPConnectTimeout.value = cfg.tcpConnectTimeout
networkTCPTimeout.value = cfg.tcpTimeout
networkSMPPingInterval.value = cfg.smpPingInterval
networkSMPPingCount.value = cfg.smpPingCount
networkEnableKeepAlive.value = cfg.enableKeepAlive
if (cfg.tcpKeepAlive != null) {
networkTCPKeepIdle.value = cfg.tcpKeepAlive.keepIdle
@@ -113,6 +115,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPConnectTimeout,
networkTCPTimeout,
networkSMPPingInterval,
networkSMPPingCount,
networkEnableKeepAlive,
networkTCPKeepIdle,
networkTCPKeepIntvl,
@@ -129,6 +132,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPConnectTimeout: MutableState<Long>,
networkTCPTimeout: MutableState<Long>,
networkSMPPingInterval: MutableState<Long>,
networkSMPPingCount: MutableState<Int>,
networkEnableKeepAlive: MutableState<Boolean>,
networkTCPKeepIdle: MutableState<Int>,
networkTCPKeepIntvl: MutableState<Int>,
@@ -170,7 +174,14 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
SectionItemView {
TimeoutSettingRow(
stringResource(R.string.network_option_ping_interval), networkSMPPingInterval,
listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000), secondsLabel
listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000), secondsLabel
)
}
SectionDivider()
SectionItemView {
IntSettingRow(
stringResource(R.string.network_option_ping_count), networkSMPPingCount,
listOf(1, 2, 3, 5, 8), ""
)
}
SectionDivider()
@@ -412,6 +423,7 @@ fun PreviewAdvancedNetworkSettingsLayout() {
networkTCPConnectTimeout = remember { mutableStateOf(10_000000) },
networkTCPTimeout = remember { mutableStateOf(10_000000) },
networkSMPPingInterval = remember { mutableStateOf(10_000000) },
networkSMPPingCount = remember { mutableStateOf(3) },
networkEnableKeepAlive = remember { mutableStateOf(true) },
networkTCPKeepIdle = remember { mutableStateOf(10) },
networkTCPKeepIntvl = remember { mutableStateOf(10) },
@@ -24,8 +24,6 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
@@ -65,11 +63,6 @@ fun AppearanceView() {
AppearanceLayout(
appIcon,
changeIcon = ::setAppIcon,
showThemeSelector = {
ModalManager.shared.showModal(true) {
ThemeSelectorView()
}
},
editPrimaryColor = { primary ->
ModalManager.shared.showModalCloseable { close ->
ColorEditor(primary, close)
@@ -81,7 +74,6 @@ fun AppearanceView() {
@Composable fun AppearanceLayout(
icon: MutableState<AppIcon>,
changeIcon: (AppIcon) -> Unit,
showThemeSelector: () -> Unit,
editPrimaryColor: (Color) -> Unit,
) {
Column(
@@ -115,8 +107,12 @@ fun AppearanceView() {
SectionSpacer()
val currentTheme by CurrentColors.collectAsState()
SectionView(stringResource(R.string.settings_section_title_themes)) {
SectionItemViewSpaceBetween(showThemeSelector) {
Text(generalGetString(R.string.theme))
SectionItemViewSpaceBetween {
val darkTheme = isSystemInDarkTheme()
val state = remember { derivedStateOf { currentTheme.second } }
ThemeSelector(state) {
ThemeManager.applyTheme(it.name, darkTheme)
}
}
SectionDivider()
SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }) {
@@ -183,6 +179,21 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
)
}
@Composable
private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme) -> Unit) {
val darkTheme = isSystemInDarkTheme()
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second to it.third }) }
ExposedDropDownSettingRow(
generalGetString(R.string.theme),
values,
state,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
SimplexApp.context.packageManager.getComponentEnabledSetting(
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
@@ -196,7 +207,6 @@ fun PreviewAppearanceSettings() {
AppearanceLayout(
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
changeIcon = {},
showThemeSelector = {},
editPrimaryColor = {},
)
}
@@ -9,6 +9,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -30,8 +31,8 @@ fun CallSettingsView(m: ChatModel,
@Composable
fun CallSettingsLayout(
webrtcPolicyRelay: Preference<Boolean>,
callOnLockScreen: Preference<CallOnLockScreen>,
webrtcPolicyRelay: SharedPreference<Boolean>,
callOnLockScreen: SharedPreference<CallOnLockScreen>,
editIceServers: () -> Unit,
) {
Column(
@@ -79,9 +80,10 @@ private fun LockscreenOpts(lockscreenOpts: State<CallOnLockScreen>, enabled: Sta
@Composable
fun SharedPreferenceToggle(
text: String,
preference: Preference<Boolean>,
preferenceState: MutableState<Boolean>? = null
) {
preference: SharedPreference<Boolean>,
preferenceState: MutableState<Boolean>? = null,
onChange: ((Boolean) -> Unit)? = null,
) {
val prefState = preferenceState ?: remember { mutableStateOf(preference.get()) }
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(text, Modifier.padding(end = 24.dp))
@@ -91,6 +93,7 @@ fun SharedPreferenceToggle(
onCheckedChange = {
preference.set(it)
prefState.value = it
onChange?.invoke(it)
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
@@ -106,12 +109,12 @@ fun SharedPreferenceToggleWithIcon(
icon: ImageVector,
stopped: Boolean = false,
onClickInfo: () -> Unit,
preference: Preference<Boolean>,
preference: SharedPreference<Boolean>,
preferenceState: MutableState<Boolean>? = null
) {
val prefState = preferenceState ?: remember { mutableStateOf(preference.get()) }
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(text, Modifier.padding(end = 4.dp))
Text(text, Modifier.padding(end = 4.dp), color = if (stopped) HighOrLowlight else Color.Unspecified)
Icon(
icon,
null,
@@ -135,7 +138,7 @@ fun SharedPreferenceToggleWithIcon(
}
@Composable
fun <T>SharedPreferenceRadioButton(text: String, prefState: MutableState<T>, preference: Preference<T>, value: T) {
fun <T>SharedPreferenceRadioButton(text: String, prefState: MutableState<T>, preference: SharedPreference<T>, value: T) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text)
val colors = RadioButtonDefaults.colors(selectedColor = MaterialTheme.colors.primary)
@@ -3,7 +3,9 @@ package chat.simplex.app.views.usersettings
import android.annotation.SuppressLint
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Log
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
import java.security.KeyStore
@@ -31,7 +33,7 @@ internal class Cryptor {
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
return String(cipher.doFinal(data))
return runCatching { String(cipher.doFinal(data))}.onFailure { Log.e(TAG, "doFinal: ${it.stackTraceToString()}") }.getOrNull()
}
fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> {
@@ -17,18 +17,13 @@ import chat.simplex.app.model.Format
import chat.simplex.app.model.FormatColor
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.AppBarTitle
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun MarkdownHelpView() {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.how_to_use_markdown), false)
Text(stringResource(R.string.you_can_use_markdown_to_format_messages__prompt))
Spacer(Modifier.height(DEFAULT_PADDING))
val bold = stringResource(R.string.bold)
@@ -31,11 +31,17 @@ fun NetworkAndServersView(
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
LaunchedEffect(Unit) {
chatModel.userSMPServersUnsaved.value = null
}
NetworkAndServersLayout(
developerTools = developerTools,
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
sessionMode = sessionMode,
showModal = showModal,
showSettingsModal = showSettingsModal,
toggleSocksProxy = { enable ->
@@ -78,9 +84,13 @@ fun NetworkAndServersView(
OnionHosts.PREFER -> generalGetString(R.string.network_use_onion_hosts_prefer_desc_in_alert)
OnionHosts.REQUIRED -> generalGetString(R.string.network_use_onion_hosts_required_desc_in_alert)
}
updateOnionHostsDialog(startsWith, onDismiss = {
onionHosts.value = prevValue
}) {
updateNetworkSettingsDialog(
title = generalGetString(R.string.update_onion_hosts_settings_question),
startsWith,
onDismiss = {
onionHosts.value = prevValue
}
) {
withApi {
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
@@ -92,6 +102,31 @@ fun NetworkAndServersView(
}
}
}
},
updateSessionMode = {
if (sessionMode.value == it) return@NetworkAndServersLayout
val prevValue = sessionMode.value
sessionMode.value = it
val startsWith = when (it) {
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
}
updateNetworkSettingsDialog(
title = generalGetString(R.string.update_network_session_mode_question),
startsWith,
onDismiss = { sessionMode.value = prevValue }
) {
withApi {
val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it)
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
if (res) {
chatModel.controller.setNetCfg(newCfg)
sessionMode.value = it
} else {
sessionMode.value = prevValue
}
}
}
}
)
}
@@ -100,10 +135,12 @@ fun NetworkAndServersView(
developerTools: Boolean,
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,
) {
Column(
Modifier.fillMaxWidth(),
@@ -112,17 +149,19 @@ fun NetworkAndServersView(
) {
AppBarTitle(stringResource(R.string.network_and_servers))
SectionView(generalGetString(R.string.settings_section_title_messages)) {
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showModal { SMPServersView(it) })
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showSettingsModal { SMPServersView(it) })
SectionDivider()
SectionItemView {
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
}
SectionDivider()
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
SectionDivider()
if (developerTools) {
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
SectionDivider()
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
}
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
}
Spacer(Modifier.height(8.dp))
SectionView(generalGetString(R.string.settings_section_title_calls)) {
@@ -181,7 +220,6 @@ private fun UseOnionHosts(
}
}
val onSelected = showModal {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
@@ -201,14 +239,47 @@ private fun UseOnionHosts(
)
}
private fun updateOnionHostsDialog(
@Composable
private fun SessionModePicker(
sessionMode: MutableState<TransportSessionMode>,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
updateSessionMode: (TransportSessionMode) -> Unit,
) {
val values = remember {
TransportSessionMode.values().map {
when (it) {
TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(R.string.network_session_mode_user), generalGetString(R.string.network_session_mode_user_description))
TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(R.string.network_session_mode_entity), generalGetString(R.string.network_session_mode_entity_description))
}
}
}
SectionItemWithValue(
generalGetString(R.string.network_session_mode_transport_isolation),
sessionMode,
values,
icon = Icons.Outlined.SafetyDivider,
onSelected = showModal {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.network_session_mode_transport_isolation))
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
}
}
)
}
private fun updateNetworkSettingsDialog(
title: String,
startsWith: String = "",
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
onDismiss: () -> Unit,
onConfirm: () -> Unit
) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.update_onion_hosts_settings_question),
title = title,
text = startsWith + "\n\n" + message,
confirmText = generalGetString(R.string.update_network_settings_confirmation),
onDismiss = onDismiss,
@@ -228,7 +299,9 @@ fun PreviewNetworkAndServersLayout() {
showSettingsModal = { {} },
toggleSocksProxy = {},
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
useOnion = {},
updateSessionMode = {},
)
}
}
@@ -46,25 +46,6 @@ enum class NotificationPreviewMode {
fun NotificationsSettingsView(
chatModel: ChatModel,
) {
val onNotificationsModeSelected = { mode: NotificationsMode ->
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
}
chatModel.notificationsMode.value = mode
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE)
SimplexService.start(SimplexApp.context)
else
SimplexService.safeStopService(SimplexApp.context)
}
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
}
val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode ->
chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
chatModel.notificationPreviewMode.value = mode
@@ -76,7 +57,7 @@ fun NotificationsSettingsView(
showPage = { page ->
ModalManager.shared.showModalCloseable(true) {
when (page) {
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode, onNotificationsModeSelected)
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode) { changeNotificationsMode(it, chatModel) }
CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)
}
}
@@ -159,7 +140,7 @@ fun NotificationPreviewView(
}
// mode, name, description
fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
val res = ArrayList<ValueTitleDesc<NotificationsMode>>()
res.add(
ValueTitleDesc(
@@ -211,3 +192,23 @@ fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> {
)
return res
}
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
}
chatModel.notificationsMode.value = mode
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE)
SimplexService.start(SimplexApp.context)
else
SimplexService.safeStopService(SimplexApp.context)
}
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
}
@@ -0,0 +1,144 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
var preferences by rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(user.fullPreferences) }
var currentPreferences by rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(preferences) }
fun savePrefs(afterSave: () -> Unit = {}) {
withApi {
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
val updatedProfile = m.controller.apiUpdateProfile(newProfile)
if (updatedProfile != null) {
m.updateCurrentUser(updatedProfile, preferences)
currentPreferences = preferences
}
afterSave()
}
}
ModalView(
close = {
if (preferences == currentPreferences) close()
else showUnsavedChangesAlert({ savePrefs(close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
PreferencesLayout(
preferences,
currentPreferences,
applyPrefs = { preferences = it },
reset = { preferences = currentPreferences },
savePrefs = ::savePrefs,
)
}
}
@Composable
private fun PreferencesLayout(
preferences: FullChatPreferences,
currentPreferences: FullChatPreferences,
applyPrefs: (FullChatPreferences) -> Unit,
reset: () -> Unit,
savePrefs: () -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.your_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) }
TimedMessagesFeatureSection(timedMessages) {
applyPrefs(preferences.copy(timedMessages = TimedMessagesPreference(allow = if (it) FeatureAllowed.YES else FeatureAllowed.NO)))
}
SectionSpacer()
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) }
FeatureSection(ChatFeature.FullDelete, allowFullDeletion) {
applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it)))
}
SectionSpacer()
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) }
FeatureSection(ChatFeature.Voice, allowVoice) {
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
}
SectionSpacer()
ResetSaveButtons(
reset = reset,
save = savePrefs,
disabled = preferences == currentPreferences
)
}
}
@Composable
private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllowed>, onSelected: (FeatureAllowed) -> Unit) {
SectionView {
SectionItemView {
ExposedDropDownSettingRow(
feature.text,
FeatureAllowed.values().map { it to it.text },
allowFeature,
icon = feature.icon,
onSelected = onSelected
)
}
}
SectionTextFooter(feature.allowDescription(allowFeature.value))
}
@Composable
private fun TimedMessagesFeatureSection(allowFeature: State<FeatureAllowed>, onSelected: (Boolean) -> Unit) {
SectionView {
SectionItemView {
PreferenceToggleWithIcon(
ChatFeature.TimedMessages.text,
ChatFeature.TimedMessages.icon,
HighOrLowlight,
allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES,
onSelected
)
}
}
SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value))
}
@Composable
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(reset, disabled = disabled) {
Text(stringResource(R.string.reset_verb), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
SectionDivider()
SectionItemView(save, disabled = disabled) {
Text(stringResource(R.string.save_and_notify_contacts), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_contacts),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@@ -1,39 +1,86 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import android.view.WindowManager
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.AppBarTitle
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.*
@Composable
fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
fun PrivacySettingsView(
chatModel: ChatModel,
setPerformLA: (Boolean) -> Unit
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
AppBarTitle(stringResource(R.string.your_privacy))
SectionView(stringResource(R.string.settings_section_title_device)) {
ChatLockItem(chatModel.performLA, setPerformLA)
SectionDivider()
val context = LocalContext.current
SettingsPreferenceItem(Icons.Outlined.VisibilityOff, stringResource(R.string.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on ->
if (on) {
(context as? FragmentActivity)?.window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
} else {
(context as? FragmentActivity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
}
}
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_chats)) {
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SectionDivider()
if (chatModel.controller.appPrefs.developerTools.get()) {
SettingsPreferenceItem(Icons.Outlined.ImageAspectRatio, stringResource(R.string.transfer_images_faster), chatModel.controller.appPrefs.privacyTransferImagesInline)
SectionDivider()
}
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SectionDivider()
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
simplexLinkMode.set(it)
chatModel.simplexLinkMode.value = it
}) }
}
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
SectionTextFooter(stringResource(R.string.simplex_link_mode_browser_warning))
}
}
}
@Composable
private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onSelected: (SimplexLinkMode) -> Unit) {
val values = remember {
SimplexLinkMode.values().map {
when (it) {
SimplexLinkMode.DESCRIPTION -> it to generalGetString(R.string.simplex_link_mode_description)
SimplexLinkMode.FULL -> it to generalGetString(R.string.simplex_link_mode_full)
SimplexLinkMode.BROWSER -> it to generalGetString(R.string.simplex_link_mode_browser)
}
}
}
ExposedDropDownSettingRow(
generalGetString(R.string.simplex_link_mode),
values,
simplexLinkModeState,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
@@ -0,0 +1,200 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionItemViewSpaceBetween
import SectionSpacer
import SectionView
import android.util.Log
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Composable
fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
var testing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
SMPServerLayout(
testing,
server,
testServer = {
testing = true
scope.launch {
val res = testServerConnection(server, m)
if (isActive) {
onUpdate(res.first)
testing = false
}
}
},
onUpdate,
onDelete
)
if (testing) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
}
}
}
@Composable
private fun SMPServerLayout(
testing: Boolean,
server: ServerCfg,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(if (server.preset) R.string.smp_servers_preset_server else R.string.smp_servers_your_server))
if (server.preset) {
PresetServer(testing, server, testServer, onUpdate, onDelete)
} else {
CustomServer(testing, server, testServer, onUpdate, onDelete)
}
}
}
@Composable
private fun PresetServer(
testing: Boolean,
server: ServerCfg,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
) {
SectionView(stringResource(R.string.smp_servers_preset_address).uppercase()) {
SelectionContainer {
Text(
server.server,
Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp),
style = TextStyle(
fontFamily = FontFamily.Monospace, fontSize = 16.sp,
color = HighOrLowlight
)
)
}
}
SectionSpacer()
UseServerSection(true, testing, server, testServer, onUpdate, onDelete)
}
@Composable
private fun CustomServer(
testing: Boolean,
server: ServerCfg,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
) {
val serverAddress = remember { mutableStateOf(server.server) }
val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } }
SectionView(
stringResource(R.string.smp_servers_your_server_address).uppercase(),
icon = Icons.Outlined.ErrorOutline,
iconTint = if (!valid.value) MaterialTheme.colors.error else Color.Transparent,
) {
val testedPreviously = remember { mutableMapOf<String, Boolean?>() }
TextEditor(
Modifier.height(144.dp),
text = serverAddress,
border = false,
fontSize = 16.sp,
background = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background
) {
testedPreviously[server.server] = server.tested
onUpdate(server.copy(server = it, tested = testedPreviously[serverAddress.value]))
}
}
SectionSpacer()
UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete)
SectionSpacer()
if (valid.value) {
SectionView(stringResource(R.string.smp_servers_add_to_another_device).uppercase()) {
QRCode(serverAddress.value, Modifier.aspectRatio(1f))
}
}
}
@Composable
private fun UseServerSection(
valid: Boolean,
testing: Boolean,
server: ServerCfg,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
) {
SectionView(stringResource(R.string.smp_servers_use_server).uppercase()) {
SectionItemViewSpaceBetween(testServer, disabled = !valid || testing) {
Text(stringResource(R.string.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else HighOrLowlight)
ShowTestStatus(server)
}
SectionDivider()
SectionItemView {
val enabled = rememberUpdatedState(server.enabled)
PreferenceToggle(stringResource(R.string.smp_servers_use_server_for_new_conn), enabled.value) { onUpdate(server.copy(enabled = it)) }
}
SectionDivider()
SectionItemView(onDelete, disabled = testing) {
Text(stringResource(R.string.smp_servers_delete_server), color = if (testing) HighOrLowlight else MaterialTheme.colors.error)
}
}
}
@Composable
fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
when (server.tested) {
true -> Icon(Icons.Outlined.Check, null, modifier, tint = SimplexGreen)
false -> Icon(Icons.Outlined.Clear, null, modifier, tint = MaterialTheme.colors.error)
else -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Transparent)
}
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, SMPTestFailure?> =
try {
val r = m.controller.testSMPServer(server.server)
server.copy(tested = r == null) to r
} catch (e: Exception) {
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")
server.copy(tested = false) to null
}
fun serverHostname(srv: String): String =
parseServerAddress(srv)?.hostnames?.firstOrNull() ?: srv
@@ -1,258 +0,0 @@
package chat.simplex.app.views.usersettings
import SectionItemViewSpaceBetween
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun SMPServersView(chatModel: ChatModel) {
val userSMPServers = chatModel.userSMPServers.value
if (userSMPServers != null) {
var isUserSMPServers by remember { mutableStateOf(userSMPServers.isNotEmpty()) }
var editSMPServers by remember { mutableStateOf(!isUserSMPServers) }
val userSMPServersStr = remember { mutableStateOf(if (isUserSMPServers) userSMPServers.joinToString(separator = "\n") else "") }
fun saveSMPServers(smpServers: List<String>) {
withApi {
val r = chatModel.controller.setUserSMPServers(smpServers = smpServers)
if (r) {
chatModel.userSMPServers.value = smpServers
if (smpServers.isEmpty()) {
isUserSMPServers = false
editSMPServers = true
} else {
editSMPServers = false
}
}
}
}
SMPServersLayout(
isUserSMPServers = isUserSMPServers,
editSMPServers = editSMPServers,
userSMPServersStr = userSMPServersStr,
isUserSMPServersOnOff = { switch ->
if (switch) {
isUserSMPServers = true
} else {
val userSMPServers = chatModel.userSMPServers.value
if (userSMPServers != null) {
if (userSMPServers.isNotEmpty()) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.use_simplex_chat_servers__question),
text = generalGetString(R.string.saved_SMP_servers_will_be_removed),
confirmText = generalGetString(R.string.confirm_verb),
onConfirm = {
saveSMPServers(listOf())
isUserSMPServers = false
userSMPServersStr.value = ""
}
)
} else {
isUserSMPServers = false
userSMPServersStr.value = ""
}
}
}
},
cancelEdit = {
val userSMPServers = chatModel.userSMPServers.value
if (userSMPServers != null) {
isUserSMPServers = userSMPServers.isNotEmpty()
editSMPServers = !isUserSMPServers
userSMPServersStr.value = if (isUserSMPServers) userSMPServers.joinToString(separator = "\n") else ""
}
},
saveSMPServers = { saveSMPServers(it) },
editOn = { editSMPServers = true },
)
}
}
@Composable
fun SMPServersLayout(
isUserSMPServers: Boolean,
editSMPServers: Boolean,
userSMPServersStr: MutableState<String>,
isUserSMPServersOnOff: (Boolean) -> Unit,
cancelEdit: () -> Unit,
saveSMPServers: (List<String>) -> Unit,
editOn: () -> Unit,
) {
Column {
AppBarTitle(stringResource(R.string.your_SMP_servers))
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
SectionItemViewSpaceBetween(padding = PaddingValues()) {
Text(stringResource(R.string.configure_SMP_servers), Modifier.padding(end = 24.dp))
Switch(
checked = isUserSMPServers,
onCheckedChange = isUserSMPServersOnOff,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
),
)
}
if (!isUserSMPServers) {
Text(stringResource(R.string.using_simplex_chat_servers), lineHeight = 22.sp)
} else {
Text(stringResource(R.string.enter_one_SMP_server_per_line))
if (editSMPServers) {
TextEditor(Modifier.height(160.dp), text = userSMPServersStr)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(horizontalAlignment = Alignment.Start) {
Row {
Text(
stringResource(R.string.cancel_verb),
color = MaterialTheme.colors.primary,
modifier = Modifier
.clickable(onClick = cancelEdit)
)
Spacer(Modifier.padding(horizontal = 8.dp))
Text(
stringResource(R.string.save_servers_button),
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable(onClick = {
val servers = userSMPServersStr.value.split("\n")
saveSMPServers(servers)
})
)
}
}
Column(horizontalAlignment = Alignment.End) {
howToButton()
}
}
} else {
Surface(
modifier = Modifier
.height(160.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(10.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
) {
SelectionContainer(
Modifier.verticalScroll(rememberScrollState())
) {
Text(
userSMPServersStr.value,
Modifier
.padding(vertical = 5.dp, horizontal = 7.dp),
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 14.sp),
)
}
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(horizontalAlignment = Alignment.Start) {
Text(
stringResource(R.string.edit_verb),
color = MaterialTheme.colors.primary,
modifier = Modifier
.clickable(onClick = editOn)
)
}
Column(horizontalAlignment = Alignment.End) {
howToButton()
}
}
}
}
}
}
}
@Composable
private fun howToButton() {
val uriHandler = LocalUriHandler.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { uriHandler.openUri("https://github.com/simplex-chat/simplexmq#using-smp-server-and-smp-agent") }
) {
Text(stringResource(R.string.how_to), color = MaterialTheme.colors.primary)
Icon(
Icons.Outlined.OpenInNew, stringResource(R.string.how_to), tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(horizontal = 5.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun PreviewSMPServersLayoutDefaultServers() {
SimpleXTheme {
SMPServersLayout(
isUserSMPServers = false,
editSMPServers = true,
userSMPServersStr = remember { mutableStateOf("") },
isUserSMPServersOnOff = {},
cancelEdit = {},
saveSMPServers = {},
editOn = {},
)
}
}
@Preview(showBackground = true)
@Composable
fun PreviewSMPServersLayoutUserServersEditOn() {
SimpleXTheme {
SMPServersLayout(
isUserSMPServers = true,
editSMPServers = true,
userSMPServersStr = remember { mutableStateOf("smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im\nsmp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im") },
isUserSMPServersOnOff = {},
cancelEdit = {},
saveSMPServers = {},
editOn = {},
)
}
}
@Preview(showBackground = true)
@Composable
fun PreviewSMPServersLayoutUserServersEditOff() {
SimpleXTheme {
SMPServersLayout(
isUserSMPServers = true,
editSMPServers = false,
userSMPServersStr = remember { mutableStateOf("smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im\nsmp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im") },
isUserSMPServersOnOff = {},
cancelEdit = {},
saveSMPServers = {},
editOn = {},
)
}
}
@@ -0,0 +1,335 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.launch
@Composable
fun SMPServersView(m: ChatModel) {
var servers by remember {
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
}
val testing = rememberSaveable { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember {
derivedStateOf {
servers.isEmpty() ||
servers == m.userSMPServers.value ||
testing.value ||
!servers.all { srv ->
val address = parseServerAddress(srv.server)
address != null && uniqueAddress(srv, address, servers)
} ||
allServersDisabled.value
}
}
fun showServer(server: ServerCfg) {
ModalManager.shared.showModalCloseable(true) { close ->
var old by remember { mutableStateOf(server) }
val index = servers.indexOf(old)
SMPServerView(
m,
old,
onUpdate = { updated ->
val newServers = ArrayList(servers)
newServers.removeAt(index)
newServers.add(index, updated)
old = updated
servers = newServers
m.userSMPServersUnsaved.value = servers
},
onDelete = {
val newServers = ArrayList(servers)
newServers.removeAt(index)
servers = newServers
m.userSMPServersUnsaved.value = servers
close()
})
}
}
val scope = rememberCoroutineScope()
SMPServersLayout(
testing = testing.value,
servers = servers,
serversUnchanged = serversUnchanged.value,
saveDisabled = saveDisabled.value,
allServersDisabled = allServersDisabled.value,
m.currentUser.value,
addServer = {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.smp_servers_add),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
servers = servers + ServerCfg.empty
// No saving until something will be changed on the next screen to prevent blank servers on the list
showServer(servers.last())
}) {
Text(stringResource(R.string.smp_servers_enter_manually))
}
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.shared.showModalCloseable { close ->
ScanSMPServer {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
}
}
}
) {
Text(stringResource(R.string.smp_servers_scan_qr))
}
val hasAllPresets = hasAllPresets(servers, m)
if (!hasAllPresets) {
SectionItemView({
AlertManager.shared.hideAlert()
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
}) {
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
}
}
}
}
)
},
testServers = {
scope.launch {
testServers(testing, servers, m) {
servers = it
m.userSMPServersUnsaved.value = servers
}
}
},
resetServers = {
servers = m.userSMPServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveSMPServers(servers, m)
},
showServer = ::showServer,
)
if (testing.value) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
}
}
}
@Composable
private fun SMPServersLayout(
testing: Boolean,
servers: List<ServerCfg>,
serversUnchanged: Boolean,
saveDisabled: Boolean,
allServersDisabled: Boolean,
currentUser: User?,
addServer: () -> Unit,
testServers: () -> Unit,
resetServers: () -> Unit,
saveSMPServers: () -> Unit,
showServer: (ServerCfg) -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(R.string.your_SMP_servers))
SectionView(stringResource(R.string.smp_servers).uppercase()) {
for (srv in servers) {
SectionItemView({ showServer(srv) }, disabled = testing) {
SmpServerView(srv, servers, testing)
}
SectionDivider()
}
SettingsActionItem(
Icons.Outlined.Add,
stringResource(R.string.smp_servers_add),
addServer,
disabled = testing,
textColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary,
iconColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary
)
}
SectionTextFooter(
remember(currentUser?.displayName) {
buildAnnotatedString {
append(generalGetString(R.string.smp_servers_per_user) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(currentUser?.displayName ?: "")
}
append(".")
}
}
)
SectionSpacer()
SectionView {
SectionItemView(resetServers, disabled = serversUnchanged) {
Text(stringResource(R.string.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else HighOrLowlight)
}
SectionDivider()
val testServersDisabled = testing || allServersDisabled
SectionItemView(testServers, disabled = testServersDisabled) {
Text(stringResource(R.string.smp_servers_test_servers), color = if (!testServersDisabled) MaterialTheme.colors.onBackground else HighOrLowlight)
}
SectionDivider()
SectionItemView(saveSMPServers, disabled = saveDisabled) {
Text(stringResource(R.string.smp_servers_save), color = if (!saveDisabled) MaterialTheme.colors.onBackground else HighOrLowlight)
}
}
SectionSpacer()
SectionView {
HowToButton()
}
}
}
@Composable
private fun SmpServerView(srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
val address = parseServerAddress(srv.server)
when {
address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer()
!srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight)
else -> ShowTestStatus(srv)
}
Spacer(Modifier.padding(horizontal = 4.dp))
val text = address?.hostnames?.firstOrNull() ?: srv.server
if (srv.enabled) {
Text(text, color = if (disabled) HighOrLowlight else MaterialTheme.colors.onBackground, maxLines = 1)
} else {
Text(text, maxLines = 1, color = HighOrLowlight)
}
}
@Composable
private fun HowToButton() {
val uriHandler = LocalUriHandler.current
SettingsActionItem(
Icons.Outlined.OpenInNew,
stringResource(R.string.how_to_use_your_servers),
{ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md") },
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary
)
}
@Composable
fun InvalidServer() {
Icon(Icons.Outlined.ErrorOutline, null, tint = MaterialTheme.colors.error)
}
private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<ServerCfg>): Boolean = servers.all { srv ->
address.hostnames.all { host ->
srv.id == s.id || !srv.server.contains(host)
}
}
private fun hasAllPresets(servers: List<ServerCfg>, m: ChatModel): Boolean =
m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true
private fun addAllPresets(servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
val toAdd = ArrayList<ServerCfg>()
for (srv in m.presetSMPServers.value ?: emptyList()) {
if (!hasPreset(srv, servers)) {
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
}
}
return toAdd
}
private fun hasPreset(srv: String, servers: List<ServerCfg>): Boolean =
servers.any { it.server == srv }
private suspend fun testServers(testing: MutableState<Boolean>, servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit) {
val resetStatus = resetTestStatus(servers)
onUpdated(resetStatus)
testing.value = true
val fs = runServersTest(resetStatus, m) { onUpdated(it) }
testing.value = false
if (fs.isNotEmpty()) {
val msg = fs.map { it.key + ": " + it.value.localizedDescription }.joinToString("\n")
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.smp_servers_test_failed),
text = generalGetString(R.string.smp_servers_test_some_failed) + "\n" + msg
)
}
}
private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
val copy = ArrayList(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
copy.removeAt(index)
copy.add(index, server.copy(tested = null))
}
}
return copy
}
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, SMPTestFailure> {
val fs: MutableMap<String, SMPTestFailure> = mutableMapOf()
val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
val (updatedServer, f) = testServerConnection(server, m)
updatedServers.removeAt(index)
updatedServers.add(index, updatedServer)
// toList() is important. Otherwise, Compose will not redraw the screen after first update
onUpdated(updatedServers.toList())
if (f != null) {
fs[serverHostname(updatedServer.server)] = f
}
}
}
return fs
}
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel) {
withApi {
if (m.controller.setUserSMPServers(servers)) {
m.userSMPServers.value = servers
m.userSMPServersUnsaved.value = null
}
}
}
@@ -0,0 +1,55 @@
package chat.simplex.app.views.usersettings
import android.Manifest
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.app.model.ServerCfg
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanSMPServer(onNext: (ServerCfg) -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ScanSMPServerLayout(onNext)
}
@Composable
private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) {
Column(
Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.smp_servers_scan_qr), false)
Box(
Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1F)
.padding(bottom = 12.dp)
) {
QRCodeScanner { text ->
val res = parseServerAddress(text)
if (res != null) {
onNext(ServerCfg(text, false, null, true))
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.smp_servers_invalid_address),
text = generalGetString(R.string.smp_servers_check_address)
)
}
}
}
}
}
@@ -4,6 +4,7 @@ import SectionDivider
import SectionItemView
import SectionSpacer
import SectionView
import android.content.Context
import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -16,14 +17,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -34,6 +35,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.CreateLinkTab
import chat.simplex.app.views.newchat.CreateLinkView
import chat.simplex.app.views.onboarding.SimpleXInfo
import chat.simplex.app.views.onboarding.WhatsNewView
@Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
@@ -43,6 +45,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
MaintainIncognitoState(chatModel)
if (user != null) {
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state }
val context = LocalContext.current
SettingsLayout(
profile = user.profile,
stopped,
@@ -55,8 +59,43 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } },
// showVideoChatPrototype = { ModalManager.shared.showCustomModal { close -> CallViewDebug(close) } },
showVersion = {
withApi {
val info = chatModel.controller.apiGetVersion()
if (info != null) {
ModalManager.shared.showModal { VersionInfoView(info) }
}
}
},
withAuth = { block ->
if (!requireAuth.value) {
block()
} else {
ModalManager.shared.showModalCloseable { close ->
val onFinishAuth = { success: Boolean ->
if (success) {
close()
block()
}
}
LaunchedEffect(Unit) {
runAuth(context, onFinishAuth)
}
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(R.string.auth_unlock),
icon = Icons.Outlined.Lock,
click = {
runAuth(context, onFinishAuth)
}
)
}
}
}
},
)
}
}
@@ -64,31 +103,21 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
val simplexTeamUri =
"simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
// TODO pass close
//fun showSectionedModal(chatModel: ChatModel, modalView: (@Composable (ChatModel) -> Unit)) {
// ModalManager.shared.showCustomModal { close ->
// ModalView(close = close, modifier = Modifier,
// background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) {
// modalView(chatModel)
// }
// }
//}
@Composable
fun SettingsLayout(
profile: LocalProfile,
stopped: Boolean,
encrypted: Boolean,
incognito: MutableState<Boolean>,
incognitoPref: Preference<Boolean>,
developerTools: Preference<Boolean>,
incognitoPref: SharedPreference<Boolean>,
developerTools: SharedPreference<Boolean>,
userDisplayName: String,
setPerformLA: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showTerminal: () -> Unit,
// showVideoChatPrototype: () -> Unit
showVersion: () -> Unit,
withAuth: (block: () -> Unit) -> Unit
) {
val uriHandler = LocalUriHandler.current
Surface(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
@@ -112,33 +141,37 @@ fun SettingsLayout(
ProfilePreview(profile, stopped = stopped)
}
SectionDivider()
SettingsActionItem(Icons.Outlined.ManageAccounts, stringResource(R.string.your_chat_profiles), { withAuth { showSettingsModal { UserProfilesView(it) }() } }, disabled = stopped)
SectionDivider()
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { showModal { IncognitoView() }() }
SectionDivider()
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped)
SectionDivider()
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
ChatPreferencesItem(showCustomModal, stopped = stopped)
}
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_settings)) {
SettingsActionItem(Icons.Outlined.Bolt, stringResource(R.string.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView() }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped)
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
}
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_help)) {
SettingsActionItem(Icons.Outlined.HelpOutline, stringResource(R.string.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
SettingsActionItem(Icons.Outlined.Add, stringResource(R.string.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.TextFormat, stringResource(R.string.markdown_in_messages), showModal { MarkdownHelpView() })
SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
SectionDivider()
SettingsActionItem(Icons.Outlined.Tag, stringResource(R.string.chat_with_the_founder), { uriHandler.openUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
SectionDivider()
@@ -160,14 +193,14 @@ fun SettingsLayout(
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
SectionDivider()
if (devTools.value) {
ChatConsoleItem(showTerminal)
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
SectionDivider()
InstallTerminalAppItem(uriHandler)
SectionDivider()
}
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) })
// SectionDivider()
AppVersionItem()
AppVersionItem(showVersion)
}
}
}
@@ -175,7 +208,7 @@ fun SettingsLayout(
@Composable
fun SettingsIncognitoActionItem(
incognitoPref: Preference<Boolean>,
incognitoPref: SharedPreference<Boolean>,
incognito: MutableState<Boolean>,
stopped: Boolean,
onClickInfo: () -> Unit,
@@ -237,6 +270,21 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) {
SettingsActionItem(
Icons.Outlined.ToggleOn,
stringResource(R.string.chat_preferences),
click = if (stopped) null else ({
withApi {
showCustomModal { m, close ->
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
}()
}
}),
disabled = stopped
)
}
@Composable fun ChatLockItem(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
SectionItemView() {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -328,8 +376,8 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable private fun AppVersionItem() {
SectionItemView() {
@Composable private fun AppVersionItem(showVersion: () -> Unit) {
SectionItemView(showVersion) {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
}
@@ -365,12 +413,18 @@ fun SettingsActionItem(icon: ImageVector, text: String, click: (() -> Unit)? = n
}
@Composable
fun SettingsPreferenceItem(icon: ImageVector, text: String, pref: Preference<Boolean>, prefState: MutableState<Boolean>? = null) {
SectionItemView() {
fun SettingsPreferenceItem(
icon: ImageVector,
text: String,
pref: SharedPreference<Boolean>,
prefState: MutableState<Boolean>? = null,
onChange: ((Boolean) -> Unit)? = null,
) {
SectionItemView {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(icon, text, tint = HighOrLowlight)
Spacer(Modifier.padding(horizontal = 4.dp))
SharedPreferenceToggle(text, pref, prefState)
SharedPreferenceToggle(text, pref, prefState, onChange)
}
}
}
@@ -382,10 +436,10 @@ fun SettingsPreferenceItemWithInfo(
text: String,
stopped: Boolean,
onClickInfo: () -> Unit,
pref: Preference<Boolean>,
pref: SharedPreference<Boolean>,
prefState: MutableState<Boolean>? = null
) {
SectionItemView(onClickInfo) {
SectionItemView(if (stopped) null else onClickInfo) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(icon, text, tint = if (stopped) HighOrLowlight else iconTint)
Spacer(Modifier.padding(horizontal = 4.dp))
@@ -395,20 +449,42 @@ fun SettingsPreferenceItemWithInfo(
}
@Composable
fun PreferenceToggleWithIcon(
fun PreferenceToggle(
text: String,
icon: ImageVector,
iconColor: Color = HighOrLowlight,
checked: Boolean,
onChange: (Boolean) -> Unit = {},
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Icon(
icon,
null,
tint = iconColor
Text(text)
Spacer(Modifier.fillMaxWidth().weight(1f))
Switch(
checked = checked,
onCheckedChange = onChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
)
)
Spacer(Modifier.padding(horizontal = 4.dp))
}
}
@Composable
fun PreferenceToggleWithIcon(
text: String,
icon: ImageVector? = null,
iconColor: Color? = HighOrLowlight,
checked: Boolean,
onChange: (Boolean) -> Unit = {},
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
if (icon != null) {
Icon(
icon,
null,
tint = iconColor ?: HighOrLowlight
)
Spacer(Modifier.padding(horizontal = 4.dp))
}
Text(text)
Spacer(Modifier.fillMaxWidth().weight(1f))
Switch(
@@ -424,6 +500,17 @@ fun PreferenceToggleWithIcon(
}
}
private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
authenticate(
generalGetString(R.string.auth_open_chat_console),
generalGetString(R.string.auth_log_in_using_credential),
context as FragmentActivity,
completed = { laResult ->
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
}
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -438,15 +525,15 @@ fun PreviewSettingsLayout() {
stopped = false,
encrypted = false,
incognito = remember { mutableStateOf(false) },
incognitoPref = Preference({ false }, {}),
developerTools = Preference({ false }, {}),
incognitoPref = SharedPreference({ false }, {}),
developerTools = SharedPreference({ false }, {}),
userDisplayName = "Alice",
setPerformLA = {},
showModal = { {} },
showSettingsModal = { {} },
showCustomModal = { {}},
showTerminal = {},
// showVideoChatPrototype = {}
showCustomModal = { {} },
showVersion = {},
withAuth = {},
)
}
}
@@ -1,44 +0,0 @@
package chat.simplex.app.views.usersettings
import SectionViewSelectable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun ThemeSelectorView() {
val darkTheme = isSystemInDarkTheme()
val allThemes by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { ValueTitleDesc(it.second, it.third, "") }) }
ThemeSelectorLayout(
allThemes,
onSelectTheme = {
ThemeManager.applyTheme(it.name, darkTheme)
},
)
}
@Composable
private fun ThemeSelectorLayout(
allThemes: List<ValueTitleDesc<DefaultTheme>>,
onSelectTheme: (DefaultTheme) -> Unit,
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.settings_section_title_themes).lowercase().capitalize(Locale.current))
val currentTheme by CurrentColors.collectAsState()
val state = remember { derivedStateOf { currentTheme.second } }
SectionViewSelectable(null, state, allThemes, onSelectTheme)
}
}
@@ -44,12 +44,9 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
close,
saveProfile = { displayName, fullName, image ->
withApi {
val p = Profile(displayName, fullName, image)
val newProfile = chatModel.controller.apiUpdateProfile(p)
val newProfile = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image))
if (newProfile != null) {
chatModel.currentUser.value?.profile?.profileId?.let {
chatModel.updateUserProfile(newProfile.toLocalProfile(it))
}
chatModel.updateCurrentUser(newProfile)
profile = newProfile
}
editProfile.value = false
@@ -97,7 +94,7 @@ fun UserProfileLayout(
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
AppBarTitle(stringResource(R.string.your_chat_profile), false)
AppBarTitle(stringResource(R.string.your_current_profile), false)
Text(
stringResource(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it),
Modifier.padding(bottom = 24.dp),
@@ -0,0 +1,141 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.chatlist.UserProfilePickerItem
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.CreateProfile
@Composable
fun UserProfilesView(m: ChatModel) {
val users by remember { derivedStateOf { m.users.map { it.user } } }
UserProfilesView(
users = users,
addUser = {
ModalManager.shared.showModalCloseable { close ->
CreateProfile(m, close)
}
},
activateUser = { user ->
withBGApi {
m.controller.changeActiveUser(user.userId)
}
},
removeUser = { user ->
val text = buildAnnotatedString {
append(generalGetString(R.string.users_delete_all_chats_deleted) + "\n\n" + generalGetString(R.string.users_delete_profile_for) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(user.displayName)
}
append(":")
}
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.users_delete_question),
text = text,
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, true)
}) {
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, false)
}
) {
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
}
}
}
)
}
)
}
@Composable
private fun UserProfilesView(
users: List<User>,
addUser: () -> Unit,
activateUser: (User) -> Unit,
removeUser: (User) -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(R.string.your_chat_profiles))
SectionView {
for (user in users) {
UserView(user, users, activateUser, removeUser)
SectionDivider()
}
SectionItemView(addUser, minHeight = 68.dp) {
Icon(Icons.Outlined.Add, stringResource(R.string.users_add), tint = MaterialTheme.colors.primary)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.users_add), color = MaterialTheme.colors.primary)
}
}
SectionTextFooter(stringResource(R.string.your_chat_profiles_stored_locally))
}
}
@Composable
private fun UserView(user: User, users: List<User>, activateUser: (User) -> Unit, removeUser: (User) -> Unit) {
var showDropdownMenu by remember { mutableStateOf(false) }
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
activateUser(user)
}
Box(Modifier.padding(horizontal = 16.dp)) {
DropdownMenu(
expanded = showDropdownMenu,
onDismissRequest = { showDropdownMenu = false },
Modifier.width(220.dp)
) {
ItemAction(stringResource(R.string.delete_verb), Icons.Outlined.Delete, color = Color.Red, onClick = {
removeUser(user)
showDropdownMenu = false
}
)
}
}
}
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean) {
if (users.size < 2) return
withBGApi {
try {
if (user.activeUser) {
val newActive = users.first { !it.activeUser }
m.controller.changeActiveUser_(newActive.userId)
}
m.controller.apiDeleteUser(user.userId, delSMPQueues)
m.users.removeAll { it.user.userId == user.userId }
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
}
}
}
@@ -0,0 +1,30 @@
package chat.simplex.app.views.usersettings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.BuildConfig
import chat.simplex.app.R
import chat.simplex.app.model.CoreVersionInfo
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.AppBarTitle
@Composable
fun VersionInfoView(info: CoreVersionInfo) {
Column(
Modifier.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
AppBarTitle(stringResource(R.string.app_version_title), false)
Text(String.format(stringResource(R.string.app_version_name), BuildConfig.VERSION_NAME))
Text(String.format(stringResource(R.string.app_version_code), BuildConfig.VERSION_CODE))
Text(String.format(stringResource(R.string.core_version), info.version))
Text(String.format(stringResource(R.string.core_build_timestamp), info.buildTimestamp))
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
}
}
@@ -0,0 +1,953 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="allow_voice_messages_only_if">Povolte hlasov├й zpr├бvy, pouze pokud je v├б┼б kontakt povol├н.</string>
<string name="allow_to_send_disappearing">Povolit odes├нl├бn├н mizej├нc├нch zpr├бv.</string>
<string name="allow_to_send_voice">Povolit odes├нl├бn├н hlasov├╜ch zpr├бv.</string>
<string name="v4_2_group_links_desc">Spr├бvci mohou vytv├б┼Щet odkazy pro p┼Щipojen├н ke skupin├бm.</string>
<string name="accept_contact_button">P┼Щijmout</string>
<string name="smp_servers_preset_add">P┼Щidejte p┼Щednastaven├й servery</string>
<string name="network_settings">Pokro─Нil├б nastaven├н s├нt─Ы</string>
<string name="accept">P┼Щijmout</string>
<string name="smp_servers_add">P┼Щidat serverтАж</string>
<string name="network_enable_socks_info">P┼Щistupovat k server┼пm p┼Щes SOCKS proxy na portu 9050\? P┼Щed povolen├нm t├йto mo┼╛nosti mus├н b├╜t spu┼бt─Ыna proxy.</string>
<string name="accept_feature">P┼Щijmout</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Umo┼╛n─Ыte sv├╜m kontakt┼пm odes├нlat mizej├нc├н zpr├бvy.</string>
<string name="about_simplex_chat">O <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="smp_servers_add_to_another_device">P┼Щidat do jin├йho za┼Щ├нzen├н</string>
<string name="accept_requests">P┼Щij├нmat ┼╛├бdosti</string>
<string name="allow_verb">Povolit</string>
<string name="allow_voice_messages_question">Povolit hlasov├й zpr├бvy\?</string>
<string name="about_simplex">O SimpleX</string>
<string name="a_plus_b">a + b</string>
<string name="accept_call_on_lock_screen">P┼Щijmout</string>
<string name="chat_item_ttl_day">1 den</string>
<string name="group_member_role_admin">spr├бvce</string>
<string name="users_add">P┼Щidat profil</string>
<string name="users_delete_all_chats_deleted">V┼бechny chaty a zpr├бvy budou smaz├бny тАУ tuto akci nelze vr├бtit zp─Ыt!</string>
<string name="allow_disappearing_messages_only_if">Povolte mizej├нc├н zpr├бvy, pouze pokud to v├б┼б kontakt povol├н.</string>
<string name="v4_3_improved_server_configuration_desc">P┼Щidejte servery skenov├бn├нm QR k├│d┼п.</string>
<string name="chat_item_ttl_month">1 m─Ыs├нc</string>
<string name="chat_item_ttl_week">1 t├╜den</string>
<string name="callstatus_accepted">p┼Щijat├╜ hovor</string>
<string name="accept_contact_incognito_button">P┼Щijmout inkognito</string>
<string name="accept_connection_request__question">P┼Щijmout ┼╛├бdost o p┼Щipojen├н\?</string>
<string name="all_group_members_will_remain_connected">V┼бichni ─Нlenov├й skupiny z┼пstanou p┼Щipojeni.</string>
<string name="allow_irreversible_message_deletion_only_if">Povolte nevratn├й smaz├бn├н zpr├бvy pouze v p┼Щ├нpad─Ы, ┼╛e v├бm to v├б┼б kontakt dovol├н.</string>
<string name="allow_direct_messages">Povolit odes├нl├бn├н p┼Щ├нm├╜ch zpr├бv ─Нlen┼пm.</string>
<string name="allow_to_delete_messages">Povolit nevratn├й smaz├бn├н odeslan├╜ch zpr├бv.</string>
<string name="clear_chat_warning">V┼бechny zpr├бvy budou smaz├бny тАУ tuto akci nelze vr├бtit zp─Ыt! Zpr├бvy budou smaz├бny POUZE pro v├бs.</string>
<string name="allow_your_contacts_irreversibly_delete">Umo┼╛n─Ыte sv├╜m kontakt┼пm nevratn─Ы odstranit odeslan├й zpr├бvy.</string>
<string name="allow_your_contacts_to_send_voice_messages">Povolte sv├╜m kontakt┼пm odes├нl├бn├н hlasov├╜ch zpr├бv.</string>
<string name="button_create_group_link">Vytvo┼Щit odkaz</string>
<string name="delete_link_question">Smazat odkaz\?</string>
<string name="button_send_direct_message">Odeslat p┼Щ├нmou zpr├бvu</string>
<string name="member_info_section_title_member">─МLEN</string>
<string name="change_member_role_question">Zm─Ыnit roli ve skupin─Ы\?</string>
<string name="info_row_connection">P┼Щipojen├н</string>
<string name="conn_level_desc_indirect">nep┼Щ├нm├й (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="conn_stats_section_title_servers">SERVERY</string>
<string name="receiving_via">P┼Щ├нjem prost┼Щednictv├нm</string>
<string name="create_secret_group_title">Vytvo┼Щen├н tajn├й skupiny</string>
<string name="group_display_name_field">Zobrazen├н n├бzvu skupiny:</string>
<string name="group_full_name_field">├Ъpln├╜ n├бzev skupiny:</string>
<string name="group_main_profile_sent">V├б┼б profil v chatu bude zasl├бn ─Нlen┼пm skupiny</string>
<string name="group_profile_is_stored_on_members_devices">Profil skupiny je ulo┼╛en v za┼Щ├нzen├нch ─Нlen┼п, nikoli na serverech.</string>
<string name="network_options_save">Ulo┼╛it</string>
<string name="update_network_settings_question">Aktualizovat nastaven├н s├нt─Ы\?</string>
<string name="incognito">Inkognito</string>
<string name="incognito_random_profile">V├б┼б n├бhodn├╜ profil</string>
<string name="incognito_random_profile_description">Va┼бemu kontaktu bude zasl├бn n├бhodn├╜ profil</string>
<string name="save_color">Ulo┼╛it barvu</string>
<string name="reset_color">Obnoven├н barev</string>
<string name="color_primary">Akcent</string>
<string name="chat_preferences_you_allow">Povol├нte</string>
<string name="chat_preferences_default">v├╜choz├н (%s)</string>
<string name="chat_preferences_yes">ano</string>
<string name="chat_preferences_no">ne</string>
<string name="chat_preferences_always">v┼╛dy</string>
<string name="set_group_preferences">Nastaven├н skupinov├╜ch p┼Щedvoleb</string>
<string name="your_preferences">Va┼бe preference</string>
<string name="timed_messages">Zmizen├н zpr├бv</string>
<string name="feature_enabled_for_contact">povoleno pro kontakt</string>
<string name="feature_received_prohibited">p┼Щijat├й, zak├бzan├й</string>
<string name="both_you_and_your_contact_can_send_disappearing">Vy i v├б┼б kontakt m┼п┼╛ete pos├нlat mizej├нc├н zpr├бvy.</string>
<string name="only_your_contact_can_send_disappearing">Zmizel├й zpr├бvy m┼п┼╛e odes├нlat pouze v├б┼б kontakt.</string>
<string name="only_you_can_delete_messages">Nevratn─Ы mazat zpr├бvy m┼п┼╛ete pouze vy (v├б┼б kontakt je m┼п┼╛e ozna─Нit ke smaz├бn├н).</string>
<string name="message_deletion_prohibited">Nevratn├й maz├бn├н zpr├бv je v tomto chatu zak├бz├бno.</string>
<string name="prohibit_direct_messages">Zak├бzat odes├нl├бn├н p┼Щ├нm├╜ch zpr├бv ─Нlen┼пm.</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_s">%ds</string>
<string name="ttl_min">%d min</string>
<string name="ttl_hour">\"%d hodina</string>
<string name="feature_offered_item_with_param">offered %s: %2s</string>
<string name="v4_2_group_links">Odkazy na skupiny</string>
<string name="v4_3_voice_messages">Hlasov├й zpr├бvy</string>
<string name="v4_3_irreversible_message_deletion_desc">Va┼бe kontakty mohou povolit ├║pln├й vymaz├бn├н zpr├бv.</string>
<string name="v4_4_disappearing_messages">Zmizen├н zpr├бv</string>
<string name="v4_4_verify_connection_security_desc">Porovnejte bezpe─Нnostn├н k├│dy se sv├╜mi kontakty.</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="thousand_abbreviation">k</string>
<string name="connect_via_contact_link">P┼Щipojit se p┼Щes kontaktn├н odkaz\?</string>
<string name="connect_via_invitation_link">P┼Щipojit se p┼Щes pozv├бnku\?</string>
<string name="connect_via_group_link">P┼Щipojit se p┼Щes odkaz skupiny\?</string>
<string name="profile_will_be_sent_to_contact_sending_link">V├б┼б profil bude odesl├бn kontaktu, od kter├йho jste obdr┼╛eli tento odkaz.</string>
<string name="server_connected">p┼Щipojeno</string>
<string name="server_error">chyba</string>
<string name="server_connecting">p┼Щipojen├н</string>
<string name="trying_to_connect_to_server_to_receive_messages">Pokus o p┼Щipojen├н k serveru pou┼╛├нvan├йmu pro p┼Щ├нjem zpr├бv od tohoto kontaktu.</string>
<string name="deleted_description">Smaz├бno</string>
<string name="invalid_chat">neplatn├╜ chat</string>
<string name="invalid_data">neplatn├й ├║daje</string>
<string name="connection_local_display_name">spojen├н <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="display_name_connection_established">spojen├н nav├бz├бno</string>
<string name="display_name_invited_to_connect">pozv├бnka k p┼Щipojen├н</string>
<string name="display_name_connecting">p┼Щipojen├нтАж</string>
<string name="description_you_shared_one_time_link">jste sd├нleli jednor├бzov├й spojen├н</string>
<string name="description_you_shared_one_time_link_incognito">sd├нleli jste jednor├бzov├╜ odkaz inkognito</string>
<string name="description_via_group_link">prost┼Щednictv├нm skupinov├йho odkazu</string>
<string name="description_via_contact_address_link">prost┼Щednictv├нm odkazu na kontaktn├н adresu</string>
<string name="description_via_contact_address_link_incognito">inkognito p┼Щes odkaz na kontaktn├н adresu</string>
<string name="description_via_one_time_link">prost┼Щednictv├нm jednor├бzov├йho odkazu</string>
<string name="description_via_one_time_link_incognito">inkognito p┼Щes jednor├бzov├╜ odkaz</string>
<string name="simplex_link_contact">SimpleX kontaktn├н adresa</string>
<string name="simplex_link_invitation">Jednor├бzov├б pozv├бnka SimpleX</string>
<string name="simplex_link_group">Skupinov├╜ odkaz SimpleX</string>
<string name="simplex_link_connection">prost┼Щednictv├нm <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode">Odkazy na SimpleX</string>
<string name="simplex_link_mode_description">Popis</string>
<string name="simplex_link_mode_full">├Ъpln├╜ odkaz</string>
<string name="simplex_link_mode_browser">Prost┼Щednictv├нm prohl├н┼╛e─Нe</string>
<string name="simplex_link_mode_browser_warning">Otev┼Щen├н odkazu v prohl├н┼╛e─Нi m┼п┼╛e sn├н┼╛it soukrom├н a bezpe─Нnost p┼Щipojen├н. Ned┼пv─Ыryhodn├й odkazy SimpleX budou ─Нerven├й.</string>
<string name="error_saving_smp_servers">Chyba p┼Щi ukl├бd├бn├н server┼п SMP</string>
<string name="error_setting_network_config">Chyba p┼Щi aktualizaci konfigurace s├нt─Ы</string>
<string name="failed_to_parse_chat_title">Nepoda┼Щilo se na─Н├нst chat</string>
<string name="failed_to_parse_chats_title">Nepoda┼Щilo se na─Н├нst chaty</string>
<string name="contact_developers">Aktualizujte aplikaci a kontaktujte v├╜voj├б┼Щe.</string>
<string name="connection_timeout">─Мasov├╜ limit p┼Щipojen├н</string>
<string name="connection_error">Chyba p┼Щipojen├н</string>
<string name="network_error_desc">Zkontrolujte pros├нm sv├й s├н┼еov├й p┼Щipojen├н pomoc├н <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> a zkuste to znovu.</string>
<string name="error_sending_message">Chyba p┼Щi odes├нl├бn├н zpr├бvy</string>
<string name="error_adding_members">Chyba p┼Щi p┼Щid├бv├бn├н prutu(┼п)</string>
<string name="contact_already_exists">Kontakt ji┼╛ existuje</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jste ji┼╛ p┼Щipojeni k <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="invalid_connection_link">Neplatn├╜ odkaz na spojen├н</string>
<string name="error_accepting_contact_request">Chyba p┼Щ├нjmu po┼╛adavku od kontaktu</string>
<string name="error_changing_address">Chuba zm─Ыny adresy</string>
<string name="settings_notifications_mode_title">Slu┼╛ba oznamov├бn├н</string>
<string name="notifications_mode_service_desc">Slu┼╛ba na pozad├н je spu┼бt─Ыna v┼╛dy - ozn├бmen├н se zobraz├н, jakmile jsou zpr├бvy k dispozici.</string>
<string name="notification_preview_mode_message">Text zpr├бvy</string>
<string name="notification_preview_mode_contact">Jm├йno kontaktu</string>
<string name="notification_preview_mode_hidden">Skryt├й</string>
<string name="notification_preview_mode_message_desc">Zobrazit kontakt a zpr├бvu</string>
<string name="notification_contact_connected">P┼Щipojeno</string>
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
<string name="auth_log_in_using_credential">P┼Щihlaste se pomoc├н sv├йho pov─Ы┼Щen├н</string>
<string name="auth_enable_simplex_lock">Zapnut├н z├бmku SimpleX</string>
<string name="reply_verb">Odpov─Ыzte na</string>
<string name="share_verb">Sd├нlet</string>
<string name="copy_verb">Kop├нrovat</string>
<string name="icon_descr_received_msg_status_unread">nep┼Щe─Нteno</string>
<string name="personal_welcome">V├нtejte <xliff:g>%1$s</xliff:g>!</string>
<string name="welcome">V├нtejte!</string>
<string name="this_text_is_available_in_settings">Tento text je k dispozici v nastaven├н</string>
<string name="icon_descr_sent_msg_status_send_failed">odesl├бn├н se nezda┼Щilo</string>
<string name="share_file">Sd├нlet souborтАж</string>
<string name="attach">P┼Щipojit</string>
<string name="icon_descr_context">Kontextov├б ikona</string>
<string name="image_decoding_exception_desc">Obr├бzek nelze dek├│dovat. Zkuste pros├нm pou┼╛├нt jin├╜ obr├бzek nebo kontaktujte v├╜voj├б┼Щe.</string>
<string name="image_descr">Obr├бzek</string>
<string name="icon_descr_waiting_for_image">─Мek├бn├н na obr├бzek</string>
<string name="icon_descr_asked_to_receive">Po┼╛├бd├бno o p┼Щijet├н obr├бzku</string>
<string name="icon_descr_image_snd_complete">Obr├бzek odesl├бn</string>
<string name="waiting_for_image">─Мek├бme na obr├бzek</string>
<string name="image_will_be_received_when_contact_is_online">Obr├бzek bude p┼Щijat, a┼╛ bude v├б┼б kontakt online, vy─Нkejte pros├нm nebo se pod├нvejte pozd─Ыji!</string>
<string name="contact_sent_large_file">V├б┼б kontakt odeslal soubor, kter├╜ je v─Ыt┼б├н ne┼╛ aktu├бln─Ы podporovan├б maxim├бln├н velikost (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
<string name="maximum_supported_file_size">V sou─Нasn├й dob─Ы je maxim├бln├н podporovan├б velikost souboru <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="error_saving_file">Chyba p┼Щi ukl├бd├бn├н souboru</string>
<string name="voice_message">Hlasov├б zpr├бva</string>
<string name="voice_message_send_text">Hlasov├б zpr├бvaтАж</string>
<string name="icon_descr_server_status_connected">P┼Щipojeno</string>
<string name="icon_descr_server_status_disconnected">Odpojeno</string>
<string name="icon_descr_server_status_error">Chyba</string>
<string name="switch_receiving_address_desc">Tato funkce je experiment├бln├н! Bude fungovat pouze v p┼Щ├нpad─Ы, ┼╛e druh├╜ klient m├б nainstalovanou verzi 4.2. Po dokon─Нen├н zm─Ыny adresy by se m─Ыla v konverzaci zobrazit zpr├бva - zkontrolujte, zda m┼п┼╛ete od tohoto kontaktu (nebo ─Нlena skupiny) st├бle p┼Щij├нmat zpr├бvy.</string>
<string name="switch_receiving_address_question">P┼Щepnout p┼Щij├нmac├н adresu\?</string>
<string name="send_verb">Poslat</string>
<string name="you_need_to_allow_to_send_voice">Abyste mohli odes├нlat hlasov├й zpr├бvy, mus├нte je povolit sv├йmu kontaktu.</string>
<string name="icon_descr_cancel_live_message">Zru┼бit ┼╛ivou zpr├бvu</string>
<string name="back">Zp─Ыt</string>
<string name="cancel_verb">Zru┼бit</string>
<string name="reset_verb">Obnovit</string>
<string name="ok">OK</string>
<string name="no_details">bez podrobnost├н</string>
<string name="add_contact">Jednor├бzov├╜ zvac├н odkaz</string>
<string name="copied">Zkop├нrov├бno do schr├бnky</string>
<string name="add_contact_or_create_group">Za─Н├нt novou konverzaci</string>
<string name="create_group">Vytvo┼Щit tajnou skupinu</string>
<string name="to_share_with_your_contact">(sd├нlet s kontaktem)</string>
<string name="only_stored_on_members_devices">(ulo┼╛eno pouze ─Нleny skupiny)</string>
<string name="toast_permission_denied">Opr├бvn─Ыn├н zam├нtnuto!</string>
<string name="use_camera_button">Pou┼╛├нt fotoapar├бt</string>
<string name="from_gallery_button">Z Galerie</string>
<string name="choose_file">Vybrat soubor</string>
<string name="to_start_a_new_chat_help_header">Pro zah├бjen├н nov├й konverzace</string>
<string name="chat_help_tap_button">Klepn─Ыte na tla─Н├нtko</string>
<string name="above_then_preposition_continuation">nad, potom:</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>P┼Щidat nov├╜ kontakt</b>: vytvo┼Щit jednor├бzov├╜ k├│d QR.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Skenovat QR k├│d</b>: p┼Щipojen├н ke kontaktu, kter├╜ v├бm uk├б┼╛e QR k├│d.</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">ЁЯТ╗ desktop: scan displayed QR code from the app, via <b>Scan QR code</b>.</string>
<string name="clear_chat_question">Vy─Нistit konverzaci\?</string>
<string name="clear_verb">─Мist├╜</string>
<string name="mark_read">Ozna─Нit p┼Щe─Нten├н</string>
<string name="mark_unread">Ozna─Нit jako nep┼Щe─Нten├й</string>
<string name="mute_chat">Ztlumit</string>
<string name="unmute_chat">Zru┼бit ztlumen├н</string>
<string name="you_invited_your_contact">Pozvali jste sv┼пj kontakt</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Kontakt, se kter├╜m jste tento odkaz sd├нleli, se NEBUDE moci p┼Щipojit!</string>
<string name="connection_you_accepted_will_be_cancelled">P┼Щipojen├н, kter├й jste p┼Щijali, bude zru┼бeno!</string>
<string name="icon_descr_help">help</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> T├╜m</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> Adresa</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Ke skupin─Ы budete p┼Щipojeni, a┼╛ bude za┼Щ├нzen├н hostitele skupiny online, vy─Нkejte pros├нm nebo se pod├нvejte pozd─Ыji!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Budete p┼Щipojeni, jakmile bude va┼бe ┼╛├бdost o p┼Щipojen├н p┼Щijata, vy─Нkejte pros├нm nebo se pod├нvejte pozd─Ыji!</string>
<string name="connection_request_sent">Po┼╛adavek na p┼Щipojen├н byl odesl├бn!</string>
<string name="your_profile_will_be_sent">V├б┼б profil v chatu bude odesl├бn va┼бemu kontaktu</string>
<string name="create_one_time_link">Vytvo┼Щit jednor├бzovou pozv├бnku</string>
<string name="one_time_link">Vytvo┼Щit jednor├бzov├╜ zvac├н odkaz</string>
<string name="security_code">Bezpe─Нnostn├н k├│d</string>
<string name="is_verified">\"%s je ov─Ы┼Щeno</string>
<string name="chat_console">Konzola pro chat</string>
<string name="smp_servers">SMP servery</string>
<string name="smp_servers_preset_address">P┼Щednastaven├б adresa serveru</string>
<string name="smp_servers_test_failed">Test serveru se nezda┼Щil!</string>
<string name="smp_servers_test_some_failed">N─Ыkter├й servery nepro┼бly testem:</string>
<string name="smp_servers_scan_qr">Naskenujte QR k├│d serveru</string>
<string name="smp_servers_enter_manually">Zadejte server ru─Нn─Ы</string>
<string name="smp_servers_invalid_address">Neplatn├б adresa serveru!</string>
<string name="smp_servers_check_address">Zkontrolujte adresu serveru a zkuste to znovu.</string>
<string name="smp_servers_delete_server">Smazat server</string>
<string name="contribute">P┼Щisp─Ыjte na</string>
<string name="how_to">Jak na to</string>
<string name="how_to_use_your_servers">Jak pou┼╛├нvat servery</string>
<string name="your_ICE_servers">Va┼бe servery ICE</string>
<string name="configure_ICE_servers">Konfigurace server┼п ICE</string>
<string name="network_settings_title">Nastaven├н s├нt─Ы</string>
<string name="network_enable_socks">Pou┼╛├нt proxy server SOCKS\?</string>
<string name="network_disable_socks">Pou┼╛├нt p┼Щ├нm├й p┼Щipojen├н k internetu\?</string>
<string name="network_use_onion_hosts_no">Ne</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hostitel├й nebudou pou┼╛iti.</string>
<string name="network_session_mode_user">Profil chatu</string>
<string name="network_session_mode_entity">P┼Щipojen├н</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="create_address">Vytvo┼Щit adresu</string>
<string name="accept_automatically">Automaticky</string>
<string name="section_title_welcome_message">UV├НTAC├Н ZPR├БVA</string>
<string name="save_and_notify_group_members">Ulo┼╛it a upozornit ─Нleny skupiny</string>
<string name="exit_without_saving">Ukon─Нit bez ulo┼╛en├н</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platforma pro zas├нl├бn├н zpr├бv a aplikace chr├бn├нc├н va┼бe soukrom├н a bezpe─Нnost.</string>
<string name="create_profile">Vytvo┼Щen├н profilu</string>
<string name="profile_is_only_shared_with_your_contacts">Profil je sd├нlen pouze s va┼бimi kontakty.</string>
<string name="display_name_cannot_contain_whitespace">Zobrazovan├й jm├йno nesm├н obsahovat b├нl├й znaky.</string>
<string name="bold">tu─Нn─Ы</string>
<string name="callstatus_in_progress">prob├нhaj├нc├н hovor</string>
<string name="decentralized">Decentralizovan├й</string>
<string name="how_it_works">Jak to funguje</string>
<string name="how_simplex_works">Jak funguje <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Pouze klientsk├б za┼Щ├нzen├н ukl├бdaj├н u┼╛ivatelsk├й profily, kontakty, skupiny a zpr├бvy odes├нlan├й pomoc├н <b>2vrstv├йho end-to-end ┼бifrov├бn├н</b>.</string>
<string name="onboarding_notifications_mode_title">Soukrom├б ozn├бmen├н</string>
<string name="onboarding_notifications_mode_periodic">Pravideln├╜</string>
<string name="ignore">Ignorovat</string>
<string name="call_already_ended">Hovor ji┼╛ skon─Нil!</string>
<string name="icon_descr_video_call">videohovor</string>
<string name="icon_descr_audio_call">audio hovor</string>
<string name="settings_audio_video_calls">Audio a video hovory</string>
<string name="call_on_lock_screen">Hovory na uzam─Нen├й obrazovce:</string>
<string name="open_simplex_chat_to_accept_call">Otev┼Щete <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro p┼Щijet├н hovoru</string>
<string name="allow_accepting_calls_from_lock_screen">Povolte vol├бn├н ze zam─Нen├й obrazovky prost┼Щednictv├нm Nastaven├н.</string>
<string name="open_verb">Otev┼Щete str├бnku</string>
<string name="icon_descr_audio_on">Zvuk zapnut</string>
<string name="icon_descr_speaker_off">Reproduktor vypnut</string>
<string name="icon_descr_speaker_on">Zapnut├╜ reproduktor</string>
<string name="icon_descr_call_progress">Prob├нhaj├нc├н hovor</string>
<string name="auto_accept_images">Automatick├й p┼Щij├нm├бn├н obr├бzk┼п</string>
<string name="settings_section_title_settings">NASTAVEN├Н</string>
<string name="settings_section_title_help">N├БPOV─ЪDA</string>
<string name="settings_section_title_device">ZA┼Ш├НZEN├Н</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="settings_experimental_features">Experiment├бln├н funkce</string>
<string name="settings_section_title_socks">SOCKS PROXY</string>
<string name="settings_section_title_icon">IKONA APLIKACE</string>
<string name="settings_section_title_themes">T├ЙMATA</string>
<string name="settings_section_title_messages">ZPR├БVY</string>
<string name="settings_section_title_calls">VOL├БN├Н</string>
<string name="export_database">Export datab├бze</string>
<string name="import_database">Import datab├бze</string>
<string name="delete_database">Odstran─Ыn├н datab├бze</string>
<string name="error_exporting_chat_database">Chyba p┼Щi exportu datab├бze chatu</string>
<string name="import_database_confirmation">Import</string>
<string name="restart_the_app_to_use_imported_chat_database">Restartujte aplikaci, abyste mohli pou┼╛├нvat importovanou datab├бzi chatu.</string>
<string name="delete_chat_profile_question">Smazat profil chatu\?</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Tuto akci nelze vz├нt zp─Ыt - v├б┼б profil, kontakty, zpr├бvy a soubory budou nen├бvratn─Ы ztraceny.</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Restartujte aplikaci a vytvo┼Щte nov├╜ profil chatu.</string>
<string name="you_must_use_the_most_recent_version_of_database">Nejnov─Ыj┼б├н verzi datab├бze chatu mus├нte pou┼╛├нvat POUZE v jednom za┼Щ├нzen├н, jinak se m┼п┼╛e st├бt, ┼╛e p┼Щestanete p┼Щij├нmat zpr├бvy od n─Ыkter├╜ch kontakt┼п.</string>
<string name="stop_chat_to_enable_database_actions">Zastavte chat a povolte akce s datab├бz├н.</string>
<string name="files_and_media_section">Soubory a m├йdia</string>
<string name="delete_files_and_media_question">Smazat soubory a m├йdia\?</string>
<string name="delete_messages">Odstran─Ыn├н zpr├бv</string>
<string name="remove_passphrase_from_keychain">Odstranit p┼Щ├нstupovou fr├бzi z ├║lo┼╛i┼бt─Ы kl├н─Н┼п\?</string>
<string name="notifications_will_be_hidden">Ozn├бmen├н budou doru─Нov├бna pouze do doby, ne┼╛ se aplikace zastav├н!</string>
<string name="remove_passphrase">Odstranit</string>
<string name="update_database">Aktualizovat</string>
<string name="current_passphrase">Aktu├бln├н p┼Щ├нstupov├б fr├бzeтАж</string>
<string name="update_database_passphrase">Aktualizovat p┼Щ├нstupovou fr├бzi datab├бze</string>
<string name="enter_correct_current_passphrase">Zadejte pros├нm spr├бvnou aktu├бln├н p┼Щ├нstupovou fr├бzi.</string>
<string name="database_is_not_encrypted">V├б┼б datab├бze konverzace nen├н za┼бifrov├бna - nastavte p┼Щ├нstupovou fr├бzi pro jej├н ochranu.</string>
<string name="keychain_is_storing_securely">K bezpe─Нn├йmu ulo┼╛en├н heslov├й fr├бze slou┼╛├н ├║lo┼╛i┼бt─Ы kl├н─Н┼п Android - umo┼╛┼Иuje fungov├бn├н slu┼╛by ozn├бmen├н.</string>
<string name="impossible_to_recover_passphrase"><b>Upozorn─Ыn├н</b>: pokud p┼Щ├нstupovou fr├бzi ztrat├нte, NEBUDE mo┼╛n├й ji obnovit ani zm─Ыnit.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">Datab├бze bude za┼бifrov├бna a p┼Щ├нstupov├б fr├бze bude ulo┼╛ena v ├║lo┼╛i┼бti kl├н─Н┼п.</string>
<string name="store_passphrase_securely">Heslo ulo┼╛te bezpe─Нn─Ы, v p┼Щ├нpad─Ы jeho ztr├бty jej NEBUDE mo┼╛n├й zm─Ыnit.</string>
<string name="file_with_path">Soubor: %s</string>
<string name="database_passphrase_is_required">Pro otev┼Щen├н chatu je vy┼╛adov├бna p┼Щ├нstupov├б fr├бze datab├бze.</string>
<string name="unknown_error">Nezn├бm├б chyba</string>
<string name="open_chat">Otev┼Щete chat</string>
<string name="restore_database">Obnovte z├бlohu datab├бze</string>
<string name="restore_database_alert_desc">Po obnoven├н z├бlohy datab├бze zadejte p┼Щedchoz├н heslo. Tuto akci nelze vr├бtit zp─Ыt.</string>
<string name="chat_is_stopped_indication">Chat je zastaven</string>
<string name="chat_archive_header">Chat se archivuje</string>
<string name="delete_chat_archive_question">Smazat archiv chatu\?</string>
<string name="join_group_question">P┼Щipojit se ke skupin─Ы\?</string>
<string name="join_group_button">P┼Щipojte se na</string>
<string name="leave_group_button">Opustit</string>
<string name="icon_descr_add_members">Pozvat ─Нleny</string>
<string name="alert_title_no_group">Skupina nebyla nalezena!</string>
<string name="alert_title_cant_invite_contacts">Nelze pozvat kontakty!</string>
<string name="snd_group_event_changed_member_role">zm─Ыnili jste roli %s na %s</string>
<string name="snd_group_event_changed_role_for_yourself">zm─Ыnili jste svou roli na %s</string>
<string name="snd_group_event_member_deleted">odstranili jste <xliff:g id="profil ─Нlena" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_user_left">ode┼бli jste</string>
<string name="rcv_conn_event_switch_queue_phase_completed">zm─Ыnila se va┼бe adresa</string>
<string name="icon_descr_expand_role">Roz┼б├н┼Щit v├╜b─Ыr rol├н</string>
<string name="invite_prohibited">Nelze pozvat kontakt!</string>
<string name="failed_to_create_user_duplicate_desc">Ji┼╛ m├бte profil chatu se stejn├╜m zobrazovac├нm n├бzvem. Zvolte pros├нm jin├й jm├йno.</string>
<string name="smp_server_test_create_queue">Vytvo┼Щit frontu</string>
<string name="smp_server_test_secure_queue">Zabezpe─Нit frontu</string>
<string name="service_notifications">Okam┼╛it├б ozn├бmen├н!</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>V nastaven├н ji lze vypnout</b> - ozn├бmen├н se budou zobrazovat pokud aplikace b─Ы┼╛├н.</string>
<string name="turn_off_battery_optimization">Chcete-li ji pou┼╛├нvat, <b>vypn─Ыte optimalizaci baterie</b> pro <xliff:g id="appName">SimpleX</xliff:g> v dal┼б├нm dialogu. V opa─Нn├йm p┼Щ├нpad─Ы budou ozn├бmen├н vypnuta.</string>
<string name="periodic_notifications_desc">Aplikace pravideln─Ы na─Н├нt├б nov├й zpr├бvy - denn─Ы spot┼Щebuje n─Ыkolik procent baterie. Aplikace nepou┼╛├нv├б push ozn├бmen├н - data ze za┼Щ├нzen├н nejsou odes├нl├бna na servery.</string>
<string name="enter_passphrase_notification_title">Je vy┼╛adov├бna p┼Щ├нstupov├б fr├бze</string>
<string name="enter_passphrase_notification_desc">Chcete-li dost├бvat ozn├бmen├н, zadejte p┼Щ├нstupovou fr├бzi do datab├бze.</string>
<string name="database_initialization_error_title">Nelze inicializovat datab├бzi</string>
<string name="hide_notification">Skr├╜t</string>
<string name="ntf_channel_calls">Vol├бn├н SimpleX Chat</string>
<string name="notification_preview_new_message">nov├б zpr├бva</string>
<string name="notification_new_contact_request">┼╜├бdost o nov├╜ kontakt</string>
<string name="delete_message_cannot_be_undone_warning">Zpr├бva bude smaz├бna - nelze to vz├нt zp─Ыt!</string>
<string name="confirm_verb">Potvrdit</string>
<string name="send_us_an_email">Po┼бlete n├бm e-mail</string>
<string name="chat_lock">Z├бmek SimpleX</string>
<string name="install_simplex_chat_for_terminal">Instalace <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro termin├бl</string>
<string name="star_on_github">Hv─Ыzda na GitHubu</string>
<string name="rate_the_app">Ohodno┼еte aplikaci</string>
<string name="your_SMP_servers">Va┼бe servery SMP</string>
<string name="network_disable_socks_info">Pokud potvrd├нte, budou servery pro zas├нl├бn├н zpr├бv vid─Ыt va┼бi IP adresu a v├б┼б poskytovatel - ke kter├╜m server┼пm se p┼Щipojujete.</string>
<string name="colored">barevn├й</string>
<string name="secret">secret</string>
<string name="callstatus_calling">vol├бn├нтАж</string>
<string name="callstate_connected">p┼Щipojeno</string>
<string name="callstate_ended">ukon─Нeno</string>
<string name="next_generation_of_private_messaging">Nov├б generace soukrom├╜ch zpr├бv</string>
<string name="people_can_connect_only_via_links_you_share">Lid├й se s v├бmi mohou spojit pouze prost┼Щednictv├нm odkaz┼п, kter├й sd├нl├нte.</string>
<string name="integrity_msg_bad_hash">┼бpatn├╜ hash zpr├бvy</string>
<string name="chat_database_imported">Importovan├б datab├бze chatu</string>
<string name="new_passphrase">Nov├б p┼Щ├нstupov├б fr├бzeтАж</string>
<string name="save_passphrase_and_open_chat">Ulo┼╛te heslo a otev┼Щete chat</string>
<string name="chat_archive_section">ARCHIV CHATU</string>
<string name="no_contacts_selected">Nebyl vybr├бn ┼╛├бdn├╜ kontakt</string>
<string name="invite_prohibited_description">Sna┼╛├нte se pozvat kontakt, se kter├╜m jste sd├нleli inkognito profil, do skupiny, ve kter├й pou┼╛├нv├бte sv┼пj hlavn├н profil</string>
<string name="info_row_group">Skupina</string>
<string name="network_options_revert">Vr├бtit</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Aktualizac├н nastaven├н se klient znovu p┼Щipoj├н ke v┼бem server┼пm.</string>
<string name="accept_feature_set_1_day">Nastavit 1 den</string>
<string name="connection_error_auth">Chyba spojen├н (AUTH)</string>
<string name="sender_may_have_deleted_the_connection_request">Odes├нlatel mo┼╛n├б smazal po┼╛adavek p┼Щipojen├н</string>
<string name="error_smp_test_server_auth">Server vy┼╛aduje autorizaci pro vytv├б┼Щen├н front, zkontrolujte heslo</string>
<string name="smp_server_test_delete_queue">Odstranit frontu</string>
<string name="delete_group_menu_action">Smazat</string>
<string name="delete_pending_connection__question">Smazat ─Нekaj├нc├н p┼Щipojen├н\?</string>
<string name="icon_descr_settings">Nastaven├н</string>
<string name="image_descr_qr_code">QR k├│d</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">V├б┼б kontakt m┼п┼╛e z aplikace naskenovat QR k├│d.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Pokud se nem┼п┼╛ete setkat osobn─Ы, uka┼╛te <b>ve videohovoru QR k├│d</b> nebo sd├нlejte odkaz.</string>
<string name="scan_code">Skenovat k├│d</string>
<string name="incorrect_code">Nespr├бvn├╜ bezpe─Нnostn├н k├│d!</string>
<string name="scan_code_from_contacts_app">Naskenujte bezpe─Нnostn├н k├│d z aplikace va┼бeho kontaktu.</string>
<string name="mark_code_verified">Ozna─Нit jako ov─Ы┼Щen├╜</string>
<string name="clear_verification">Zru┼бte ov─Ы┼Щen├н</string>
<string name="to_verify_compare">Chcete-li ov─Ы┼Щit koncov├й ┼бifrov├бn├н u sv├йho kontaktu, porovnejte (nebo naskenujte) k├│d na sv├╜ch za┼Щ├нzen├нch.</string>
<string name="your_settings">Va┼бe nastaven├н</string>
<string name="your_simplex_contact_address">Va┼бe <xliff:g id="appName">Adresa kontaktu SimpleX</xliff:g></string>
<string name="database_passphrase_and_export">Datab├бzov├б hesla a export</string>
<string name="your_chat_profiles">Va┼бe profily v chatu</string>
<string name="chat_with_the_founder">Zas├нl├бn├н ot├бzek a n├бpad┼п</string>
<string name="smp_servers_test_server">Testovac├н server</string>
<string name="enter_one_ICE_server_per_line">Servery ICE (jeden na ┼Щ├бdek)</string>
<string name="network_use_onion_hosts_required_desc">Pro p┼Щipojen├н budou vy┼╛adov├бni Onion hostitel├й.</string>
<string name="update_network_session_mode_question">Aktualizovat re┼╛im izolace\?</string>
<string name="app_version_code">Sestaven├н aplikace: %s</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">M┼п┼╛ete sd├нlet svou adresu jako odkaz nebo jako QR k├│d - kdokoli se k v├бm bude moci p┼Щipojit. O sv├й kontakty nep┼Щijdete, pokud ji pozd─Ыji sma┼╛ete.</string>
<string name="share_link">Sd├нlet odkaz</string>
<string name="delete_address">Odstranit adresu</string>
<string name="full_name__field">Cel├й jm├йno:</string>
<string name="your_current_profile">V├б┼б sou─Нasn├╜ profil</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Pro zachov├бn├н soukrom├н m├б aplikace m├нsto push ozn├бmen├н <b><xliff:g id="appName">SimpleX</xliff:g> slu┼╛bu na pozad├н</b> - denn─Ы spot┼Щebuje n─Ыkolik procent baterie.</string>
<string name="periodic_notifications">Pravideln├б ozn├бmen├н</string>
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> slu┼╛ba</string>
<string name="simplex_service_notification_text">P┼Щ├нjem zpr├бvтАж</string>
<string name="ntf_channel_messages">Zpr├бvy SimpleX Chat</string>
<string name="settings_notification_preview_mode_title">Zobrazen├н n├бhledu</string>
<string name="settings_notification_preview_title">N├бhled ozn├бmen├н</string>
<string name="notifications_mode_off">Spust├н se p┼Щi otev┼Щen├н aplikace</string>
<string name="notifications_mode_periodic">Spou┼бt├н se pravideln─Ы</string>
<string name="notifications_mode_service">V┼╛dy zapnuto</string>
<string name="notifications_mode_periodic_desc">Kontroluje nov├й zpr├бvy ka┼╛d├╜ch 10 minut po dobu a┼╛ 1 minuty</string>
<string name="notification_preview_mode_contact_desc">Zobrazit pouze kontakt</string>
<string name="notification_preview_somebody">Skryt├╜ kontakt:</string>
<string name="la_notice_turn_on">Zapn─Ыte funkci</string>
<string name="auth_simplex_lock_turned_on">Zapnut├╜ z├бmek SimpleX Lock</string>
<string name="auth_unlock">Odemknut├н str├бnky</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Ov─Ы┼Щov├бn├н za┼Щ├нzen├н nen├н povoleno. Jakmile povol├нte ov─Ы┼Щov├бn├н za┼Щ├нzen├н, m┼п┼╛ete z├бmek SimpleX Lock zapnout prost┼Щednictv├нm Nastaven├н.</string>
<string name="auth_device_authentication_is_disabled_turning_off">Ov─Ы┼Щov├бn├н za┼Щ├нzen├н je zak├бz├бno. Vypnut├н z├бmku SimpleX Lock.</string>
<string name="edit_verb">Upravit</string>
<string name="delete_verb">Smazat</string>
<string name="for_everybody">Pro v┼бechny</string>
<string name="icon_descr_sent_msg_status_sent">odesl├бno</string>
<string name="contact_connection_pending">p┼Щipojen├нтАж</string>
<string name="images_limit_desc">Sou─Нasn─Ы lze odeslat pouze 10 obr├бzk┼п</string>
<string name="image_decoding_exception_title">Chyba dek├│dov├бn├н</string>
<string name="image_saved">Obr├бzek ulo┼╛en do galerie</string>
<string name="icon_descr_file">Soubor</string>
<string name="large_file">Velk├╜ soubor!</string>
<string name="file_will_be_received_when_contact_is_online">Soubor bude p┼Щijat, a┼╛ bude v├б┼б kontakt online, vy─Нkejte pros├нm nebo se pod├нvejte pozd─Ыji!</string>
<string name="file_saved">Soubor ulo┼╛en</string>
<string name="file_not_found">Soubor nebyl nalezen</string>
<string name="voice_message_with_duration">Hlasov├б zpr├бva (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontakt a v┼бechny zpr├бvy budou smaz├бny - nelze to vz├нt zp─Ыt!</string>
<string name="button_delete_contact">Smazat kontakt</string>
<string name="text_field_set_contact_placeholder">Nastaven├н jm├йna kontaktuтАж</string>
<string name="view_security_code">Zobrazen├н bezpe─Нnostn├нho k├│du</string>
<string name="icon_descr_record_voice_message">Nahr├бt hlasovou zpr├бvu</string>
<string name="voice_messages_prohibited">Hlasov├й zpr├бvy jsou zak├бz├бny!</string>
<string name="ask_your_contact_to_enable_voice">Pros├нm, po┼╛├бdejte kontaktn├н osobu, aby umo┼╛nila odes├нl├бn├н hlasov├╜ch zpr├бv.</string>
<string name="send_live_message_desc">Poslat ┼╛ivou zpr├бvu - zpr├бva se bude aktualizovat pro p┼Щ├нjemce b─Ыhem psan├н.</string>
<string name="share_one_time_link">Vytvo┼Щit jednor├бzovou pozv├бnku</string>
<string name="scan_QR_code">Skenovat QR k├│d</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">( skenovat nebo vlo┼╛it ze schr├бnky)</string>
<string name="edit_image">Upravit obr├бzek</string>
<string name="delete_image">Smazat obr├бzek</string>
<string name="callstatus_error">chyba vol├бn├н</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protokol a k├│d s otev┼Щen├╜m zdrojov├╜m k├│dem - servery m┼п┼╛e provozovat kdokoli.</string>
<string name="create_your_profile">Vytvo┼Щte si sv┼пj profil</string>
<string name="make_private_connection">Vytvo┼Щte si soukrom├й p┼Щipojen├н</string>
<string name="encrypted_video_call">Videohovor ┼бifrovan├╜ e2e</string>
<string name="encrypted_audio_call">e2e ┼бifrovan├╜ audio hovor</string>
<string name="status_contact_has_e2e_encryption">kontakt m├б ┼бifrov├бn├н e2e</string>
<string name="status_contact_has_no_e2e_encryption">kontakt nem├б ┼бifrov├бn├н e2e</string>
<string name="call_connection_peer_to_peer">peer-to-peer</string>
<string name="call_connection_via_relay">p┼Щes rel├й</string>
<string name="icon_descr_hang_up">Zav─Ыsit</string>
<string name="icon_descr_video_off">Video vypnuto</string>
<string name="icon_descr_video_on">Video zapnuto</string>
<string name="icon_descr_audio_off">Zvuk vypnut├╜</string>
<string name="integrity_msg_bad_id">┼бpatn├й ID zpr├бvy</string>
<string name="integrity_msg_duplicate">duplicitn├н zpr├бva</string>
<string name="alert_title_skipped_messages">P┼Щesko─Нen├й zpr├бvy</string>
<string name="privacy_and_security">Ochrana osobn├нch ├║daj┼п a zabezpe─Нen├н</string>
<string name="your_privacy">Va┼бe soukrom├н</string>
<string name="protect_app_screen">Ochrana obrazovky aplikace</string>
<string name="send_link_previews">Odes├нl├бn├н n├бhled┼п odkaz┼п</string>
<string name="full_backup">Z├бlohov├бn├н dat aplikace</string>
<string name="confirm_new_passphrase">Potvrdit novou heslovou fr├бziтАж</string>
<string name="error_with_info">Chyba: %s</string>
<string name="leave_group_question">Opustit skupinu\?</string>
<string name="icon_descr_group_inactive">Skupina je neaktivn├н</string>
<string name="rcv_group_event_member_left">left</string>
<string name="clear_contacts_selection_button">Vymazat</string>
<string name="switch_verb">P┼Щepnout</string>
<string name="member_role_will_be_changed_with_notification">Role bude zm─Ыn─Ыna na \"%s\". V┼бichni ve skupin─Ы budou informov├бni.</string>
<string name="error_removing_member">Chyba p┼Щi odebr├бn├н ─Нlena</string>
<string name="error_saving_group_profile">Chyba p┼Щi ukl├бd├бn├н profilu skupiny</string>
<string name="network_option_seconds_label">sec</string>
<string name="incognito_info_allows">Umo┼╛┼Иuje m├нt v jednom profilu chatu mnoho anonymn├нch spojen├н bez jak├╜chkoli sd├нlen├╜ch ├║daj┼п mezi nimi.</string>
<string name="incognito_info_share">Pokud s n─Ыk├╜m sd├нl├нte inkognito profil, bude tento profil pou┼╛it pro skupiny, do kter├╜ch v├бs pozve.</string>
<string name="theme_system">Syst├йm</string>
<string name="voice_messages">Hlasov├й zpr├бvy</string>
<string name="both_you_and_your_contacts_can_delete">Vy i v├б┼б kontakt m┼п┼╛ete nevratn─Ы mazat odeslan├й zpr├бvy.</string>
<string name="ttl_m">\"%dm</string>
<string name="ttl_mth">\"%dmth</string>
<string name="ttl_hours">\"%d hodin</string>
<string name="ttl_h">\"%dh</string>
<string name="ttl_d">\"%dd</string>
<string name="v4_2_security_assessment">Posouzen├н bezpe─Нnosti</string>
<string name="v4_2_security_assessment_desc">Bezpe─Нnost SimpleX Chat byla prov─Ы┼Щena spole─Нnost├н Trail of Bits.</string>
<string name="v4_3_irreversible_message_deletion">Nevratn├й maz├бn├н zpr├бv</string>
<string name="v4_3_improved_server_configuration">Vylep┼бen├б konfigurace serveru</string>
<string name="v4_3_improved_privacy_and_security">Vylep┼бen├б ochrana soukrom├н a zabezpe─Нen├н</string>
<string name="v4_3_improved_privacy_and_security_desc">Skryt├н obrazovky aplikace v posledn├нch aplikac├нch.</string>
<string name="v4_4_disappearing_messages_desc">Odeslan├й zpr├бvy se po uplynut├н nastaven├й doby odstran├н.</string>
<string name="v4_4_live_messages">┼╜iv├й zpr├бvy</string>
<string name="v4_4_live_messages_desc">P┼Щ├нjemci uvid├н aktualizace b─Ыhem jejich psan├н.</string>
<string name="v4_4_verify_connection_security">Ov─Ы┼Щen├н zabezpe─Нen├н p┼Щipojen├н</string>
<string name="v4_4_french_interface">Francouzsk├й rozhran├н</string>
<string name="v4_4_french_interface_descr">D├нky u┼╛ivatel┼пm - p┼Щisp├нvejte prost┼Щednictv├нm Weblate!</string>
<string name="v4_5_multiple_chat_profiles">V├нce chatovac├нch profil┼п</string>
<string name="v4_5_multiple_chat_profiles_descr">R┼пzn├б jm├йna, avatary a dopravn├н izolace.</string>
<string name="v4_5_message_draft">N├бvrh zpr├бvy</string>
<string name="v4_5_message_draft_descr">Zachov├бn├н posledn├нho n├бvrhu zpr├бvy s p┼Щ├нlohami.</string>
<string name="v4_5_transport_isolation">Izolace transportu</string>
<string name="v4_5_transport_isolation_descr">Podle profilu chatu (v├╜choz├н) nebo podle p┼Щipojen├н (BETA).</string>
<string name="you_will_join_group">P┼Щipoj├нte se ke skupin─Ы, na kterou tento odkaz odkazuje, a spoj├нte se s jej├нmi ─Нleny.</string>
<string name="connect_via_link_verb">P┼Щipojen├н</string>
<string name="connected_to_server_to_receive_messages_from_contact">Jste p┼Щipojeni k serveru, kter├╜ se pou┼╛├нv├б k p┼Щij├нm├бn├н zpr├бv od tohoto kontaktu.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Pokou┼б├нte se p┼Щipojit k serveru pou┼╛├нvan├йmu pro p┼Щ├нjem zpr├бv od tohoto kontaktu (chyba: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="marked_deleted_description">ozna─Нeno jako smaz├бno</string>
<string name="sending_files_not_yet_supported">Odes├нl├бn├н soubor┼п zat├нm nen├н podporov├бno</string>
<string name="receiving_files_not_yet_supported">p┼Щij├нm├бn├н soubor┼п zat├нm nen├н podporov├бno</string>
<string name="sender_you_pronoun">ty</string>
<string name="unknown_message_format">nezn├бm├╜ form├бt zpr├бvy</string>
<string name="invalid_message_format">neplatn├╜ form├бt zpr├бvy</string>
<string name="live">┼╜IV─Ъ</string>
<string name="description_via_group_link_incognito">inkognito p┼Щes skupinov├╜ odkaz</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Ujist─Ыte se, ┼╛e adresy server┼п SMP jsou ve spr├бvn├йm form├бtu, odd─Ыlen├й na ┼Щ├бdc├нch a nejsou duplicitn├н.</string>
<string name="failed_to_create_user_title">Chyba p┼Щi vytv├б┼Щen├н profilu!</string>
<string name="failed_to_create_user_duplicate_title">Duplicitn├н zobrazovan├й jm├йno!</string>
<string name="failed_to_active_user_title">Chyba p┼Щi p┼Щep├нn├бn├н profilu!</string>
<string name="error_joining_group">Chyba p┼Щi p┼Щipojov├бn├н ke skupin─Ы</string>
<string name="cannot_receive_file">Nelze p┼Щijmout soubor</string>
<string name="sender_cancelled_file_transfer">Odes├нlatel zru┼бil p┼Щenos souboru.</string>
<string name="error_receiving_file">Chyba p┼Щi p┼Щ├нjmu souboru</string>
<string name="error_creating_address">Chyba p┼Щi vytv├б┼Щen├н adresy</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Zkontrolujte, zda jste pou┼╛ili spr├бvn├╜ odkaz, nebo po┼╛├бdejte kontakt, aby v├бm poslal jin├╜.</string>
<string name="connection_error_auth_desc">Pokud v├б┼б kontakt spojen├н nesmazal nebo tento odkaz ji┼╛ byl pou┼╛it, m┼п┼╛e se jednat o chybu - nahlaste ji pros├нm. Chcete-li se p┼Щipojit, po┼╛├бdejte pros├нm sv┼пj kontakt o vytvo┼Щen├н jin├йho odkazu pro p┼Щipojen├н a zkontrolujte, zda m├бte stabiln├н p┼Щipojen├н k s├нti.</string>
<string name="error_deleting_contact">Chyba maz├бn├н kontaktu</string>
<string name="error_deleting_group">Chyba maz├бn├н skupiny</string>
<string name="error_deleting_contact_request">Chyba maz├бn├н ┼╛├бdosti kontaktu</string>
<string name="error_deleting_pending_contact_connection">Chyba maz├бn├н prob├нhaj├нc├нho p┼Щipojen├н kontaktu</string>
<string name="error_smp_test_failed_at_step">Test selhal v kroku %s.</string>
<string name="error_smp_test_certificate">Je mo┼╛n├й, ┼╛e otisk certifik├бtu v adrese serveru je nespr├бvn├╜.</string>
<string name="smp_server_test_connect">P┼Щipojit</string>
<string name="smp_server_test_disconnect">Odpojit</string>
<string name="error_deleting_user">Chyba maz├бn├н u┼╛ivatelsk├йho profilu</string>
<string name="icon_descr_instant_notifications">Okam┼╛it├б ozn├бmen├н</string>
<string name="service_notifications_disabled">Okam┼╛it├б ozn├бmen├н jsou vypnut├б!</string>
<string name="turning_off_service_and_periodic">Je aktivn├н optimalizace baterie, kter├б vyp├нn├б slu┼╛bu na pozad├н a pravideln├й po┼╛adavky na nov├й zpr├бvy. M┼п┼╛ete je znovu povolit prost┼Щednictv├нm nastaven├н.</string>
<string name="periodic_notifications_disabled">Pravideln├б ozn├бmen├н jsou vypnuta!</string>
<string name="database_initialization_error_desc">Datab├бze nefunguje spr├бvn─Ы. Klepnut├нm na se dozv├нte v├нce</string>
<string name="notifications_mode_off_desc">Aplikace m┼п┼╛e p┼Щij├нmat ozn├бmen├н pouze p┼Щi sv├йm spu┼бt─Ыn├н, ┼╛├бdn├б slu┼╛ba na pozad├н se nespust├н</string>
<string name="notification_display_mode_hidden_desc">Skr├╜t kontakt a zpr├бvu</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Chcete-li chr├бnit sv├й informace, zapn─Ыte z├бmek SimpleX Lock. P┼Щed zapnut├нm t├йto funkce budete vyzv├бni k dokon─Нen├н ov─Ы┼Щen├н.</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">P┼Щi spu┼бt─Ыn├н nebo obnoven├н aplikace po 30 sekund├бch na pozad├н budete vyzv├бni k ov─Ы┼Щen├н.</string>
<string name="auth_disable_simplex_lock">Vypnut├н z├бmku SimpleX</string>
<string name="auth_confirm_credential">Potvr─Пte sv├й pov─Ы┼Щen├н</string>
<string name="auth_unavailable">Ov─Ы┼Щov├бn├н nen├н k dispozici</string>
<string name="auth_stop_chat">Zastaven├н chatu</string>
<string name="auth_open_chat_console">Otev┼Щete konzolu chatu</string>
<string name="message_delivery_error_title">Chyba doru─Нen├н zpr├бvy</string>
<string name="message_delivery_error_desc">Tento kontakt s nejv─Ыt┼б├н pravd─Ыpodobnost├н smazal spojen├н s v├бmi.</string>
<string name="save_verb">Ulo┼╛it</string>
<string name="reveal_verb">Odhalit</string>
<string name="hide_verb">Skr├╜t</string>
<string name="delete_message__question">Smazat zpr├бvu\?</string>
<string name="delete_message_mark_deleted_warning">Zpr├бva bude ozna─Нena ke smaz├бn├н. P┼Щ├нjemce (p┼Щ├нjemci) bude moci tuto zpr├бvu odhalit.</string>
<string name="for_me_only">Smazat pro m─Ы</string>
<string name="icon_descr_edited">upraveno</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">neautorizovan├й odesl├бn├н</string>
<string name="group_preview_you_are_invited">jste pozv├бni do skupiny</string>
<string name="group_preview_join_as">p┼Щipojit jako %s</string>
<string name="group_connection_pending">p┼Щipojuje seтАж</string>
<string name="tap_to_start_new_chat">Klepnut├нm na zah├бj├нte nov├╜ chat</string>
<string name="chat_with_developers">Chatujte s v├╜voj├б┼Щi</string>
<string name="you_have_no_chats">Nem├бte ┼╛├бdn├й konverzace</string>
<string name="icon_descr_cancel_image_preview">Zru┼бit n├бhled obr├бzku</string>
<string name="share_message">Sd├нlet zpr├бvuтАж</string>
<string name="share_image">Sd├нlet obr├бzekтАж</string>
<string name="icon_descr_cancel_file_preview">Zru┼бit n├бhled souboru</string>
<string name="images_limit_title">P┼Щ├нli┼б mnoho obr├бzk┼п!</string>
<string name="waiting_for_file">─Мek├бn├н na soubor</string>
<string name="notifications">Ozn├бmen├н</string>
<string name="delete_contact_question">Smazat kontakt\?</string>
<string name="icon_descr_server_status_pending">─Мek├б na vy┼Щ├нzen├н</string>
<string name="verify_security_code">Ov─Ы┼Щen├н bezpe─Нnostn├нho k├│du</string>
<string name="icon_descr_send_message">Odeslat zpr├бvu</string>
<string name="only_group_owners_can_enable_voice">Pouze majitel├й skupin mohou povolit zas├нl├бn├н hlasov├╜ch zpr├бv.</string>
<string name="send_live_message">Odeslat ┼╛ivou zpr├бvu</string>
<string name="live_message">┼╜iv├й zpr├бvy!</string>
<string name="connect_via_link_or_qr">P┼Щipojit se prost┼Щednictv├нm odkazu / QR k├│du</string>
<string name="thank_you_for_installing_simplex">D─Ыkujeme za instalaci <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="you_can_connect_to_simplex_chat_founder">M┼п┼╛ete se <font color="#0088ff">p┼Щipojit k <xliff:g id="appNameFull">SimpleX Chat</xliff:g> v├╜voj├б┼Щ┼пm a polo┼╛it jim p┼Щ├нpadn├й dotazy a z├нskat aktualizace</font>.</string>
<string name="to_connect_via_link_title">K p┼Щipojen├н prost┼Щednictv├нm odkazu</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Pokud jste dostal <xliff:g id="appName">SimpleX Chat</xliff:g> zvac├н odkaz, M┼п┼╛ete ho otev┼Щ├нt v prohl├н┼╛e─Нi:</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Pokud zvol├нte odm├нtnut├н, odes├нlatel NEBUDE upozorn─Ыn.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">ЁЯУ▒ mobiln├н telefon: tap <b>Otev┼Щete v mobiln├н aplikaci</b>, potom klikn─Ыte <b>P┼Щipojit</b>.</string>
<string name="reject_contact_button">Odm├нtnout</string>
<string name="clear_chat_button">─Мist├б konverzace</string>
<string name="clear_chat_menu_action">─Мist├╜</string>
<string name="delete_contact_menu_action">Smazat</string>
<string name="set_contact_name">Nastavit jm├йno kontaktu</string>
<string name="you_accepted_connection">P┼Щijali jste spojen├н</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Aby se p┼Щipojen├н dokon─Нilo, mus├н b├╜t v├б┼б kontakt online. Toto p┼Щipojen├н m┼п┼╛ete zru┼бit a kontakt odebrat (a zkusit to pozd─Ыji s nov├╜m odkazem).</string>
<string name="contact_wants_to_connect_with_you">Chce se s v├бmi spojit!</string>
<string name="icon_descr_profile_image_placeholder">Z├бstupce profilov├йho obr├бzku</string>
<string name="image_descr_profile_image">profilov├╜ obr├бzek</string>
<string name="icon_descr_close_button">Tla─Н├нtko Zav┼Щ├нt</string>
<string name="alert_title_contact_connection_pending">Kontakt je┼бt─Ы nen├н p┼Щipojen!</string>
<string name="image_descr_link_preview">n├бhledov├╜ obr├бzek odkazu</string>
<string name="icon_descr_cancel_link_preview">Zru┼бit n├бhled odkazu</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> Logo</string>
<string name="icon_descr_email">E-mail</string>
<string name="icon_descr_more_button">V├нce na</string>
<string name="show_QR_code">Zobrazit QR k├│d</string>
<string name="invalid_QR_code">Neplatn├╜ QR k├│d</string>
<string name="this_QR_code_is_not_a_link">Tento QR k├│d nen├н odkaz!</string>
<string name="invalid_contact_link">Neplatn├╜ odkaz!</string>
<string name="this_link_is_not_a_valid_connection_link">Tento odkaz nen├н platn├╜m odkazem pro p┼Щipojen├н!</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Pokud se nem┼п┼╛ete setkat osobn─Ы, m┼п┼╛ete <b>skenovat QR k├│d ve videohovoru</b> nebo m┼п┼╛e v├б┼б kontakt sd├нlet pozv├бnku.</string>
<string name="connect_via_link">P┼Щipojte se prost┼Щednictv├нm odkazu</string>
<string name="connect_button">P┼Щipojit</string>
<string name="paste_button">Vlo┼╛it</string>
<string name="this_string_is_not_a_connection_link">Tento ┼Щet─Ыzec nen├н odkazem na p┼Щipojen├н!</string>
<string name="you_can_also_connect_by_clicking_the_link">M┼п┼╛ete se tak├й p┼Щipojit kliknut├нm na odkaz. Pokud se otev┼Щe v prohl├н┼╛e─Нi, klikn─Ыte na tla─Н├нtko <b>Otev┼Щ├нt v mobiln├н aplikaci</b>.</string>
<string name="is_not_verified">\"%s nen├н ov─Ы┼Щeno</string>
<string name="how_to_use_simplex_chat">Jak ji pou┼╛├нvat</string>
<string name="markdown_help">N├бpov─Ыda k markdown</string>
<string name="smp_servers_save">Ulo┼╛it servery</string>
<string name="markdown_in_messages">Markdown ve zpr├бv├бch</string>
<string name="smp_servers_test_servers">Testovac├н servery</string>
<string name="smp_servers_preset_server">P┼Щednastaven├╜ server</string>
<string name="smp_servers_your_server">V├б┼б server</string>
<string name="smp_servers_your_server_address">Adresa va┼бeho serveru</string>
<string name="smp_servers_use_server">Pou┼╛├нt server</string>
<string name="smp_servers_use_server_for_new_conn">Pou┼╛├нt pro nov├б p┼Щipojen├н</string>
<string name="smp_servers_per_user">Servery pro nov├б p┼Щipojen├н va┼бeho aktu├бln├нho profilu chatu.</string>
<string name="use_simplex_chat_servers__question">Pou┼╛├нt <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servery\?</string>
<string name="using_simplex_chat_servers">Pou┼╛it├н <xliff:g id="appNameFull">SimpleX Chat</xliff:g> server┼п.</string>
<string name="saved_ICE_servers_will_be_removed">Ulo┼╛en├й servery WebRTC ICE budou odstran─Ыny.</string>
<string name="error_saving_ICE_servers">Chyba p┼Щi ukl├бd├бn├н server┼п ICE</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Ujist─Ыte se, ┼╛e adresy server┼п WebRTC ICE jsou ve spr├бvn├йm form├бtu, odd─Ыlen├й na ┼Щ├бdc├нch a nejsou duplicitn├н.</string>
<string name="save_servers_button">Ulo┼╛it</string>
<string name="network_and_servers">S├н┼е a servery</string>
<string name="network_socks_toggle">Pou┼╛├нt proxy server SOCKS (port 9050)</string>
<string name="update_onion_hosts_settings_question">Aktualizovat nastaven├н hostitel┼п .onion\?</string>
<string name="network_use_onion_hosts">Pou┼╛├нt hostitele .onion</string>
<string name="network_use_onion_hosts_prefer">Kdy┼╛ je k dispozici</string>
<string name="network_use_onion_hosts_required">Povinn├й</string>
<string name="network_use_onion_hosts_prefer_desc">Onion hostitel├й se pou┼╛ij├н, pokud jsou k dispozici.</string>
<string name="network_use_onion_hosts_no_desc">Onion hostitel├й nebudou pou┼╛iti.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hostitel├й budou pou┼╛iti, pokud budou k dispozici.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Pro p┼Щipojen├н budou vy┼╛adov├бni Onion hostitel├й.</string>
<string name="network_session_mode_transport_isolation">Izolace p┼Щenosu</string>
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
<string name="network_session_mode_entity_description">Odd─Ыlit TCP p┼Щipojen├н (a SOCKS pov─Ы┼Щen├н) bude pou┼╛ito <b>pro v┼бechny kontakty a ─Нleny skupin</b>. <b>Upozorn─Ыn├н</b>: Pokud m├бte mnoho p┼Щipojen├н, m┼п┼╛e b├╜t spot┼Щeba baterie a provoz podstatn─Ы vy┼б┼б├н a n─Ыkter├б p┼Щipojen├н mohou selhat.</string>
<string name="appearance_settings">Vzhled</string>
<string name="app_version_title">Verze aplikace</string>
<string name="app_version_name">Verze aplikace: v%s</string>
<string name="core_version">Verze j├бdra: v%s</string>
<string name="core_build_timestamp">J├бdro sestaveno na: %s</string>
<string name="delete_address__question">Smazat adresu\?</string>
<string name="all_your_contacts_will_remain_connected">V┼бechny va┼бe kontakty z┼пstanou p┼Щipojeny.</string>
<string name="contact_requests">┼╜├бdosti o kontakt</string>
<string name="display_name__field">Zobrazovan├й jm├йno:</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">V├б┼б profil je ulo┼╛en v za┼Щ├нzen├н a je sd├нlen pouze s va┼бimi kontakty. <xliff:g id="appName">SimpleX</xliff:g> servery v├б┼б profil nevid├н.</string>
<string name="save_preferences_question">Ulo┼╛it p┼Щedvolby\?</string>
<string name="save_and_notify_contact">Ulo┼╛it a upozornit kontakt</string>
<string name="save_and_notify_contacts">Ulo┼╛it a upozornit kontakty</string>
<string name="you_control_your_chat">Chat m├бte pod kontrolou!</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Na serverech neukl├бd├бme ┼╛├бdn├й va┼бe kontakty ani zpr├бvy (po doru─Нen├н).</string>
<string name="your_profile_is_stored_on_your_device">V├б┼б profil, kontakty a doru─Нen├й zpr├бvy jsou ulo┼╛eny ve va┼бem za┼Щ├нzen├н.</string>
<string name="display_name">Zobrazovan├й jm├йno</string>
<string name="full_name_optional__prompt">Cel├й jm├йno (voliteln├й)</string>
<string name="create_profile_button">Vytvo┼Щit</string>
<string name="how_to_use_markdown">Jak pou┼╛├нvat markdown</string>
<string name="you_can_use_markdown_to_format_messages__prompt">K form├бtov├бn├н zpr├бv m┼п┼╛ete pou┼╛├нt markdown:</string>
<string name="italic">kurz├нva</string>
<string name="strikethrough">p┼Щe┼бkrtnout</string>
<string name="callstatus_missed">zme┼бkan├й vol├бn├н</string>
<string name="callstatus_rejected">odm├нtnut├╜ hovor</string>
<string name="callstatus_connecting">spojovac├н hovorтАж</string>
<string name="callstatus_ended">hovor ukon─Нen <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstate_starting">za─Н├нn├бтАж</string>
<string name="callstate_waiting_for_answer">─Нek├б na odpov─Ы─ПтАж</string>
<string name="callstate_waiting_for_confirmation">─Нek├бn├н na potvrzen├нтАж</string>
<string name="callstate_received_answer">obdr┼╛el odpov─Ы─ПтАж</string>
<string name="callstate_received_confirmation">obdr┼╛el potvrzen├нтАж</string>
<string name="callstate_connecting">p┼Щipojen├нтАж</string>
<string name="privacy_redefined">Nov├й vymezen├н soukrom├н</string>
<string name="first_platform_without_user_ids">1. Platforma bez identifik├бtor┼п u┼╛ivatel┼п - soukrom├б u┼╛ od z├бm─Ыru.</string>
<string name="immune_to_spam_and_abuse">Odoln├б v┼п─Нi spamu a zneu┼╛it├н</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">K ochran─Ы soukrom├н, m├нsto u┼╛ivatelsk├╜ch ID pou┼╛├нvan├╜ch v┼бemi ostatn├нmi platformami, <xliff:g id="appName">SimpleX</xliff:g> m├б identifik├бtory pro fronty zpr├бv, zvl├б┼б┼е pro ka┼╛d├╜ z va┼бich kontakt┼п.</string>
<string name="many_people_asked_how_can_it_deliver">Mnoho lid├н se ptalo: <i>if <xliff:g id="appName">SimpleX</xliff:g> nem├б ┼╛├бdn├й identifik├бtory u┼╛ivatel┼п, jak m┼п┼╛e doru─Нovat zpr├бvy\?</i></string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Vy kontrolujete p┼Щez kter├╜ server(y) <b>p┼Щij├нmat</b> zpr├бvy, va┼бe kontakty - servery kter├й pou┼╛├нv├бte pro konverzaci.</string>
<string name="read_more_in_github">Dal┼б├н informace najdete v na┼бem repozit├б┼Щi GitHub.</string>
<string name="read_more_in_github_with_link">Dal┼б├н informace najdete v na┼бem <font color="#0088ff">├║lo┼╛i┼бti GitHub</font>.</string>
<string name="use_chat">Konverzovat</string>
<string name="onboarding_notifications_mode_subtitle">Lze zm─Ыnit pozd─Ыji v nastaven├н.</string>
<string name="onboarding_notifications_mode_off">Kdy┼╛ je aplikace spu┼бt─Ыn├б</string>
<string name="onboarding_notifications_mode_service">Okam┼╛it├╜</string>
<string name="onboarding_notifications_mode_off_desc"><b>Nejlep┼б├н pro baterii</b>. Budete p┼Щij├нmat ozn├бmen├н pouze kdy┼╛ aplikace b─Ы┼╛├н, slu┼╛ba na pozad├н NEBUDE pou┼╛ita.</string>
<string name="onboarding_notifications_mode_periodic_desc">&lt;Dobr├й pro baterii&lt;/b&gt;. Slu┼╛ba na pozad├н kontroluje nov├й zpr├бvy ka┼╛d├╜ch 10 minut. M┼п┼╛ete zme┼бkat hovory a nal├йhav├й zpr├бvy.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Vyu┼╛├нv├б v├нce baterie</b>! Slu┼╛ba na pozad├н je spu┼бt─Ыna v┼╛dy - ozn├бmen├н se zobraz├н, jakmile jsou zpr├бvy k dispozici.</string>
<string name="paste_the_link_you_received">Vlo┼╛en├н p┼Щijat├йho odkazu</string>
<string name="incoming_video_call">P┼Щ├нchoz├н videohovor</string>
<string name="incoming_audio_call">P┼Щ├нchoz├н zvukov├╜ hovor</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> se s v├бmi chce spojit prost┼Щednictv├нm</string>
<string name="video_call_no_encryption">videohovoru (ne┼бifrovan├йho e2e).</string>
<string name="audio_call_no_encryption">zvukov├╜ hovor (ne┼бifrovan├╜ e2e)</string>
<string name="reject">Odm├нtnout</string>
<string name="your_calls">Va┼бe hovory</string>
<string name="connect_calls_via_relay">Spojen├н p┼Щes rel├й</string>
<string name="show_call_on_lock_screen">Zobrazit</string>
<string name="no_call_on_lock_screen">Zak├бzat</string>
<string name="your_ice_servers">Va┼бe servery ICE</string>
<string name="webrtc_ice_servers">WebRTC servery ICE</string>
<string name="status_no_e2e_encryption">bez ┼бifrov├бn├н e2e</string>
<string name="icon_descr_flip_camera">Flipov├б kamera</string>
<string name="icon_descr_call_pending_sent">Prob├нhaj├нc├н hovor</string>
<string name="icon_descr_call_missed">Zme┼бkan├╜ hovor</string>
<string name="icon_descr_call_rejected">Odm├нtnut├╜ hovor</string>
<string name="icon_descr_call_connecting">Spojen├н hovoru</string>
<string name="icon_descr_call_ended">Ukon─Нen├╜ hovor</string>
<string name="answer_call">P┼Щijmout hovor</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> p┼Щesko─Нen├б zpr├бva (zpr├бvy)</string>
<string name="alert_text_skipped_messages_it_can_happen_when">M┼п┼╛e se to st├бt, kdy┼╛: 1. Zpr├бvy na serveru vypr┼б├н, pokud nebyly p┼Щijaty po dobu 30 dn┼п, 2. Server, kter├╜ pou┼╛├нv├бte pro p┼Щ├нjem zpr├бv od tohoto kontaktu, byl aktualizov├бn a restartov├бn. 3. Spojen├н je naru┼бeno. P┼Щipojte se k v├╜voj├б┼Щ┼пm prost┼Щednictv├нm Nastaven├н, abyste mohli dost├бvat aktualizace o serverech. Budeme p┼Щid├бvat redundantn├н servery, abychom zabr├бnili ztr├бt─Ы zpr├бv.</string>
<string name="settings_section_title_you">VY</string>
<string name="settings_section_title_support">PODPORA SIMPLEX CHAT</string>
<string name="settings_section_title_develop">DEVELOP</string>
<string name="settings_developer_tools">N├бstroje pro v├╜voj├б┼Щe</string>
<string name="settings_section_title_incognito">Re┼╛im inkognito</string>
<string name="your_chat_database">Va┼бe chatovac├н datab├бze</string>
<string name="run_chat_section">SPUSTIT CHAT</string>
<string name="chat_is_running">Chat je spu┼бt─Ыn</string>
<string name="chat_is_stopped">Chat je zastaven</string>
<string name="chat_database_section">CHAT DATAB├БZE</string>
<string name="database_passphrase">Heslo datab├бze</string>
<string name="new_database_archive">Archiv nov├й datab├бze</string>
<string name="old_database_archive">Archiv star├й datab├бze</string>
<string name="error_starting_chat">Chyba p┼Щi spu┼бt─Ыn├н chatu</string>
<string name="stop_chat_question">Zastavit chat\?</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Zastaven├н chatu pro export, import nebo smaz├бn├н datab├бze chatu. B─Ыhem zastaven├н chatu nebudete moci p┼Щij├нmat a odes├нlat zpr├бvy.</string>
<string name="stop_chat_confirmation">Zastavit</string>
<string name="set_password_to_export">Nastaven├н p┼Щ├нstupov├й fr├бze pro export</string>
<string name="set_password_to_export_desc">Datab├бze je ┼бifrov├бna pomoc├н n├бhodn├й p┼Щ├нstupov├й fr├бze. P┼Щed exportem ji zm─Ы┼Иte.</string>
<string name="error_stopping_chat">Chyba p┼Щi zastaven├н chatu</string>
<string name="import_database_question">Importovat datab├бzi chatu\?</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Va┼бe aktu├бln├н datab├бze chatu bude Smaz├бna a Nahrazena importovanou datab├бz├н. Tuto akci nelze vz├нt zp─Ыt - v├б┼б profil, kontakty, zpr├бvy a soubory budou nen├бvratn─Ы ztraceny.</string>
<string name="error_deleting_database">Chyba p┼Щi maz├бn├н datab├бze chatu</string>
<string name="error_importing_database">Chyba p┼Щi importu datab├бze chatu</string>
<string name="chat_database_deleted">Datab├бze chatu odstran─Ыna</string>
<string name="delete_files_and_media_for_all_users">Odstran─Ыn├н soubor┼п pro v┼бechny profily chatu</string>
<string name="delete_files_and_media_all">Odstranit v┼бechny soubory</string>
<string name="delete_files_and_media_desc">Tuto akci nelze vr├бtit zp─Ыt - v┼бechny p┼Щijat├й a odeslan├й soubory a m├йdia budou smaz├бny. Obr├бzky s n├нzk├╜m rozli┼бen├нm z┼пstanou zachov├бny.</string>
<string name="no_received_app_files">┼╜├бdn├й p┼Щijat├й ani odeslan├й soubory</string>
<string name="total_files_count_and_size">%d soubor(┼п) s celkovou velikost├н %s</string>
<string name="chat_item_ttl_none">nikdy</string>
<string name="chat_item_ttl_seconds">\"%s vte┼Щin(y)</string>
<string name="messages_section_title">Zpr├бvy</string>
<string name="messages_section_description">Toto nastaven├н se vztahuje na zpr├бvy ve va┼бem aktu├бln├нm profilu chatu.</string>
<string name="delete_messages_after">Smazat zpr├бvy po</string>
<string name="enable_automatic_deletion_question">Povolit automatick├й maz├бn├н zpr├бv\?</string>
<string name="enable_automatic_deletion_message">Tuto akci nelze vz├нt zp─Ыt - zpr├бvy odeslan├й a p┼Щijat├й d┼Щ├нve, ne┼╛ bylo zvoleno, budou smaz├бny. M┼п┼╛e to trvat n─Ыkolik minut.</string>
<string name="error_changing_message_deletion">Chyba zm─Ыny nastaven├н</string>
<string name="save_passphrase_in_keychain">Ulo┼╛it p┼Щ├нstupovou fr├бzi do ├║lo┼╛i┼бt─Ы kl├н─Н┼п</string>
<string name="database_encrypted">Datab├бze za┼бifrov├бna!</string>
<string name="error_encrypting_database">Chyba ┼бifrov├бn├н datab├бze</string>
<string name="encrypt_database">┼аifrovat</string>
<string name="encrypted_with_random_passphrase">Datab├бze je za┼бifrov├бna pomoc├н n├бhodn├й p┼Щ├нstupov├й fr├бze, m┼п┼╛ete ji zm─Ыnit.</string>
<string name="keychain_allows_to_receive_ntfs">K bezpe─Нn├йmu ulo┼╛en├н p┼Щ├нstupov├й fr├бze se pou┼╛ije ├║lo┼╛i┼бt─Ы kl├н─Н┼п Android, po restartov├бn├н aplikace nebo zm─Ыn─Ы p┼Щ├нstupov├й fr├бze - umo┼╛n├н p┼Щij├нm├бn├н ozn├бmen├н.</string>
<string name="you_have_to_enter_passphrase_every_time">Mus├нte zadat p┼Щ├нstupovou fr├бzi p┼Щi ka┼╛d├йm spu┼бt─Ыn├н aplikace - nen├н ulo┼╛ena v za┼Щ├нzen├н.</string>
<string name="encrypt_database_question">┼аifrovat datab├бzi\?</string>
<string name="change_database_passphrase_question">Zm─Ыnit p┼Щ├нstupovou fr├бzi datab├бze\?</string>
<string name="database_will_be_encrypted">Datab├бze bude za┼бifrov├бna.</string>
<string name="database_encryption_will_be_updated">┼аifrovac├н heslov├б fr├бze datab├бze bude aktualizov├бna a ulo┼╛ena do ├║lo┼╛i┼бt─Ы kl├н─Н┼п.</string>
<string name="database_passphrase_will_be_updated">┼аifrovac├н heslov├б fr├бze datab├бze bude aktualizov├бna.</string>
<string name="store_passphrase_securely_without_recover">Ulo┼╛te pros├нm bezpe─Нn─Ы p┼Щ├нstupovou fr├бzi, pokud ji ztrat├нte, NEBUDE mo┼╛n├й p┼Щistupovat k chatu.</string>
<string name="wrong_passphrase">┼аpatn├б p┼Щ├нstupov├б fr├бze k datab├бzi</string>
<string name="encrypted_database">Za┼бifrovan├б datab├бze</string>
<string name="database_error">Chyba datab├бze</string>
<string name="keychain_error">Chyba kl├н─Нenky</string>
<string name="passphrase_is_different">Datab├бzov├б heslov├б fr├бze se li┼б├н od ulo┼╛en├й v kl├н─Нence.</string>
<string name="cannot_access_keychain">Nelze z├нskat p┼Щ├нstup k ├║lo┼╛i┼бti kl├н─Н┼п pro ulo┼╛en├н hesla k datab├бzi.</string>
<string name="unknown_database_error_with_info">Nezn├бm├б chyba datab├бze: %s</string>
<string name="wrong_passphrase_title">┼аpatn├б p┼Щ├нstupov├б fr├бze!</string>
<string name="enter_correct_passphrase">Zadejte spr├бvnou p┼Щ├нstupovou fr├бzi.</string>
<string name="enter_passphrase">Zadejte p┼Щ├нstupovou fr├бziтАж</string>
<string name="database_backup_can_be_restored">Pokus o zm─Ыnu p┼Щ├нstupov├й fr├бze datab├бze nebyl dokon─Нen.</string>
<string name="restore_database_alert_title">Obnovit z├бlohu datab├бze\?</string>
<string name="restore_database_alert_confirm">Obnovit</string>
<string name="database_restore_error">Chyba obnoven├н datab├бze</string>
<string name="restore_passphrase_not_found_desc">Heslo nebylo nalezeno v ├║lo┼╛i┼бti kl├н─Н┼п, zadejte jej pros├нm ru─Нn─Ы. K t├йto situaci mohlo doj├нt, pokud jste obnovili data aplikace pomoc├н z├бlohovac├нho n├бstroje. Pokud tomu tak nen├н, obra┼еte se na v├╜voj├б┼Щe.</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Chat m┼п┼╛ete spustit p┼Щes Nastaven├н aplikace / Datab├бze nebo restartov├бn├нm aplikace.</string>
<string name="save_archive">Ulo┼╛it archiv</string>
<string name="delete_archive">Smazat archiv</string>
<string name="group_invitation_item_description">pozv├бnka do skupiny <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="archive_created_on_ts">Vytvo┼Щeno dne <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Jste zv├бni do skupiny. P┼Щipojte se a spojte se s ─Нleny skupiny.</string>
<string name="join_group_incognito_button">P┼Щipojte se inkognito</string>
<string name="joining_group">P┼Щipojen├н ke skupin─Ы</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">P┼Щipojili jste se k t├йto skupin─Ы. P┼Щipojen├н k pozv├бn├н ─Нlena skupiny.</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">P┼Щestanete dost├бvat zpr├бvy z t├йto skupiny. Historie chatu bude zachov├бna.</string>
<string name="alert_title_group_invitation_expired">Platnost pozv├бnky vypr┼бela!</string>
<string name="alert_message_group_invitation_expired">Pozv├бnka do skupiny ji┼╛ nen├н platn├б, byla odstran─Ыna odes├нlatelem.</string>
<string name="alert_message_no_group">Tato skupina ji┼╛ neexistuje.</string>
<string name="alert_title_cant_invite_contacts_descr">Pro tuto skupinu pou┼╛├нv├бte inkognito profil - abyste zabr├бnili sd├нlen├н sv├йho hlavn├нho profilu, nen├н pozv├бn├н kontakt┼п povoleno.</string>
<string name="you_sent_group_invitation">Odeslali jste pozv├бnku do skupiny</string>
<string name="you_are_invited_to_group">Jste pozv├бni do skupiny</string>
<string name="group_invitation_tap_to_join">Klepnut├нm se p┼Щipoj├нte</string>
<string name="group_invitation_tap_to_join_incognito">Klepnut├нm se p┼Щipoj├нte inkognito</string>
<string name="you_joined_this_group">P┼Щipojili jste se k t├йto skupin─Ы</string>
<string name="you_rejected_group_invitation">Odm├нtli jste pozv├бnku do skupiny</string>
<string name="group_invitation_expired">Platnost pozv├бnky do skupiny vypr┼бela</string>
<string name="rcv_group_event_member_added">pozv├бnka <xliff:g id="profil ─Нlena" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">p┼Щipojeno</string>
<string name="rcv_group_event_changed_member_role">zm─Ыnil roli %s na %s</string>
<string name="rcv_group_event_changed_your_role">zm─Ыnil svou roli na %s</string>
<string name="rcv_group_event_member_deleted">odstran─Ыno <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_user_deleted">odstranil v├бs</string>
<string name="rcv_group_event_group_deleted">odstran─Ыna skupina</string>
<string name="rcv_group_event_updated_group_profile">aktualizoval profil skupiny</string>
<string name="rcv_group_event_invited_via_your_group_link">pozv├бni prost┼Щednictv├нm odkazu na va┼бi skupinu</string>
<string name="snd_group_event_group_profile_updated">profil skupiny aktualizov├бn</string>
<string name="rcv_conn_event_switch_queue_phase_changing">zm─Ыna adresyтАж</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">jste zm─Ыnili adresu pro %s</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">zm─Ыna adresy pro %sтАж</string>
<string name="snd_conn_event_switch_queue_phase_completed">zm─Ыnili jste adresu</string>
<string name="snd_conn_event_switch_queue_phase_changing">zm─Ыna adresyтАж</string>
<string name="group_member_role_member">─Нlen</string>
<string name="group_member_role_owner">vlastn├нk</string>
<string name="group_member_status_removed">odstran─Ыno</string>
<string name="group_member_status_left">opustil</string>
<string name="group_member_status_group_deleted">skupina smaz├бna</string>
<string name="group_member_status_invited">pozv├бnka</string>
<string name="group_member_status_introduced">p┼Щipojuj├нc├н (zaveden├╜)</string>
<string name="group_member_status_intro_invitation">p┼Щipojen├н (pozv├бnka na p┼Щedstaven├н)</string>
<string name="group_member_status_accepted">p┼Щipojen├н (p┼Щijato)</string>
<string name="group_member_status_announced">p┼Щipojen├н (ozn├бmeno)</string>
<string name="group_member_status_connected">p┼Щipojen</string>
<string name="group_member_status_complete">kompletn├н</string>
<string name="group_member_status_creator">tv┼пrce</string>
<string name="group_member_status_connecting">p┼Щipojov├бn├н</string>
<string name="no_contacts_to_add">┼╜├бdn├й kontakty k p┼Щid├бn├н</string>
<string name="new_member_role">Nov├б role ─Нlena</string>
<string name="invite_to_group_button">Pozvat do skupiny</string>
<string name="skip_inviting_button">P┼Щesko─Нit pozv├бn├н ─Нlen┼п</string>
<string name="select_contacts">Vybrat kontakty</string>
<string name="icon_descr_contact_checked">Zkontrolovan├й kontakty</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> kontakt(y) vybr├бn(y)</string>
<string name="button_add_members">Pozvat ─Нleny</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBERS</string>
<string name="group_info_member_you">vy: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="button_delete_group">Smazat skupinu</string>
<string name="delete_group_question">Smazat skupinu\?</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Skupina bude smaz├бna pro v┼бechny ─Нleny - nelze to vz├нt zp─Ыt!</string>
<string name="delete_group_for_self_cannot_undo_warning">Skupina bude smaz├бna pro v├бs - toto nelze vz├нt zp─Ыt!</string>
<string name="button_leave_group">Opustit skupinu</string>
<string name="button_edit_group_profile">Upravit profil skupiny</string>
<string name="group_link">Odkaz na skupinu</string>
<string name="create_group_link">Vytvo┼Щit odkaz na skupinu</string>
<string name="delete_link">Smazat odkaz</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">M┼п┼╛ete sd├нlet odkaz nebo QR k├│d - ke skupin─Ы se bude moci p┼Щipojit kdokoli. O ─Нleny skupiny nep┼Щijdete, pokud ji pozd─Ыji odstran├нte.</string>
<string name="error_creating_link_for_group">Chyba p┼Щi vytv├б┼Щen├н odkazu skupiny</string>
<string name="error_deleting_link_for_group">Chyba p┼Щi odstra┼Иov├бn├н odkazu skupiny</string>
<string name="only_group_owners_can_change_prefs">P┼Щedvolby skupiny mohou m─Ыnit pouze vlastn├нci skupiny.</string>
<string name="section_title_for_console">PRO KONSOLE</string>
<string name="info_row_local_name">M├нstn├н n├бzev</string>
<string name="info_row_database_id">ID datab├бze</string>
<string name="button_remove_member">Odstranit ─Нlena</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">─Мlen bude odstran─Ыn ze skupiny - toto nelze vz├нt zp─Ыt!</string>
<string name="remove_member_confirmation">Odstranit</string>
<string name="role_in_group">Role</string>
<string name="change_role">Zm─Ыnit roli</string>
<string name="change_verb">Zm─Ыnit</string>
<string name="member_role_will_be_changed_with_invitation">Role bude zm─Ыn─Ыna na \"%s\". ─Мlen obdr┼╛├н novou pozv├бnku.</string>
<string name="error_changing_role">Chyba p┼Щi zm─Ыn─Ы role</string>
<string name="conn_level_desc_direct">p┼Щ├нmo</string>
<string name="sending_via">Odes├нl├бn├н p┼Щes</string>
<string name="network_status">Stav s├нt─Ы</string>
<string name="switch_receiving_address">P┼Щep├нna─Н p┼Щij├нmac├н adresy</string>
<string name="group_is_decentralized">Skupina je pln─Ы decentralizovan├б - je viditeln├б pouze pro ─Нleny.</string>
<string name="group_unsupported_incognito_main_profile_sent">Zde nen├н podporov├бn re┼╛im inkognito - ─Нlen┼пm skupiny bude zasl├бn v├б┼б hlavn├н profil.</string>
<string name="save_group_profile">Ulo┼╛en├н profilu skupiny</string>
<string name="network_options_reset_to_defaults">Obnoven├н v├╜choz├нho nastaven├н</string>
<string name="network_option_tcp_connection_timeout">─Мasov├╜ limit p┼Щipojen├н TCP</string>
<string name="network_option_protocol_timeout">─Мasov├╜ limit protokolu</string>
<string name="network_option_ping_interval">Interval PING</string>
<string name="network_option_ping_count">Po─Нet PING</string>
<string name="network_option_enable_tcp_keep_alive">Povolit TCP keep-alive</string>
<string name="update_network_settings_confirmation">Aktualizovat</string>
<string name="users_delete_question">Smazat profil chatu\?</string>
<string name="users_delete_profile_for">Smazat profil chatu pro</string>
<string name="users_delete_with_connections">Profil a p┼Щipojen├н k serveru</string>
<string name="users_delete_data_only">Pouze lok├бln├н profilov├б data</string>
<string name="incognito_random_profile_from_contact_description">N├бhodn├╜ profil bude zasl├бn kontaktu, od kter├йho jste obdr┼╛eli tento odkaz.</string>
<string name="incognito_info_protects">Re┼╛im inkognito chr├бn├н soukrom├н va┼бeho hlavn├нho profilov├йho jm├йna a obr├бzku - pro ka┼╛d├╜ nov├╜ kontakt je vytvo┼Щen nov├╜ n├бhodn├╜ profil.</string>
<string name="incognito_info_find">Chcete-li naj├нt profil pou┼╛it├╜ pro inkognito p┼Щipojen├н, klepn─Ыte na n├бzev kontaktu nebo skupiny v horn├н ─Н├бsti chatu.</string>
<string name="theme_light">Sv─Ыtlo</string>
<string name="theme_dark">Tmav├╜</string>
<string name="theme">T├йma</string>
<string name="chat_preferences_contact_allows">Kontakt povoluje</string>
<string name="chat_preferences_on">na</string>
<string name="chat_preferences_off">vypnuto</string>
<string name="chat_preferences">P┼Щedvolby chatu</string>
<string name="contact_preferences">P┼Щedvolby kontakt┼п</string>
<string name="group_preferences">P┼Щedvolby skupiny</string>
<string name="direct_messages">P┼Щ├нm├й zpr├бvy</string>
<string name="full_deletion">Smazat pro v┼бechny</string>
<string name="feature_enabled">povoleno</string>
<string name="feature_enabled_for_you">povoleno pro v├бs</string>
<string name="feature_off">vypnuto</string>
<string name="prohibit_sending_disappearing_messages">Zak├бzat zas├нl├бn├н mizej├нc├нch zpr├бv.</string>
<string name="contacts_can_mark_messages_for_deletion">Kontakty mohou ozna─Нit zpr├бvy ke smaz├бn├н; vy je budete moci zobrazit.</string>
<string name="prohibit_sending_voice_messages">Zak├бzat odes├нl├бn├н hlasov├╜ch zpr├бv.</string>
<string name="only_you_can_send_disappearing">Miz├нc├н zpr├бvy m┼п┼╛ete odes├нlat pouze vy.</string>
<string name="disappearing_prohibited_in_this_chat">Miz├нc├н zpr├бvy jsou v tomto chatu zak├бz├бny.</string>
<string name="only_your_contact_can_delete">Nevratn─Ы mazat zpr├бvy m┼п┼╛e pouze v├б┼б kontakt (vy je m┼п┼╛ete ozna─Нit ke smaz├бn├н).</string>
<string name="both_you_and_your_contact_can_send_voice">Hlasov├й zpr├бvy m┼п┼╛ete pos├нlat vy i v├б┼б kontakt.</string>
<string name="only_you_can_send_voice">Hlasov├й zpr├бvy m┼п┼╛ete pos├нlat pouze vy.</string>
<string name="only_your_contact_can_send_voice">Hlasov├й zpr├бvy m┼п┼╛e odes├нlat pouze v├б┼б kontakt.</string>
<string name="voice_prohibited_in_this_chat">Hlasov├й zpr├бvy jsou v tomto chatu zak├бz├бny.</string>
<string name="prohibit_sending_disappearing">Zak├бzat pos├нl├бn├н mizej├нc├нch zpr├бv.</string>
<string name="prohibit_message_deletion">Zak├бzat nevratn├й maz├бn├н zpr├бv.</string>
<string name="prohibit_sending_voice">Zak├бzat odes├нl├бn├н hlasov├╜ch zpr├бv.</string>
<string name="group_members_can_send_disappearing">─Мlenov├й skupiny mohou pos├нlat mizej├нc├н zpr├бvy.</string>
<string name="disappearing_messages_are_prohibited">Miz├нc├н zpr├бvy jsou v t├йto skupin─Ы zak├бz├бny.</string>
<string name="group_members_can_send_dms">─Мlenov├й skupiny mohou pos├нlat p┼Щ├нm├й zpr├бvy.</string>
<string name="direct_messages_are_prohibited_in_chat">P┼Щ├нm├й zpr├бvy mezi ─Нleny jsou v t├йto skupin─Ы zak├бz├бny.</string>
<string name="group_members_can_delete">─Мlenov├й skupiny mohou nevratn─Ы mazat odeslan├й zpr├бvy.</string>
<string name="message_deletion_prohibited_in_chat">Nevratn├й maz├бn├н zpr├бv je v t├йto skupin─Ы zak├бz├бno.</string>
<string name="group_members_can_send_voice">─Мlenov├й skupiny mohou pos├нlat hlasov├й zpr├бvy.</string>
<string name="voice_messages_are_prohibited">Hlasov├й zpr├бvy jsou v t├йto skupin─Ы zak├бz├бny.</string>
<string name="delete_after">Smazat po</string>
<string name="ttl_month">\"%d m─Ыs├нc</string>
<string name="ttl_months">\"%d m─Ыs├нc┼п</string>
<string name="ttl_day">\"%d den</string>
<string name="ttl_days">\"%d dn┼п</string>
<string name="ttl_week">\"%d t├╜den</string>
<string name="ttl_weeks">\"%d t├╜dn┼п</string>
<string name="ttl_w">\"%dw</string>
<string name="feature_offered_item">nab├нzeno %s</string>
<string name="feature_cancelled_item">zru┼бeno %s</string>
<string name="whats_new">Co je nov├йho</string>
<string name="new_in_version">Novinky v %s</string>
<string name="v4_2_auto_accept_contact_requests">Automatick├й p┼Щij├нm├бn├н ┼╛├бdost├н o kontakt</string>
<string name="v4_2_auto_accept_contact_requests_desc">S volitelnou uv├нtac├н zpr├бvou.</string>
<string name="v4_3_voice_messages_desc">Max. 40 sekund, p┼Щij├нm├б se okam┼╛it─Ы.</string>
<string name="v4_5_private_filenames">Soukrom├й n├бzvy soubor┼п</string>
<string name="v4_5_private_filenames_descr">Pro ochranu ─Нasov├йho p├бsma, obrazov├й/hlasov├й soubory pou┼╛├нvaj├н UTC.</string>
<string name="v4_5_reduced_battery_usage">Sn├н┼╛en├н spot┼Щeby baterie</string>
<string name="v4_5_reduced_battery_usage_descr">Dal┼б├н vylep┼бen├н se chystaj├н ji┼╛ brzy!</string>
<string name="v4_5_italian_interface">Italsk├й rozhran├н</string>
<string name="v4_5_italian_interface_descr">D├нky u┼╛ivatel┼пm - p┼Щisp├нvejte prost┼Щednictv├нm Weblate!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Budete p┼Щipojeni, a┼╛ bude za┼Щ├нzen├н va┼бeho kontaktu online, vy─Нkejte pros├нm nebo se pod├нvejte pozd─Ыji!</string>
<string name="your_contact_address">Va┼бe kontaktn├н adresa</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">V├б┼б profil v chatu bude odesl├бn va┼бemu kontaktu</string>
<string name="your_chat_profiles_stored_locally">Va┼бe profily v chatu jsou ulo┼╛eny lok├бln─Ы, pouze ve va┼бem za┼Щ├нzen├н.</string>
<string name="your_chats">Va┼бe konverzace</string>
</resources>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,969 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="thousand_abbreviation">k</string>
<string name="connect_via_contact_link">Se connecter via le lien du contact \?</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="profile_will_be_sent_to_contact_sending_link">Votre profil va ├кtre envoy├й au contact qui vous a envoy├й ce lien.</string>
<string name="you_will_join_group">Vous allez rejoindre le groupe correspondant ├а ce lien et ├кtre mis en relation avec les autres membres du groupe.</string>
<string name="connect_via_link_verb">Se connecter</string>
<string name="connect_via_group_link">Se connecter via le lien du groupe \?</string>
<string name="connect_via_invitation_link">Se connecter via un lien d\'invitation \?</string>
<string name="server_error">erreur</string>
<string name="server_connecting">connexion</string>
<string name="server_connected">connect├й</string>
<string name="display_name_connection_established">connexion ├йtablie</string>
<string name="display_name_invited_to_connect">invit├й ├а se connecter</string>
<string name="simplex_link_invitation">Invitation unique SimpleX</string>
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode">Liens SimpleX</string>
<string name="simplex_link_mode_description">Description</string>
<string name="error_deleting_contact">Erreur lors de la suppression du contact</string>
<string name="error_joining_group">Erreur lors de la liaison avec le groupe</string>
<string name="sender_cancelled_file_transfer">L\'exp├йditeur a annul├й le transfert de fichiers.</string>
<string name="deleted_description">supprim├й</string>
<string name="marked_deleted_description">supprim├й</string>
<string name="unknown_message_format">format de message inconnu</string>
<string name="display_name_connecting">connexionтАж</string>
<string name="description_you_shared_one_time_link_incognito">vous avez partag├й un lien unique en incognito</string>
<string name="description_via_group_link">via le lien de groupe</string>
<string name="description_via_contact_address_link">via le lien d\'adresse du contact</string>
<string name="description_via_contact_address_link_incognito">mode incognito via le lien d\'adresse du contact</string>
<string name="simplex_link_group">Lien de groupe SimpleX</string>
<string name="simplex_link_mode_browser">Via navigateur</string>
<string name="simplex_link_mode_browser_warning">Ouvrir le lien dans le navigateur peut r├йduire la confidentialit├й et la s├йcurit├й de la connexion. Les liens SimpleX non fiables seront en rouge.</string>
<string name="network_error_desc">V├йrifiez votre connexion r├йseau avec <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> et r├йessayez.</string>
<string name="error_receiving_file">Erreur lors de la r├йception du fichier</string>
<string name="sender_may_have_deleted_the_connection_request">L\'exp├йditeur a peut-├кtre supprim├й la demande de connexion.</string>
<string name="connected_to_server_to_receive_messages_from_contact">Vous ├кtes connect├й┬╖e au serveur utilis├й pour recevoir les messages de ce contact.</string>
<string name="sending_files_not_yet_supported">l\'envoi de fichiers n\'est pas encore support├й</string>
<string name="sender_you_pronoun">vous</string>
<string name="description_via_group_link_incognito">mode incognito via le lien de groupe</string>
<string name="simplex_link_contact">Adresse de contact SimpleX</string>
<string name="trying_to_connect_to_server_to_receive_messages">Tentative de connexion au serveur utilis├й pour recevoir les messages de ce contact.</string>
<string name="receiving_files_not_yet_supported">la r├йception de fichiers n\'est pas encore support├йe</string>
<string name="connection_local_display_name">connexion <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="description_you_shared_one_time_link">vous avez partag├й un lien unique</string>
<string name="description_via_one_time_link">via un lien unique</string>
<string name="description_via_one_time_link_incognito">mode incognito via un lien unique</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs SMP sont au bon format, s├йpar├йes par des lignes et ne sont pas dupliqu├йes.</string>
<string name="error_setting_network_config">Erreur lors de la mise ├а jour de la configuration r├йseau</string>
<string name="error_creating_address">Erreur lors de la cr├йation de l\'adresse</string>
<string name="contact_already_exists">Contact d├йj├а existant</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Veuillez v├йrifier que vous avez utilis├й le bon lien ou demandez ├а votre contact de vous en envoyer un autre.</string>
<string name="connection_error">Erreur de connexion</string>
<string name="error_adding_members">Erreur lors de l\'ajout de membre┬╖s</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentative de connexion au serveur utilis├й pour recevoir les messages de ce contact (erreur : <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="invalid_message_format">format de message invalide</string>
<string name="simplex_link_mode_full">Lien entier</string>
<string name="error_saving_smp_servers">Erreur lors de la sauvegarde des serveurs SMP</string>
<string name="cannot_receive_file">Impossible de recevoir le fichier</string>
<string name="invalid_connection_link">Lien de connection invalide</string>
<string name="connection_timeout">D├йlai de connexion</string>
<string name="error_sending_message">Erreur lors de l\'envoi du message</string>
<string name="you_are_already_connected_to_vName_via_this_link">Vous ├кtes d├йj├а connect├й ├а <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="connection_error_auth">Erreur de connexion (AUTH)</string>
<string name="connection_error_auth_desc">A moins que votre contact ait supprim├й la connexion ou que ce lien ait d├йj├а ├йt├й utilis├й, il peut s\'agir d\'un bug - veuillez le signaler.
\nPour vous connecter, veuillez demander ├а votre contact de cr├йer un autre lien de connexion et v├йrifiez que vous disposez d\'une connexion r├йseau stable.</string>
<string name="error_accepting_contact_request">Erreur de validation de la demande de contact</string>
<string name="error_deleting_group">Erreur lors de la suppression du groupe</string>
<string name="error_deleting_contact_request">Erreur lors de la suppression du contact</string>
<string name="error_deleting_pending_contact_connection">Erreur lors de la suppression de la connexion en attente</string>
<string name="error_changing_address">Erreur de changement d\'adresse</string>
<string name="error_smp_test_failed_at_step">├Йchec du test ├а l\'├йtape %s.</string>
<string name="error_smp_test_certificate">Il est possible que l\'empreinte du certificat dans l\'adresse du serveur soit incorrecte</string>
<string name="smp_server_test_connect">Se connecter</string>
<string name="smp_server_test_create_queue">Cr├йer une file d\'attente</string>
<string name="smp_server_test_secure_queue">File d\'attente s├йcuris├йe</string>
<string name="smp_server_test_delete_queue">Supprimer la file d\'attente</string>
<string name="smp_server_test_disconnect">Se d├йconnecter</string>
<string name="icon_descr_instant_notifications">Notifications instantan├йes</string>
<string name="service_notifications">Notifications instantan├йes !</string>
<string name="service_notifications_disabled">Les notifications instantan├йes sont d├йsactiv├йes !</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Il peut ├кtre d├йsactiv├й via les param├иtres</b> - les notifications seront toujours affich├йes lorsque l\'application est en cours d\'ex├йcution.</string>
<string name="turning_off_service_and_periodic">L\'optimisation de la batterie est active et d├йsactive le service de fond et les demandes p├йriodiques de nouveaux messages. Vous pouvez les r├йactiver via les param├иtres.</string>
<string name="periodic_notifications">Notifications p├йriodiques</string>
<string name="periodic_notifications_disabled">Les notifications p├йriodiques sont d├йsactiv├йes !</string>
<string name="enter_passphrase_notification_title">Une phrase secr├иte est n├йcessaire</string>
<string name="turn_off_battery_optimization">Pour l\'utiliser, veuillez <b>d├йsactiver l\'optimisation de la batterie</b> pour <xliff:g id="appName">SimpleX</xliff:g> dans la prochaine fen├кtre de dialogue. Sinon, les notifications seront d├йsactiv├йes.</string>
<string name="error_smp_test_server_auth">Le serveur requiert une autorisation pour cr├йer des files d\'attente, v├йrifiez le mot de passe</string>
<string name="periodic_notifications_desc">L\'application r├йcup├иre p├йriodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les donn├йes de votre appareil ne sont pas envoy├йes aux serveurs.</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Pour prot├йger votre vie priv├йe, au lieu des notifications push, l\'application poss├иde un <b><xliff:g id="appName">SimpleX</xliff:g> service de fond</b> - il utilise quelques pour cent de la batterie par jour.</string>
<string name="hide_notification">Cacher</string>
<string name="settings_notification_preview_mode_title">Montrer l\'aper├зu</string>
<string name="notification_preview_mode_contact">Nom du contact</string>
<string name="notification_preview_somebody">Contact masqu├йтАп:</string>
<string name="notification_preview_new_message">nouveau message</string>
<string name="notification_new_contact_request">Nouvelle demande de contact</string>
<string name="notification_contact_connected">Connect├й</string>
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
<string name="la_notice_turn_on">Activer</string>
<string name="auth_simplex_lock_turned_on">SimpleX Lock activ├й</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Il vous sera demand├й de vous authentifier lorsque vous d├йmarrez ou reprenez l\'application apr├иs 30 secondes en arri├иre-plan.</string>
<string name="auth_unlock">D├йverrouiller</string>
<string name="auth_enable_simplex_lock">Activer SimpleX Lock</string>
<string name="auth_disable_simplex_lock">D├йsactiver SimpleX Lock</string>
<string name="auth_unavailable">Authentification indisponible</string>
<string name="auth_device_authentication_is_disabled_turning_off">L\'authentification de l\'appareil est d├йsactiv├йe. D├йsactivation de SimpleX Lock.</string>
<string name="auth_open_chat_console">Ouvrir la console du chat</string>
<string name="message_delivery_error_title">Erreur de distribution du message</string>
<string name="message_delivery_error_desc">Il est fort probable que ce contact ait supprim├й la connexion avec vous.</string>
<string name="reply_verb">R├йpondre</string>
<string name="share_verb">Partager</string>
<string name="copy_verb">Copier</string>
<string name="delete_verb">Supprimer</string>
<string name="save_verb">Sauvegarder</string>
<string name="edit_verb">Modifier</string>
<string name="reveal_verb">R├йv├йler</string>
<string name="hide_verb">Cacher</string>
<string name="allow_verb">Autoriser</string>
<string name="delete_message__question">Supprimer le messageтАп\?</string>
<string name="for_me_only">Supprimer pour moi</string>
<string name="your_chats">Vos chats</string>
<string name="notification_preview_mode_message">Texte du message</string>
<string name="notification_preview_mode_hidden">Cach├й</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_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.</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">L\'authentification de l\'appareil n\'est pas activ├йe. Vous pouvez activer SimpleX Lock via Param├иtres, une fois que vous avez activ├й l\'authentification de l\'appareil.</string>
<string name="database_initialization_error_desc">La base de donn├йes ne fonctionne pas correctement. Appuyez ici pour en savoir plus.</string>
<string name="ntf_channel_calls">Appels SimpleX Chat</string>
<string name="ntf_channel_messages">Messages SimpleX Chat</string>
<string name="settings_notifications_mode_title">Service de notification</string>
<string name="notifications_mode_periodic">Lancer p├йriodiquement</string>
<string name="notifications_mode_off">Ex├йcuter lorsque lтАЩapp est ouverte</string>
<string name="notifications_mode_service">Toujours activ├й</string>
<string name="failed_to_parse_chat_title">├Йchec du chargement du chat</string>
<string name="failed_to_parse_chats_title">├Йchec du chargement des chats</string>
<string name="contact_developers">Veuillez mettre ├а jour lтАЩapp et contacter les d├йveloppeurs.</string>
<string name="simplex_service_notification_text">R├йcup├йration des messagesтАж</string>
<string name="settings_notification_preview_title">Aper├зu de notification</string>
<string name="database_initialization_error_title">├Йchec dтАЩinitialisation de la base de donn├йes</string>
<string name="enter_passphrase_notification_desc">Pour recevoir des notifications, veuillez entrer la phrase secr├иte de la base de donn├йes</string>
<string name="simplex_service_notification_title">service <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="this_text_is_available_in_settings">Ce texte est disponible dans les param├иtres</string>
<string name="group_preview_join_as">rejoindre en tant que %s</string>
<string name="group_preview_you_are_invited">vous ├кtes invit├й┬╖e au groupe</string>
<string name="chat_with_developers">Discuter avec les d├йveloppeurs</string>
<string name="tap_to_start_new_chat">Appuyez ici pour d├йmarrer une nouvelle discussion</string>
<string name="you_have_no_chats">Vous n\'avez aucune discussion</string>
<string name="images_limit_title">Trop dтАЩimages !</string>
<string name="share_file">Partager le fichierтАж</string>
<string name="attach">Attacher</string>
<string name="icon_descr_cancel_image_preview">Annuler lтАЩaper├зu dтАЩimage</string>
<string name="icon_descr_cancel_file_preview">Annuler lтАЩaper├зu du fichier</string>
<string name="icon_descr_sent_msg_status_send_failed">├йchec dтАЩenvoi</string>
<string name="icon_descr_received_msg_status_unread">non lu</string>
<string name="welcome">Bienvenue !</string>
<string name="contact_connection_pending">connexionтАж</string>
<string name="group_connection_pending">connexionтАж</string>
<string name="share_message">Partager le messageтАж</string>
<string name="share_image">Partager lтАЩimageтАж</string>
<string name="images_limit_desc">Envoi de 10 images en m├кme temps maximum</string>
<string name="personal_welcome">Bienvenue <xliff:g>%1$s</xliff:g> !</string>
<string name="notifications_mode_periodic_desc">V├йrifie les nouveaux messages toutes les 10 minutes pendant 1 minute au maximum.</string>
<string name="notification_preview_mode_contact_desc">Afficher uniquement le contact</string>
<string name="for_everybody">Pour tous</string>
<string name="icon_descr_sent_msg_status_sent">envoy├й</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">envoi non autoris├й</string>
<string name="icon_descr_context">Ic├┤ne contextuelle</string>
<string name="image_descr">Image</string>
<string name="image_decoding_exception_desc">L\'image ne peut pas ├кtre d├йcod├йe. Veuillez essayer une autre image ou contacter les d├йveloppeurs.</string>
<string name="icon_descr_waiting_for_image">En attente de l\'image</string>
<string name="icon_descr_asked_to_receive">Demand├й ├а recevoir l\'image</string>
<string name="icon_descr_image_snd_complete">Image envoy├йe</string>
<string name="waiting_for_image">En attente de l\'image</string>
<string name="image_saved">Image enregistr├йe dans la photot├иque</string>
<string name="icon_descr_file">Fichier</string>
<string name="large_file">Fichier trop lourd !</string>
<string name="file_saved">Fichier sauvegard├й</string>
<string name="file_not_found">Fichier introuvable</string>
<string name="error_saving_file">Erreur lors de la sauvegarde du fichier</string>
<string name="delete_contact_question">Supprimer le contact \?</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Le contact et tous les messages seront supprim├йs - impossible de revenir en arri├иre !</string>
<string name="button_delete_contact">Supprimer le contact</string>
<string name="icon_descr_server_status_connected">Connect├й</string>
<string name="icon_descr_send_message">Envoyer un message</string>
<string name="switch_receiving_address_question">Changement d\'adresse de r├йception \?</string>
<string name="icon_descr_record_voice_message">Enregistrer un message vocal</string>
<string name="allow_voice_messages_question">Autoriser les messages vocaux \?</string>
<string name="you_need_to_allow_to_send_voice">Vous devez autoriser votre contact ├а envoyer des messages vocaux pour pouvoir en envoyer.</string>
<string name="voice_messages_prohibited">Messages vocaux interdits !</string>
<string name="ask_your_contact_to_enable_voice">Veuillez demander ├а votre contact de permettre l\'envoi de messages vocaux.</string>
<string name="cancel_verb">Annuler</string>
<string name="only_group_owners_can_enable_voice">Seuls les propri├йtaires de groupes peuvent activer les messages vocaux.</string>
<string name="back">Retour</string>
<string name="no_details">aucun d├йtail</string>
<string name="add_contact">Lien d\'invitation unique</string>
<string name="copied">Copi├й dans le presse-papiers</string>
<string name="share_one_time_link">Cr├йer un lien d\'invitation unique</string>
<string name="add_contact_or_create_group">Commencer une nouvelle discussion</string>
<string name="connect_via_link_or_qr">Se connecter via un lien / code QR</string>
<string name="scan_QR_code">Scanner un code QR</string>
<string name="to_share_with_your_contact">(├а partager avec votre contact)</string>
<string name="create_group">Cr├йer un groupe secret</string>
<string name="from_gallery_button">Depuis la Photot├иque</string>
<string name="choose_file">Choisir le fichier</string>
<string name="to_start_a_new_chat_help_header">Pour d├йmarrer une nouvelle discussion</string>
<string name="chat_help_tap_button">Appuyez sur le bouton</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scanner un code QR</b> : pour vous connecter ├а votre contact qui vous montre un code QR.</string>
<string name="to_connect_via_link_title">Pour se connecter via un lien</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si vous avez re├зu un lien d\'invitation <xliff:g id="appName">SimpleX Chat</xliff:g>, vous pouvez l\'ouvrir dans votre navigateur :</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">ЁЯТ╗ bureau : scanner le code QR affich├й depuis l\'app, via <b>Scanner le code QR</b>.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">ЁЯУ▒ mobile : appuyez sur <b>Ouvrir dans l\'application</b>, puis appuyez sur <b>se connecter</b> dans l\'app.</string>
<string name="accept_contact_incognito_button">Accepter en incognito</string>
<string name="reject_contact_button">Rejeter</string>
<string name="clear_chat_question">Effacer la conversation \?</string>
<string name="clear_chat_warning">Tous les messages seront supprim├йs - impossible de revenir en arri├иre ! Les messages seront supprim├йs UNIQUEMENT pour vous.</string>
<string name="clear_chat_menu_action">Effacer</string>
<string name="delete_contact_menu_action">Supprimer</string>
<string name="delete_group_menu_action">Supprimer</string>
<string name="mark_read">Marquer comme lu</string>
<string name="mark_unread">Marquer non lu</string>
<string name="set_contact_name">D├йfinir le nom du contact</string>
<string name="you_invited_your_contact">Vous avez invit├й votre contact</string>
<string name="you_accepted_connection">Vous avez accept├й la connexion</string>
<string name="delete_pending_connection__question">Supprimer la connexion en attente \?</string>
<string name="connection_you_accepted_will_be_cancelled">La connexion que vous avez accept├йe sera annul├йe !</string>
<string name="alert_title_contact_connection_pending">Le contact n\'est pas encore connect├й !</string>
<string name="icon_descr_close_button">Bouton fermer</string>
<string name="image_descr_profile_image">image de profil</string>
<string name="image_descr_link_preview">image d\'aper├зu du lien</string>
<string name="icon_descr_cancel_link_preview">annuler l\'aper├зu du lien</string>
<string name="icon_descr_settings">Param├иtres</string>
<string name="icon_descr_address">Adresse <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_help">aide</string>
<string name="icon_descr_simplex_team">├Йquipe <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_more_button">Plus</string>
<string name="show_QR_code">Afficher le code QR</string>
<string name="invalid_QR_code">Code QR invalide</string>
<string name="this_QR_code_is_not_a_link">Ce code QR n\'est pas un lien !</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Vous serez connect├й┬╖e lorsque votre demande de connexion sera accept├йe, veuillez attendre ou v├йrifier plus tard !</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Vous serez connect├й┬╖e lorsque l\'appareil de votre contact sera en ligne, veuillez attendre ou v├йrifier plus tard !</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Votre contact peut scanner le code QR depuis l\'app.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Votre profil de chat sera envoy├й
\n├а votre contact</string>
<string name="share_invitation_link">Partager le lien d\'invitation</string>
<string name="your_profile_will_be_sent">Votre profil de chat sera envoy├й ├а votre contact</string>
<string name="paste_button">Coller</string>
<string name="this_string_is_not_a_connection_link">Cette cha├оne n\'est pas un lien de connexion !</string>
<string name="you_can_also_connect_by_clicking_the_link">Vous pouvez aussi vous connecter en cliquant sur le lien. Si il s\'ouvre dans le navigateur, cliquez sur <b>Ouvrir dans l\'app mobile</b>.</string>
<string name="create_one_time_link">Cr├йer un lien d\'invitation unique</string>
<string name="text_field_set_contact_placeholder">D├йfinir le nom du contactтАж</string>
<string name="icon_descr_server_status_disconnected">D├йconnect├й</string>
<string name="icon_descr_server_status_error">Erreur</string>
<string name="icon_descr_server_status_pending">En attente</string>
<string name="accept_connection_request__question">Accepter la demande de connexion \?</string>
<string name="clear_verb">Effacer</string>
<string name="clear_chat_button">Effacer la conversation</string>
<string name="paste_connection_link_below_to_connect">Collez le lien que vous avez re├зu dans le cadre ci-dessous pour vous connecter avec votre contact.</string>
<string name="connect_via_link">Se connecter via un lien</string>
<string name="clear_verification">Retirer la v├йrification</string>
<string name="one_time_link">Lien d\'invitation unique</string>
<string name="your_contact_address">Votre adresse de contact</string>
<string name="scan_code">Scanner le code</string>
<string name="incorrect_code">Code de s├йcurit├й incorrect !</string>
<string name="security_code">Code de s├йcurit├й</string>
<string name="mark_code_verified">Marquer comme v├йrifi├й</string>
<string name="view_security_code">Afficher le code de s├йcurit├й</string>
<string name="verify_security_code">V├йrifier le code de s├йcurit├й</string>
<string name="confirm_verb">Confirmer</string>
<string name="reset_verb">R├йinitialisation</string>
<string name="ok">OK</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scanner ou coller depuis le presse-papiers)</string>
<string name="only_stored_on_members_devices">(uniquement stock├й par les membres du groupe)</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Votre contact a besoin d\'├кtre en ligne pour completer la connexion.
\nVous pouvez annuler la connexion et supprimer le contact (et r├йessayer plus tard avec un autre lien).</string>
<string name="contact_wants_to_connect_with_you">veut ├йtablir une connexion !</string>
<string name="icon_descr_profile_image_placeholder">image de profil (placeholder)</string>
<string name="image_descr_qr_code">Code QR</string>
<string name="image_descr_simplex_logo">Logo <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_email">E-mail</string>
<string name="connect_button">Se connecter</string>
<string name="notifications_mode_off_desc">L\'application peut recevoir des notifications uniquement lorsqu\'elle est en cours d\'ex├йcution, aucun service d\'arri├иre-plan ne sera lanc├й.</string>
<string name="notifications_mode_service_desc">Le service d\'arri├иre-plan fonctionne en permanence. Les notifications s\'affichent d├иs que les messages sont disponibles.</string>
<string name="notification_preview_mode_message_desc">Afficher le contact et le message</string>
<string name="notification_display_mode_hidden_desc">Masquer le contact et le message</string>
<string name="auth_log_in_using_credential">Connectez-vous en utilisant votre identifiant</string>
<string name="auth_confirm_credential">Confirmez vos identifiants</string>
<string name="auth_stop_chat">Arr├кter le chat</string>
<string name="delete_message_cannot_be_undone_warning">Le message sera supprim├й - impossible de revenir en arri├иre !</string>
<string name="delete_message_mark_deleted_warning">Le message sera marqu├й comme supprim├й. Le┬╖s destinataire┬╖s pourrai┬╖ent r├йv├йler ce message.</string>
<string name="icon_descr_edited">modifi├й</string>
<string name="image_will_be_received_when_contact_is_online">L\'image sera re├зue quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tardтАп!</string>
<string name="image_decoding_exception_title">Erreur de d├йcodage</string>
<string name="contact_sent_large_file">Votre contact a envoy├й un fichier dont la taille est sup├йrieure ├а la taille maximale actuellement prise en charge (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
<string name="waiting_for_file">En attente du fichier</string>
<string name="voice_message">Message vocal</string>
<string name="toast_permission_denied">Autorisation refus├йe !</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="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="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="accept_contact_button">Accepter</string>
<string name="mute_chat">Muet</string>
<string name="unmute_chat">D├йmute</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Le contact avec lequel vous avez partag├й ce lien NE pourra PAS se connecter !</string>
<string name="invalid_contact_link">Lien invalide !</string>
<string name="this_link_is_not_a_valid_connection_link">Ce lien n\'est pas un lien de connexion valide !</string>
<string name="connection_request_sent">Demande de connexion envoy├йe !</string>
<string name="file_will_be_received_when_contact_is_online">Le fichier sera re├зu quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tardтАп!</string>
<string name="voice_message_send_text">Message vocalтАж</string>
<string name="maximum_supported_file_size">La taille maximale support├йs des fichiers actuellement est de <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="voice_message_with_duration">Message vocal (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="notifications">Notifications</string>
<string name="switch_receiving_address_desc">Cette fonctionnalit├й est exp├йrimentale ! Elle ne fonctionnera que si l\'autre client a la version 4.2 install├йe. Vous devriez voir le message dans la conversation une fois le changement d\'adresse effectu├й. V├йrifiez que vous pouvez toujours recevoir des messages de ce contact (ou membre du groupe).</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Vous serez connect├й┬╖e au groupe lorsque l\'appareil de l\'h├┤te sera en ligne, veuillez attendre ou v├йrifier plus tard !</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Si vous ne pouvez pas vous rencontrer en personne, <b>montrez le code QR lors d\'un appel vid├йo</b>, ou partagez le lien.</string>
<string name="scan_code_from_contacts_app">Scannez le code de s├йcurit├й depuis l\'application de votre contact.</string>
<string name="to_verify_compare">Pour v├йrifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils.</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Si vous ne pouvez pas vous rencontrer en personne, vous pouvez <b>scanner un code QR lors d\'un appel vid├йo</b>, ou votre contact peut partager un lien d\'invitation.</string>
<string name="smp_servers_add">Ajouter un serveurтАж</string>
<string name="markdown_in_messages">Markdown dans les messages</string>
<string name="smp_servers_preset_add">Ajouter des serveurs pr├йd├йfinis</string>
<string name="use_simplex_chat_servers__question">Utiliser les serveurs <xliff:g id="appNameFull">SimpleX Chat</xliff:g> \?</string>
<string name="smp_servers_delete_server">Supprimer le serveur</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliqu├йes, un par ligne.</string>
<string name="network_enable_socks_info">Acc├йder aux serveurs via un proxy SOCKS sur le port 9050 \? Le proxy doit ├кtre d├йmarr├й avant d\'activer cette option.</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_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> les messages de vos contacts.</string>
<string name="your_settings">Vos param├иtres</string>
<string name="chat_lock">SimpleX Lock</string>
<string name="chat_console">Console du chat</string>
<string name="smp_servers">Serveurs SMP</string>
<string name="smp_servers_test_servers">Tester les serveurs</string>
<string name="smp_servers_save">Sauvegarder les serveurs</string>
<string name="smp_servers_scan_qr">Scanner un code QR de serveur</string>
<string name="smp_servers_use_server">Utiliser ce serveur</string>
<string name="smp_servers_use_server_for_new_conn">Utiliser pour les nouvelles connexions</string>
<string name="smp_servers_add_to_another_device">Ajouter ├а un autre appareil</string>
<string name="install_simplex_chat_for_terminal">Installer <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour terminal</string>
<string name="star_on_github">Star sur GitHub</string>
<string name="contribute">Contribuer</string>
<string name="rate_the_app">├Йvaluer l\'app</string>
<string name="your_SMP_servers">Vos serveurs SMP</string>
<string name="how_to_use_your_servers">Comment utiliser vos serveurs</string>
<string name="saved_ICE_servers_will_be_removed">Les serveurs WebRTC ICE sauvegard├йs seront supprim├йs.</string>
<string name="your_ICE_servers">Vos serveurs ICE</string>
<string name="configure_ICE_servers">Configurer les serveurs ICE</string>
<string name="network_settings">Param├иtres r├йseau avanc├йs</string>
<string name="network_settings_title">Param├иtres r├йseau</string>
<string name="network_socks_toggle">Utiliser un proxy SOCKS (port 9050)</string>
<string name="network_enable_socks">Utiliser un proxy SOCKS \?</string>
<string name="network_disable_socks">Utiliser une connexion Internet directe \?</string>
<string name="network_disable_socks_info">Si vous confirmez, les serveurs de messagerie seront en mesure de voir votre adresse IP, votre fournisseur ainsi que les serveurs auxquels vous vous connectez.</string>
<string name="network_use_onion_hosts_no">Non</string>
<string name="network_use_onion_hosts_required">Requis</string>
<string name="network_use_onion_hosts_prefer_desc">Les h├┤tes .onion seront utilis├йs lorsqu\'ils sont disponibles.</string>
<string name="appearance_settings">Apparence</string>
<string name="create_address">Cr├йer une adresse</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Vous pouvez partager votre adresse sous forme de lien ou de code QR - n\'importe qui pourra se connecter ├а vous. Vous ne perdrez pas vos contacts si vous la supprimez par la suite.</string>
<string name="your_current_profile">Votre profil de chat</string>
<string name="edit_image">Modifier l\'image</string>
<string name="save_and_notify_contacts">Sauvegarder et notifier les contacts</string>
<string name="save_and_notify_group_members">Sauvegarder et en informer les membres du groupe</string>
<string name="your_profile_is_stored_on_your_device">Votre profil, vos contacts et les messages re├зus sont stock├йs sur votre appareil.</string>
<string name="profile_is_only_shared_with_your_contacts">Le profil n\'est partag├й qu\'avec vos contacts.</string>
<string name="display_name_cannot_contain_whitespace">Le nom d\'affichage ne peut pas contenir d\'espace.</string>
<string name="full_name_optional__prompt">Nom complet (optionnel)</string>
<string name="create_profile_button">Cr├йer</string>
<string name="about_simplex">├А propos de SimpleX</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Vous pouvez utiliser le format markdown pour mettre en forme les messages :</string>
<string name="bold">gras</string>
<string name="italic">italique</string>
<string name="strikethrough">barr├й</string>
<string name="callstatus_accepted">appel accept├й</string>
<string name="callstatus_connecting">connexion ├а l\'appelтАж</string>
<string name="callstatus_error">erreur d\'appel</string>
<string name="callstate_received_answer">r├йponse re├зuтАж</string>
<string name="callstate_received_confirmation">confimation re├зuтАж</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="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="paste_the_link_you_received">Coller le lien re├зu</string>
<string name="use_chat">Utiliser le chat</string>
<string name="onboarding_notifications_mode_title">Notifications priv├йes</string>
<string name="onboarding_notifications_mode_subtitle">Peut ├кtre modifi├й ult├йrieurement via les param├иtres.</string>
<string name="onboarding_notifications_mode_off">Quand l\'application fonctionne</string>
<string name="onboarding_notifications_mode_periodic">P├йriodique</string>
<string name="onboarding_notifications_mode_service">Instantan├йe</string>
<string name="onboarding_notifications_mode_off_desc"><b>├Йconomie de batterie</b>. Vous recevrez des notifications uniquement lorsque l\'application est en cours d\'ex├йcution, le service de fond ne sera PAS utilis├й.</string>
<string name="about_simplex_chat">├А propos de <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">Comment l\'utiliser</string>
<string name="markdown_help">Aide Markdown</string>
<string name="save_servers_button">Sauvegarder</string>
<string name="network_and_servers">R├йseau et serveurs</string>
<string name="save_and_notify_contact">Sauvegarder et en informer les contacts</string>
<string name="exit_without_saving">Quitter sans sauvegarder</string>
<string name="callstatus_rejected">appel rejet├й</string>
<string name="callstatus_in_progress">appel en cours</string>
<string name="callstatus_ended">appel termin├й <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstate_starting">lancementтАж</string>
<string name="is_verified">%s est v├йrifi├й┬╖e</string>
<string name="is_not_verified">%s n\'est pas v├йrifi├й┬╖e</string>
<string name="your_simplex_contact_address">Votre adresse de contact <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="database_passphrase_and_export">Phrase secr├иte et exportation de la base de donn├йes</string>
<string name="chat_with_the_founder">Envoyez vos questions et id├йes</string>
<string name="send_us_an_email">Envoyez nous un e-mail</string>
<string name="smp_servers_preset_address">Adresse du serveur pr├йd├йfinie</string>
<string name="smp_servers_test_server">Tester le serveur</string>
<string name="smp_servers_test_failed">├Йchec du test du serveur !</string>
<string name="smp_servers_test_some_failed">Certains serveurs n\'ont pas r├йussi le test :</string>
<string name="smp_servers_enter_manually">Entrer un serveur manuellement</string>
<string name="smp_servers_preset_server">Serveur pr├йd├йfini</string>
<string name="smp_servers_your_server">Votre serveur</string>
<string name="smp_servers_your_server_address">Votre adresse de serveur</string>
<string name="smp_servers_invalid_address">Adresse de serveur invalide !</string>
<string name="smp_servers_check_address">V├йrifiez l\'adresse du serveur et r├йessayez.</string>
<string name="using_simplex_chat_servers">Utilise les serveurs <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
<string name="how_to">Comment faire</string>
<string name="enter_one_ICE_server_per_line">Serveurs ICE (un par ligne)</string>
<string name="error_saving_ICE_servers">Erreur lors de la sauvegarde des serveurs ICE</string>
<string name="update_onion_hosts_settings_question">Mettre ├а jour le param├иtre des h├┤tes .onion \?</string>
<string name="network_use_onion_hosts_prefer">Quand disponible</string>
<string name="network_use_onion_hosts_no_desc">Les h├┤tes .onion ne seront pas utilis├йs.</string>
<string name="network_use_onion_hosts_required_desc">Les h├┤tes .onion seront n├йcessaires pour la connexion.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Les h├┤tes .onion ne seront pas utilis├йs.</string>
<string name="delete_address__question">Supprimer l\'adresse \?</string>
<string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connect├йs.</string>
<string name="share_link">Partager le lien</string>
<string name="delete_address">Supprimer l\'adresse</string>
<string name="contact_requests">Demandes de contact</string>
<string name="accept_requests">Accepter les demandes</string>
<string name="accept_automatically">Automatiquement</string>
<string name="section_title_welcome_message">MESSAGE DE BIENVENUE</string>
<string name="display_name__field">Nom affich├й :</string>
<string name="full_name__field">Nom complet :</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stock├й sur votre appareil et partag├й uniquement avec vos contacts.
\n
\nLes serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
<string name="delete_image">Supprimer l\'image</string>
<string name="save_preferences_question">Sauvegarder les pr├йf├йrences \?</string>
<string name="you_control_your_chat">Vous ma├оtrisez vos discussions !</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plateforme de messagerie et d\'applications qui prot├иge votre vie priv├йe et votre s├йcurit├й.</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Nous ne stockons aucun de vos contacts ou messages (une fois d├йlivr├йs) sur les serveurs.</string>
<string name="create_profile">Cr├йer le profil</string>
<string name="display_name">Nom affich├й</string>
<string name="how_to_use_markdown">Comment utiliser markdown</string>
<string name="a_plus_b">a + b</string>
<string name="colored">color├й</string>
<string name="secret">secret</string>
<string name="callstatus_calling">appelтАж</string>
<string name="callstatus_missed">appel manqu├й</string>
<string name="callstate_waiting_for_answer">en attente de r├йponseтАж</string>
<string name="callstate_waiting_for_confirmation">en attente de confirmationтАж</string>
<string name="callstate_connected">connect├й</string>
<string name="callstate_ended">termin├й</string>
<string name="next_generation_of_private_messaging">La nouvelle g├йn├йration de messagerie priv├йe</string>
<string name="privacy_redefined">La vie priv├йe red├йfinie</string>
<string name="first_platform_without_user_ids">La 1├иre plateforme sans aucun identifiant d\'utilisateur тАУ priv├йe par design.</string>
<string name="immune_to_spam_and_abuse">Prot├йg├й du spam et des abus</string>
<string name="people_can_connect_only_via_links_you_share">On ne peut se connecter ├а vous quтАЩavec les liens que vous partagez.</string>
<string name="decentralized">D├йcentralis├й</string>
<string name="create_your_profile">Cr├йez votre profil</string>
<string name="make_private_connection">├Йtablir une connexion priv├йe</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="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="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_service_desc"><b>Batterie plus utilis├йe </b> ! Le service de fond est toujours en cours d\'ex├йcution - les notifications s\'afficheront d├иs que les messages seront disponibles.</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> messageт╕▒s manqu├йт╕▒s</string>
<string name="integrity_msg_bad_id">ID de message incorrecte</string>
<string name="settings_section_title_settings">PARAM├ИTRES</string>
<string name="alert_text_skipped_messages_it_can_happen_when">C\'est possible quand :
\n1. Les messages expirent du serveur (apr├иs 30 jours si ils ne sont pas re├зu).
\n2. Le serveur que vous utilisez pour recevoir les messages de ce contact a ├йt├й mise ├а jour ou red├йmarr├й.
\n3. La connection est compromise.
\nVeuillez vous connecter aux d├йveloppeurs via les Param├иtres pour recevoir les mises ├а jour concernant les serveurs.
\nNous allons ajouter une redondance des serveurs pour ├йviter la perte de messages.</string>
<string name="icon_descr_call_rejected">Appel rejet├й</string>
<string name="rcv_group_event_member_deleted">a retir├й <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_member_deleted">vous avez retir├й <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_invited_via_your_group_link">invit├й par votre lien de groupe</string>
<string name="snd_conn_event_switch_queue_phase_completed">vous avez chang├й d\'adresse</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Arr├кtez le chat pour exporter, importer ou supprimer la base de donn├йes du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arr├кt├й.</string>
<string name="enable_automatic_deletion_message">Cette action ne peut ├кtre annul├йe - les messages envoy├йs et re├зus avant la date s├йlectionn├йe seront supprim├йs. Cela peut prendre plusieurs minutes.</string>
<string name="encrypted_with_random_passphrase">La base de donn├йes est chiffr├йe ├а l\'aide d\'une phrase secr├иte al├йatoire, que vous pouvez modifier.</string>
<string name="restore_database">Restaurer la sauvegarde de la base de donn├йes</string>
<string name="restore_passphrase_not_found_desc">La phrase secr├иte n\'a pas ├йt├й trouv├йe dans le Keystore, veuillez la saisir manuellement. Cela a pu se produire si vous avez restaur├й les donn├йes de l\'app ├а l\'aide d\'un outil de sauvegarde. Si ce n\'est pas le cas, veuillez contacter les d├йveloppeurs.</string>
<string name="restore_database_alert_desc">Veuillez entrer le mot de passe pr├йc├йdent apr├иs avoir restaur├й la sauvegarde de la base de donn├йes. Cette action ne peut pas ├кtre annul├йe.</string>
<string name="database_restore_error">Erreur de restauration de la base de donn├йes</string>
<string name="archive_created_on_ts">Cr├й├й le <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="encrypted_video_call">appel vid├йo (chiffrement de bout en bout)</string>
<string name="audio_call_no_encryption">appel audio (sans chiffrement)</string>
<string name="encrypted_audio_call">appel audio (chiffrement de bout en bout)</string>
<string name="accept">Accepter</string>
<string name="reject">Rejeter</string>
<string name="icon_descr_video_call">appel vid├йo</string>
<string name="icon_descr_audio_call">appel audio</string>
<string name="accept_call_on_lock_screen">Accepter</string>
<string name="allow_accepting_calls_from_lock_screen">Activer les appels depuis l\'├йcran verrouill├й via les Param├иtres.</string>
<string name="open_verb">Ouvrir</string>
<string name="call_connection_via_relay">via relais</string>
<string name="icon_descr_hang_up">Raccrocher</string>
<string name="icon_descr_video_on">Vid├йo ON</string>
<string name="icon_descr_video_off">Vid├йo OFF</string>
<string name="icon_descr_call_progress">Appel en cours</string>
<string name="icon_descr_call_ended">Appel termin├й</string>
<string name="your_privacy">Votre vie priv├йe</string>
<string name="settings_section_title_device">APPAREIL</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="settings_developer_tools">Outils du d├йveloppeur</string>
<string name="settings_section_title_icon">ICONE DE L\'APP</string>
<string name="your_chat_database">Votre base de donn├йes de chat</string>
<string name="run_chat_section">LANCER LE CHAT</string>
<string name="stop_chat_question">Arr├кter le chat \?</string>
<string name="restart_the_app_to_use_imported_chat_database">Red├йmarrez l\'application pour utiliser la base de donn├йes de chat import├йe.</string>
<string name="chat_item_ttl_day">1 jour</string>
<string name="delete_messages">Supprimer les messages</string>
<string name="save_passphrase_in_keychain">Sauvegarder la phrase secr├иte dans le keystore</string>
<string name="database_encrypted">Base de donn├йes chiffr├йe !</string>
<string name="error_encrypting_database">Erreur lors du chiffrement de la base de donn├йes</string>
<string name="update_database">Mise ├а jour</string>
<string name="encrypt_database">Chiffrer</string>
<string name="enter_correct_current_passphrase">Veuillez entrer la phrase secr├иte actuelle correcte.</string>
<string name="database_is_not_encrypted">Votre base de donn├йes de chat n\'est pas chiffr├йe - d├йfinissez une phrase secr├иte pour la prot├йger.</string>
<string name="impossible_to_recover_passphrase"><b>Veuillez noter</b> : vous NE pourrez PAS r├йcup├йrer ou modifier la phrase secr├иte si vous la perdez.</string>
<string name="keychain_allows_to_receive_ntfs">Le Keystore d\'Android sera utilis├й pour stocker en toute s├йcurit├й la phrase secr├иte apr├иs sa modification ou red├йmarrage de l\'app - cela permettra de recevoir les notifications.</string>
<string name="store_passphrase_securely_without_recover">Veuillez conserver votre phrase secr├иte en lieu s├╗r, vous NE pourrez PAS acc├йder au chat si vous la perdez.</string>
<string name="passphrase_is_different">La phrase secr├иte de la base de donn├йes est diff├йrente de celle enregistr├йe dans le Keystore.</string>
<string name="unknown_error">Erreur inconnue</string>
<string name="enter_correct_passphrase">Entrez la phrase secr├иte correcte.</string>
<string name="alert_message_no_group">Ce groupe n\'existe plus.</string>
<string name="you_joined_this_group">Vous avez rejoint ce groupe</string>
<string name="you_rejected_group_invitation">Vous avez rejet├й l\'invitation du groupe</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">vous avez chang├й d\'adresse pour %s</string>
<string name="rcv_conn_event_switch_queue_phase_changing">changement d\'adresseтАж</string>
<string name="incoming_video_call">Appel vid├йo entrant</string>
<string name="video_call_no_encryption">appel vid├йo (sans chiffrement)</string>
<string name="ignore">Ignorer</string>
<string name="call_already_ended">Appel d├йj├а termin├й !</string>
<string name="settings_audio_video_calls">Appels audio et vid├йo</string>
<string name="status_e2e_encrypted">chiffr├й de bout en bout</string>
<string name="settings_section_title_develop">DEVELOPPER</string>
<string name="settings_experimental_features">Fonctionnalit├йs exp├йrimentales</string>
<string name="settings_section_title_socks">SOCKS PROXY</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_calls">APPELS</string>
<string name="import_database">Importer la base de donn├йes</string>
<string name="new_database_archive">Nouvelle archive de base de donn├йes</string>
<string name="old_database_archive">Archives de l\'ancienne base de donn├йes</string>
<string name="delete_database">Supprimer la base de donn├йes</string>
<string name="error_starting_chat">Erreur lors du d├йmarrage du chat</string>
<string name="import_database_confirmation">Importer</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Cette action ne peut ├кtre annul├йe - votre profil, vos contacts, vos messages et vos fichiers seront irr├йversiblement perdus.</string>
<string name="chat_database_deleted">Base de donn├йes du chat supprim├йe</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Red├йmarrez l\'application pour cr├йer un nouveau profil de chat.</string>
<string name="you_must_use_the_most_recent_version_of_database">Vous devez utiliser la version la plus r├йcente de votre base de donn├йes de chat sur un seul appareil UNIQUEMENT, sinon vous risquez de ne plus recevoir les messages de certains contacts.</string>
<string name="total_files_count_and_size">%d fichier┬╖s avec une taille totale de %s</string>
<string name="chat_item_ttl_none">jamais</string>
<string name="chat_item_ttl_week">1 semaine</string>
<string name="database_will_be_encrypted_and_passphrase_stored">La base de donn├йes sera chiffr├йe et la phrase secr├иte sera stock├йe dans le Keystore.</string>
<string name="database_encryption_will_be_updated">La phrase secr├иte de la base de donn├йes sera mise ├а jour et stock├йe dans le Keystore.</string>
<string name="database_passphrase_will_be_updated">La phrase secr├иte de la base de donn├йes sera mise ├а jour.</string>
<string name="store_passphrase_securely">Veuillez conserver votre phrase secr├иte en lieu s├╗r, vous NE pourrez PAS la changer si vous la perdez.</string>
<string name="wrong_passphrase">Mauvaise phrase secr├иte pour la base de donn├йes</string>
<string name="encrypted_database">Base de donn├йes chiffr├йe</string>
<string name="database_error">Erreur de base de donn├йes</string>
<string name="error_with_info">Erreur : %s</string>
<string name="cannot_access_keychain">Impossible d\'acc├йder au Keystore pour enregistrer le mot de passe de la base de donn├йes</string>
<string name="unknown_database_error_with_info">Erreur de base de donn├йes inconnue : %s</string>
<string name="wrong_passphrase_title">Mauvaise phrase secr├иte !</string>
<string name="leave_group_question">Quitter le groupe \?</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Vous ne recevrez plus de messages de ce groupe. L\'historique du chat sera conserv├й.</string>
<string name="icon_descr_add_members">Inviter des membres</string>
<string name="icon_descr_group_inactive">Groupe inactif</string>
<string name="alert_title_group_invitation_expired">Invitation expir├йe !</string>
<string name="alert_message_group_invitation_expired">L\'invitation du groupe n\'est plus valide, elle a ├йt├й supprim├й par l\'exp├йditeur.</string>
<string name="alert_title_no_group">Groupe introuvable !</string>
<string name="alert_title_cant_invite_contacts">Impossible d\'inviter les contacts !</string>
<string name="alert_title_cant_invite_contacts_descr">Vous utilisez un profil incognito pour ce groupe - pour ├йviter de partager votre profil principal ; inviter des contacts n\'est pas possible</string>
<string name="you_sent_group_invitation">Vous avez envoy├й une invitation de groupe</string>
<string name="rcv_group_event_member_left">a quitt├й</string>
<string name="icon_descr_speaker_on">Haut-parleur ON</string>
<string name="send_link_previews">Envoi d\'aper├зus de liens</string>
<string name="error_deleting_database">Erreur lors de la suppression de la base de donn├йes du chat</string>
<string name="error_stopping_chat">Erreur lors de l\'arr├кt du chat</string>
<string name="error_exporting_chat_database">Erreur lors de l\'exportation de la base de donn├йes du chat</string>
<string name="import_database_question">Importer la base de donn├йes du chat \?</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Votre base de donn├йes de chat actuelle sera SUPPRIM├ЙE et REMPLAC├ЙE par celle qui a ├йt├й import├йe.
\nCette action ne peut ├кtre annul├йe - votre profil, vos contacts, vos messages et vos fichiers seront irr├йm├йdiablement perdus.</string>
<string name="enter_passphrase">Entrez la phrase secr├иteтАж</string>
<string name="incoming_audio_call">Appel audio entrant</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> veut se connecter ├а vous via</string>
<string name="your_calls">Vos appels</string>
<string name="connect_calls_via_relay">Se connecter via relais</string>
<string name="call_on_lock_screen">Appels en ├йcran verrouill├й :</string>
<string name="show_call_on_lock_screen">Montrer</string>
<string name="no_call_on_lock_screen">D├йsactiver</string>
<string name="your_ice_servers">Vos serveurs ICE</string>
<string name="webrtc_ice_servers">Serveurs WebRTC ICE</string>
<string name="open_simplex_chat_to_accept_call">Ouvrez <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour d├йcrocher</string>
<string name="status_no_e2e_encryption">sans chiffrement de bout en bout</string>
<string name="status_contact_has_e2e_encryption">Ce contact a le chiffrement de bout en bout</string>
<string name="status_contact_has_no_e2e_encryption">Ce contact n\'a pas le chiffrement de bout en bout</string>
<string name="call_connection_peer_to_peer">pair-├а-pair</string>
<string name="icon_descr_audio_off">Audio OFF</string>
<string name="icon_descr_audio_on">Audio ON</string>
<string name="icon_descr_speaker_off">Haut-parleur OFF</string>
<string name="icon_descr_flip_camera">Retourner la cam├йra</string>
<string name="icon_descr_call_pending_sent">Appel en suspend</string>
<string name="icon_descr_call_missed">Appel manqu├й</string>
<string name="icon_descr_call_connecting">Appel en connexion</string>
<string name="answer_call">R├йpondre ├а l\'appel</string>
<string name="integrity_msg_bad_hash">hash de message incorrect</string>
<string name="integrity_msg_duplicate">message dupliqu├й</string>
<string name="alert_title_skipped_messages">Messages manqu├йs</string>
<string name="privacy_and_security">Vie priv├йe et s├йcurit├й</string>
<string name="protect_app_screen">Prot├йger l\'├йcran de l\'app</string>
<string name="auto_accept_images">Images auto-accept├йes</string>
<string name="full_backup">Sauvegarde des donn├йes de l\'app</string>
<string name="settings_section_title_you">VOUS</string>
<string name="settings_section_title_help">AIDE</string>
<string name="settings_section_title_support">SOUTENEZ SIMPLEX CHAT</string>
<string name="settings_section_title_incognito">Mode Incognito</string>
<string name="chat_is_running">Le chat est en cours d\'ex├йcution</string>
<string name="chat_is_stopped">Le chat est arr├кt├й</string>
<string name="chat_database_section">BASE DE DONN├ЙES DU CHAT</string>
<string name="database_passphrase">Phrase secr├иte de la base de donn├йes</string>
<string name="export_database">Exporter la base de donn├йes</string>
<string name="stop_chat_confirmation">Arr├кter</string>
<string name="set_password_to_export">D├йfinir la phrase secr├иte pour l\'export</string>
<string name="set_password_to_export_desc">La base de donn├йes est chiffr├йe ├а l\'aide d\'une phrase secr├иte al├йatoire. Veuillez la changer avant d\'exporter.</string>
<string name="error_importing_database">Erreur lors de l\'importation de la base de donn├йes du chat</string>
<string name="chat_database_imported">Base de donn├йes du chat import├йe</string>
<string name="delete_chat_profile_question">Supprimer le profil du chat \?</string>
<string name="stop_chat_to_enable_database_actions">Arr├кter le chat pour agir sur la base de donn├йes.</string>
<string name="delete_files_and_media_question">Supprimer les fichiers et m├йdias \?</string>
<string name="delete_files_and_media_desc">Cette action ne peut ├кtre annul├йe - tous les fichiers et m├йdias re├зus et envoy├йs seront supprim├йs. Les photos ├а faible r├йsolution seront conserv├йes.</string>
<string name="no_received_app_files">Aucun fichier re├зu ou envoy├й</string>
<string name="chat_item_ttl_month">1 mois</string>
<string name="chat_item_ttl_seconds">%s seconde┬╖s</string>
<string name="delete_messages_after">Supprimer les messages apr├иs</string>
<string name="enable_automatic_deletion_question">Activer la suppression automatique des messages \?</string>
<string name="error_changing_message_deletion">Erreur de changement de param├иtre</string>
<string name="remove_passphrase_from_keychain">Retirer la phrase secr├иte du Keystore \?</string>
<string name="notifications_will_be_hidden">Les notifications seront d├йlivr├йes jusqu\'├а ce que l\'application s\'arr├кte !</string>
<string name="remove_passphrase">Supprimer</string>
<string name="current_passphrase">Phrase secr├иte actuelleтАж</string>
<string name="new_passphrase">Nouvelle phrase secr├иteтАж</string>
<string name="confirm_new_passphrase">Confirmer la nouvelle phrase secr├иteтАж</string>
<string name="update_database_passphrase">Mise ├а jour de la phrase secr├иte de la base de donn├йes</string>
<string name="keychain_is_storing_securely">Le Keystore d\'Android est utilis├й pour stocker en toute s├йcurit├й la phrase secr├иte - elle permet au service de notification de fonctionner.</string>
<string name="you_have_to_enter_passphrase_every_time">Vous devez saisir la phrase secr├иte ├а chaque fois que l\'application d├йmarre - elle n\'est pas stock├йe sur l\'appareil.</string>
<string name="encrypt_database_question">Chiffrer la base de donn├йes \?</string>
<string name="change_database_passphrase_question">Changer la phrase secr├иte de la base de donn├йes \?</string>
<string name="database_will_be_encrypted">La base de donn├йes sera chiffr├йe.</string>
<string name="keychain_error">Erreur de la keychain</string>
<string name="file_with_path">Fichier : %s</string>
<string name="database_passphrase_is_required">La phrase secr├иte de la base de donn├йes est n├йcessaire pour ouvrir le chat.</string>
<string name="save_passphrase_and_open_chat">Sauvegarder la phrase secr├иte et ouvrir le chat</string>
<string name="open_chat">Ouvrir le chat</string>
<string name="database_backup_can_be_restored">La tentative de modification de la phrase secr├иte de la base de donn├йes n\'a pas abouti.</string>
<string name="restore_database_alert_title">Restaurer la sauvegarde de la base de donn├йes \?</string>
<string name="restore_database_alert_confirm">Restaurer</string>
<string name="chat_is_stopped_indication">Le chat est arr├кt├й</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Vous pouvez lancer le chat via les Param├иtres / la Base de donn├йes de l\'app ou en la red├йmarrant.</string>
<string name="chat_archive_header">Archives du chat</string>
<string name="chat_archive_section">ARCHIVE DU CHAT</string>
<string name="save_archive">Sauvegarder l\'archive</string>
<string name="delete_archive">Supprimer l\'archive</string>
<string name="delete_chat_archive_question">Supprimer l\'archive du chat \?</string>
<string name="group_invitation_item_description">Invitation au groupe <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="join_group_question">Rejoindre le groupe \?</string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Vous ├кtes invit├й┬╖e dans un groupe. Rejoignez le pour vous connecter avec ses membres.</string>
<string name="join_group_button">Rejoindre</string>
<string name="join_group_incognito_button">Rejoindre en incognito</string>
<string name="joining_group">Entrain de rejoindre le groupe</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Vous avez rejoint ce groupe. Connexion ├а l\'invitation d\'un membre du groupe.</string>
<string name="leave_group_button">Quitter</string>
<string name="you_are_invited_to_group">Vous ├кtes invit├й┬╖e au groupe</string>
<string name="group_invitation_tap_to_join">Appuyez pour rejoindre</string>
<string name="group_invitation_tap_to_join_incognito">Appuyez pour rejoindre incognito</string>
<string name="group_invitation_expired">Invitation au groupe expir├йe</string>
<string name="rcv_group_event_member_added">a invit├й <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">est connect├й┬╖e</string>
<string name="rcv_group_event_changed_member_role">a modifi├й le r├┤le de %s pour %s</string>
<string name="rcv_group_event_changed_your_role">a modifi├й votre r├┤le pour %s</string>
<string name="rcv_group_event_user_deleted">vous a retir├й</string>
<string name="rcv_group_event_group_deleted">a supprim├й le groupe</string>
<string name="rcv_group_event_updated_group_profile">mise ├а jour du profil de groupe</string>
<string name="snd_group_event_changed_member_role">vous avez modifi├й le r├┤le de %s pour %s</string>
<string name="snd_group_event_changed_role_for_yourself">vous avez modifi├й votre r├┤le pour %s</string>
<string name="snd_group_event_user_left">vous avez quitt├й</string>
<string name="snd_group_event_group_profile_updated">mise ├а jour du profil de groupe</string>
<string name="rcv_conn_event_switch_queue_phase_completed">adresse modifi├йe pour vous</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">changement d\'adresse pour %sтАж</string>
<string name="snd_conn_event_switch_queue_phase_changing">changement d\'adresseтАж</string>
<string name="group_member_role_member">membre</string>
<string name="group_member_role_admin">admin</string>
<string name="group_member_role_owner">propri├йtaire</string>
<string name="group_member_status_removed">supprim├й</string>
<string name="group_member_status_left">a quitt├й</string>
<string name="group_member_status_group_deleted">groupe supprim├й</string>
<string name="group_member_status_invited">invit├й┬╖e</string>
<string name="group_member_status_introduced">connexion (introduite)</string>
<string name="group_member_status_intro_invitation">connexion (introduite par invitation)</string>
<string name="group_member_status_accepted">connexion (accept├йe)</string>
<string name="group_member_status_announced">connexion (annonc├йe)</string>
<string name="group_member_status_connected">connect├й</string>
<string name="group_member_status_complete">complet</string>
<string name="group_member_status_creator">cr├йateur</string>
<string name="group_member_status_connecting">connexion</string>
<string name="no_contacts_to_add">Aucun contact ├а ajouter</string>
<string name="new_member_role">Nouveau r├┤le</string>
<string name="delete_group_question">Supprimer le groupeтАп\?</string>
<string name="group_link">Lien du groupe</string>
<string name="button_create_group_link">Cr├йer un lien</string>
<string name="button_edit_group_profile">Modifier le profil du groupe</string>
<string name="remove_member_confirmation">Supprimer</string>
<string name="member_info_section_title_member">MEMBRE</string>
<string name="live_message">Message dynamique !</string>
<string name="send_live_message">Envoyer un message dynamique</string>
<string name="send_live_message_desc">Envoyez un message dynamique - il sera mis ├а jour pour leт╕▒s destinataireт╕▒s au fur et ├а mesure que vous le tapez</string>
<string name="send_verb">Envoyer</string>
<string name="member_role_will_be_changed_with_invitation">Le r├┤le sera chang├й pour ┬л%s┬╗. Le membre va recevoir une nouvelle invitation.</string>
<string name="live">LIVE</string>
<string name="button_add_members">Inviter des membres</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Vous pouvez partager un lien ou un code QR - n\'importe qui pourra rejoindre le groupe. Vous ne perdrez pas les membres du groupe si vous le supprimez par la suite.</string>
<string name="info_row_local_name">Nom local</string>
<string name="create_group_link">Cr├йer un lien de groupe</string>
<string name="error_deleting_link_for_group">Erreur lors de la suppression du lien du groupe</string>
<string name="error_creating_link_for_group">Erreur lors de la cr├йation du lien du groupe</string>
<string name="only_group_owners_can_change_prefs">Seuls les propri├йtaires du groupe peuvent modifier les pr├йf├йrences du groupe.</string>
<string name="section_title_for_console">POUR TERMINAL</string>
<string name="change_member_role_question">Changer le r├┤le du groupe \?</string>
<string name="member_role_will_be_changed_with_notification">Le r├┤le sera chang├й pour ┬л%s┬╗. Les membres du groupe seront notifi├йs.</string>
<string name="icon_descr_contact_checked">Contact v├йrifi├йт╕▒e</string>
<string name="clear_contacts_selection_button">Effacer</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contact┬╖s s├йlectionn├й┬╖e┬╖s</string>
<string name="skip_inviting_button">Passer lтАЩinvitation de membres</string>
<string name="select_contacts">S├йlectionnez des contacts</string>
<string name="no_contacts_selected">Aucun contact s├йlectionn├й</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBRES</string>
<string name="group_info_member_you">vous : <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="button_delete_group">Supprimer le groupe</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Le groupe va ├кtre supprim├й pour tout les membres - impossible de revenir en arri├иreтАп!</string>
<string name="delete_group_for_self_cannot_undo_warning">Le groupe va ├кtre supprim├й pour vous - impossible de revenir en arri├иreтАп!</string>
<string name="button_leave_group">Quitter le groupe</string>
<string name="delete_link_question">Supprimer le lienтАп\?</string>
<string name="delete_link">Supprimer le lien</string>
<string name="all_group_members_will_remain_connected">Tous les membres du groupe resteront connect├йs.</string>
<string name="icon_descr_expand_role">├Йtendre la s├йlection de r├┤le</string>
<string name="invite_to_group_button">Inviter au groupe</string>
<string name="invite_prohibited">Impossible d\'inviter le contactтАп!</string>
<string name="invite_prohibited_description">Vous essayez d\'inviter un contact avec lequel vous avez partag├й un profil incognito ├а rejoindre le groupe dans lequel vous utilisez votre profil principal</string>
<string name="info_row_database_id">ID de base de donn├йes</string>
<string name="button_remove_member">Retirer le membre</string>
<string name="button_send_direct_message">Envoi de message direct</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Ce membre sera retir├й du groupe - impossible de revenir en arri├иreтАп!</string>
<string name="role_in_group">R├┤le</string>
<string name="change_role">Changer le r├┤le</string>
<string name="change_verb">Changer</string>
<string name="switch_verb">├Йchanger</string>
<string name="error_removing_member">Erreur lors de la suppression d\'un membre</string>
<string name="error_changing_role">Erreur lors du changement de r├┤le</string>
<string name="group_full_name_field">Nom complet du groupe :</string>
<string name="update_network_settings_confirmation">Mise ├а jour</string>
<string name="chat_preferences_on">on</string>
<string name="chat_preferences_off">off</string>
<string name="direct_messages">Messages dynamiques</string>
<string name="full_deletion">Supprimer pour tous</string>
<string name="only_you_can_delete_messages">Vous ├кtes le seul ├а pouvoir supprimer des messages de mani├иre irr├йversible (votre contact peut les marquer comme supprim├й).</string>
<string name="conn_stats_section_title_servers">SERVEURS</string>
<string name="receiving_via">R├йception via</string>
<string name="theme_system">Syst├иme</string>
<string name="allow_direct_messages">Autoriser l\'envoi de messages directs aux membres.</string>
<string name="prohibit_direct_messages">Interdire l\'envoi de messages directs aux membres.</string>
<string name="group_members_can_delete">Les membres du groupe peuvent supprimer de mani├иre irr├йversible les messages envoy├йs.</string>
<string name="message_deletion_prohibited_in_chat">La suppression irr├йversible de messages est interdite dans ce groupe.</string>
<string name="sending_via">Envoi via</string>
<string name="network_status">├Йtat du r├йseau</string>
<string name="switch_receiving_address">Changer d\'adresse de r├йception</string>
<string name="create_secret_group_title">Cr├йer un groupe secret</string>
<string name="group_main_profile_sent">Votre profil de chat sera envoy├й aux membres du groupe</string>
<string name="network_option_enable_tcp_keep_alive">Activer le TCP keep-alive</string>
<string name="network_options_save">Sauvegarder</string>
<string name="update_network_settings_question">Mettre ├а jour les param├иtres r├йseau \?</string>
<string name="incognito">Incognito</string>
<string name="incognito_random_profile">Votre profil al├йatoire</string>
<string name="incognito_random_profile_description">Un profil al├йatoire sera envoy├й ├а votre contact</string>
<string name="incognito_random_profile_from_contact_description">Un profil al├йatoire sera envoy├й au contact qui vous a envoy├й ce lien</string>
<string name="incognito_info_allows">Cela permet d\'avoir plusieurs connections anonymes sans aucune donn├йes partag├йes entre elles sur un m├кme profil.</string>
<string name="incognito_info_find">Pour trouver le profil utilis├й lors d\'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat.</string>
<string name="theme_light">Clair</string>
<string name="theme_dark">Sombre</string>
<string name="theme">Th├иme</string>
<string name="save_color">Sauvegarder la couleur</string>
<string name="reset_color">R├йinitialisation des couleurs</string>
<string name="color_primary">Principale</string>
<string name="chat_preferences_you_allow">Vous autorisez</string>
<string name="chat_preferences_contact_allows">Votre contact autorise</string>
<string name="chat_preferences_default">par d├йfaut (%s)</string>
<string name="chat_preferences_no">non</string>
<string name="chat_preferences_always">toujours</string>
<string name="chat_preferences">Pr├йf├йrences de chat</string>
<string name="contact_preferences">Pr├йf├йrences de contact</string>
<string name="group_preferences">Pr├йf├йrences du groupe</string>
<string name="set_group_preferences">D├йfinir les pr├йf├йrences du groupe</string>
<string name="your_preferences">Vos pr├йf├йrences</string>
<string name="allow_your_contacts_irreversibly_delete">Autorise votre contact ├а supprimer de fa├зon d├йfinitive des messages envoy├йs.</string>
<string name="contacts_can_mark_messages_for_deletion">Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter.</string>
<string name="allow_your_contacts_to_send_voice_messages">Autorise vos contacts ├а envoyer des messages vocaux.</string>
<string name="allow_voice_messages_only_if">Autoriser les messages vocaux uniquement si votre contact les autorise.</string>
<string name="prohibit_sending_voice_messages">Interdire l\'envoi de messages vocaux.</string>
<string name="only_you_can_send_disappearing">Seulement vous pouvez envoyer des messages ├йph├йm├иres.</string>
<string name="only_you_can_send_voice">Vous seul pouvez envoyer des messages vocaux.</string>
<string name="allow_to_delete_messages">Autoriser la suppression irr├йversible de messages envoy├йs.</string>
<string name="disappearing_messages_are_prohibited">Les messages ├йph├йm├иres sont interdits dans ce groupe.</string>
<string name="group_members_can_send_voice">Les membres du groupe peuvent envoyer des messages vocaux.</string>
<string name="delete_after">Supprimer apr├иs</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_s">%ds</string>
<string name="ttl_min">%d min</string>
<string name="ttl_month">%d mois</string>
<string name="ttl_months">%d mois</string>
<string name="ttl_m">%dm</string>
<string name="ttl_mth">%dm</string>
<string name="ttl_hour">%d heure</string>
<string name="ttl_hours">%d heures</string>
<string name="ttl_h">%dh</string>
<string name="ttl_day">%d jour</string>
<string name="ttl_days">%d jours</string>
<string name="ttl_d">%dj</string>
<string name="ttl_week">%d semaine</string>
<string name="ttl_weeks">%d semaines</string>
<string name="ttl_w">%dsmn</string>
<string name="timed_messages">Messages ├йph├йm├иres</string>
<string name="voice_messages">Messages vocaux</string>
<string name="feature_enabled">activ├й</string>
<string name="feature_enabled_for_you">activ├й pour vous</string>
<string name="feature_enabled_for_contact">activ├й pour le contact</string>
<string name="feature_off">off</string>
<string name="feature_received_prohibited">re├зu, non autoris├й</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Autorise votre contact ├а envoyer des messages ├йph├йm├иres.</string>
<string name="conn_level_desc_direct">directe</string>
<string name="group_is_decentralized">Le groupe est enti├иrement d├йcentralis├й тАУ il n\'est visible que par ses membres.</string>
<string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages ├йph├йm├иres.</string>
<string name="network_options_revert">Revenir en arri├иre</string>
<string name="prohibit_sending_disappearing_messages">Interdit lтАЩenvoi de messages ├йph├йm├иres.</string>
<string name="incognito_info_protects">Le mode Incognito prot├иge la confidentialit├й de votre profil principal тАФ pour chaque nouveau contact un nouveau profil al├йatoire est cr├й├й.</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">La mise ├а jour des ces param├иtres reconnectera le client ├а tous les serveurs.</string>
<string name="incognito_info_share">Lorsque vous partagez un profil incognito avec quelqu\'un, ce profil sera utilis├й pour les groupes auxquels il vous invite.</string>
<string name="chat_preferences_yes">oui</string>
<string name="allow_disappearing_messages_only_if">Autorise les messages ├йph├йm├иres seulement si votre contact les autorises.</string>
<string name="allow_irreversible_message_deletion_only_if">Autoriser la suppression irr├йversible des messages uniquement si votre contact vous l\'autorise.</string>
<string name="only_your_contact_can_delete">Seul votre contact peut supprimer de mani├иre irr├йversible des messages (vous pouvez les marquer comme supprim├й).</string>
<string name="only_your_contact_can_send_disappearing">Seulement votre contact peut envoyer des messages ├йph├йm├иres.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Vous et votre contact ├кtes tous deux en mesure d\'envoyer des messages ├йph├йm├иres.</string>
<string name="voice_messages_are_prohibited">Les messages vocaux sont interdits dans ce groupe.</string>
<string name="group_display_name_field">Nom affich├й du groupe :</string>
<string name="group_unsupported_incognito_main_profile_sent">Le mode Incognito n\'est pas support├й ici - votre profil principal sera envoy├й aux membres du groupe</string>
<string name="conn_level_desc_indirect">indirecte (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="info_row_group">Groupe</string>
<string name="info_row_connection">Connexion</string>
<string name="network_option_seconds_label">sec</string>
<string name="network_option_tcp_connection_timeout">D├йlai de connexion TCP</string>
<string name="group_profile_is_stored_on_members_devices">Le profil du groupe est stock├й sur les appareils des membres, pas sur les serveurs.</string>
<string name="save_group_profile">Sauvegarder le profil du groupe</string>
<string name="error_saving_group_profile">Erreur lors de la sauvegarde du profil de groupe</string>
<string name="network_options_reset_to_defaults">R├йinitialisation des valeurs par d├йfaut</string>
<string name="network_option_protocol_timeout">D├йlai du protocole</string>
<string name="network_option_ping_interval">Intervalle de PING</string>
<string name="both_you_and_your_contacts_can_delete">Vous et votre contact ├кtes tous deux en mesure de supprimer de mani├иre irr├йversible les messages envoy├йs.</string>
<string name="message_deletion_prohibited">La suppression irr├йversible de message est interdite dans ce chat.</string>
<string name="both_you_and_your_contact_can_send_voice">Vous et votre contact ├кtes tous deux en mesure d\'envoyer des messages vocaux.</string>
<string name="only_your_contact_can_send_voice">Seul votre contact peut envoyer des messages vocaux.</string>
<string name="voice_prohibited_in_this_chat">Les messages vocaux sont interdits dans ce chat.</string>
<string name="disappearing_prohibited_in_this_chat">Les messages ├йph├йm├иres sont interdits dans cette discussion.</string>
<string name="allow_to_send_voice">Autoriser l\'envoi de messages vocaux.</string>
<string name="prohibit_sending_voice">Interdire l\'envoi de messages vocaux.</string>
<string name="allow_to_send_disappearing">Autorise lтАЩenvoi de messages ├йph├йm├иres.</string>
<string name="prohibit_sending_disappearing">Interdit lтАЩenvoi de messages ├йph├йm├иres.</string>
<string name="prohibit_message_deletion">Interdire la suppression irr├йversible des messages.</string>
<string name="group_members_can_send_dms">Les membres du groupe peuvent envoyer des messages directs.</string>
<string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans ce groupe.</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_desc">Comparez les codes de s├йcurit├й avec vos contacts.</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_group_links">Liens de groupe</string>
<string name="v4_2_auto_accept_contact_requests_desc">Avec message de bienvenue facultatif.</string>
<string name="v4_3_voice_messages">Messages vocaux</string>
<string name="v4_3_voice_messages_desc">Max 40 secondes, r├йception imm├йdiate.</string>
<string name="v4_3_irreversible_message_deletion">Suppression irr├йversible des messages</string>
<string name="v4_3_irreversible_message_deletion_desc">Vos contacts peuvent autoriser la suppression compl├иte des messages.</string>
<string name="v4_3_improved_privacy_and_security">Une meilleure s├йcurit├й et protection de la vie priv├йe</string>
<string name="v4_3_improved_privacy_and_security_desc">Masquer l\'├йcran de l\'app dans les apps r├йcentes.</string>
<string name="v4_4_disappearing_messages">Messages ├йph├йm├иres</string>
<string name="v4_4_disappearing_messages_desc">Les messages envoy├йs seront supprim├йs apr├иs une dur├йe d├йtermin├йe.</string>
<string name="v4_4_live_messages">Messages dynamiques</string>
<string name="accept_feature">Accepter</string>
<string name="v4_2_auto_accept_contact_requests">Demandes de contact auto-accept├йes</string>
<string name="whats_new">Quoi de neuf \?</string>
<string name="v4_2_group_links_desc">Les admins peuvent cr├йer les liens qui permettent de rejoindre les groupes.</string>
<string name="accept_feature_set_1_day">D├йfinir 1 jour</string>
<string name="v4_2_security_assessment_desc">La s├йcurit├й de SimpleX Chat a ├йt├й audit├йe par Trail of Bits.</string>
<string name="v4_3_improved_server_configuration">Configuration de serveur am├йlior├йe</string>
<string name="v4_3_improved_server_configuration_desc">Ajoutez des serveurs en scannant des codes QR.</string>
<string name="invalid_data">donn├йes invalides</string>
<string name="invalid_chat">chat invalide</string>
<string name="icon_descr_cancel_live_message">Annuler le message dynamique</string>
<string name="feature_offered_item">offert %s</string>
<string name="feature_offered_item_with_param">offert %s: %2s</string>
<string name="feature_cancelled_item">annul├й %s</string>
<string name="app_version_title">Version de l\'application</string>
<string name="core_simplexmq_version">simplexmq : v%s (%2s)</string>
<string name="app_version_code">Build de l\'app : %s</string>
<string name="app_version_name">Version de l\'app : v%s</string>
<string name="core_build_timestamp">C┼Уur compil├й le : %s</string>
<string name="core_version">Version du c┼Уur : v%s</string>
<string name="network_option_ping_count">Nombre de PING</string>
<string name="users_delete_all_chats_deleted">Toutes les discussions et tous les messages seront supprim├йs - il est impossible de revenir en arri├иre !</string>
<string name="delete_files_and_media_all">Effacer tous les fichiers</string>
<string name="users_delete_profile_for">Supprimer le profil de chat pour</string>
<string name="network_session_mode_user_description">Une connexion TCP distincte (et un identifiant SOCKS) sera utilis├йe <b>pour chaque profil de chat que vous avez dans l\'application</b>.</string>
<string name="users_delete_question">Supprimer le profil du chat \?</string>
<string name="files_and_media_section">Fichiers &amp; m├йdias</string>
<string name="messages_section_title">Messages</string>
<string name="smp_servers_per_user">Les serveurs pour les nouvelles connexions de votre profil de chat actuel</string>
<string name="messages_section_description">Ce param├иtre s\'applique aux messages de votre profil de chat actuel</string>
<string name="network_session_mode_entity">Connexion</string>
<string name="delete_files_and_media_for_all_users">Effacer les fichiers de tous les profils de chat</string>
<string name="users_delete_with_connections">Profil et connexions au serveur</string>
<string name="network_session_mode_transport_isolation">Isolement du transport</string>
<string name="update_network_session_mode_question">Mettre ├а jour le mode d\'isolation du transport \?</string>
<string name="your_chat_profiles_stored_locally">Vos profils de chat sont stock├йs localement, uniquement sur votre appareil</string>
<string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilis├йe <b>pour chaque contact et membre de groupe</b>.
\n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de r├йseau peut ├кtre nettement plus ├йlev├йe et certaines liaisons peuvent ├йchouer.</string>
<string name="network_session_mode_user">Profil de chat</string>
<string name="users_add">Ajouter un profil</string>
<string name="users_delete_data_only">Donn├йes de profil local uniquement</string>
<string name="error_deleting_user">Erreur lors de la suppression du profil utilisateur</string>
<string name="your_chat_profiles">Vos profils de chat</string>
<string name="failed_to_active_user_title">Erreur lors du changement de profil !</string>
<string name="failed_to_create_user_title">Erreur lors de la cr├йation du profil !</string>
<string name="failed_to_create_user_duplicate_desc">Vous avez d├йj├а un profil de chat avec ce m├кme nom affich├й. Veuillez choisir un autre nom.</string>
<string name="failed_to_create_user_duplicate_title">Nom d\'affichage en double !</string>
<string name="v4_4_french_interface">Interface en fran├зais</string>
<string name="v4_5_transport_isolation_descr">Par profil de chat (par d├йfaut) ou par connexion (BETA).</string>
<string name="v4_5_italian_interface">Interface en italien</string>
<string name="v4_5_message_draft">Brouillon de message</string>
<string name="v4_5_reduced_battery_usage_descr">Plus d\'am├йliorations ├а venir !</string>
<string name="v4_5_multiple_chat_profiles">Diff├йrents profils de chat</string>
<string name="v4_5_message_draft_descr">Conserver le brouillon du dernier message, avec les pi├иces jointes.</string>
<string name="v4_5_reduced_battery_usage">R├йduction de la consommation de batterie</string>
<string name="v4_5_private_filenames">Noms de fichiers priv├йs</string>
<string name="v4_5_italian_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="v4_4_french_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="v4_5_private_filenames_descr">Pour pr├йserver le fuseau horaire, les fichiers image/voix utilisent le syst├иme UTC.</string>
<string name="v4_5_transport_isolation">Isolation du transport</string>
<string name="v4_5_multiple_chat_profiles_descr">Diff├йrents noms, avatars et mode d\'isolation de transport.</string>
</resources>
@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="share_image">рдЪрд┐рддреНрд░ рд╕рд╛рдЭрд╛ рдХрд░реЗрдВтАж</string>
<string name="chat_preferences_off">рдмрдВрдж</string>
<string name="accept_feature_set_1_day">1 рджрд┐рди рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдХрд░реЗрдВ</string>
<string name="v4_3_improved_server_configuration_desc">рдХреНрдпреВрдЖрд░ рд╕рдВрд╣рд┐рддрд╛ рд╕реНрдХреИрди рдХрд░рдХреЗ рд╕рд░реНрд╡рд░ рдЬреЛрдбрд╝реЗрдВред</string>
<string name="group_preview_you_are_invited">рдЖрдкрдХреЛ рд╕рдореВрд╣ рдореЗрдВ рдЖрдордВрддреНрд░рд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ</string>
<string name="icon_descr_server_status_connected">рдЬреБрдбрд╝реЗ рд╣реБрдП</string>
<string name="use_camera_button">рдХреИрдорд░реЗ рдХрд╛ рдкреНрд░рдпреЛрдЧ рдХрд░реЗрдВ</string>
<string name="above_then_preposition_continuation">рдКрдкрд░,рддрдм:</string>
<string name="accept_contact_button">рд╕реНрд╡реАрдХрд╛рд░ рдХрд░рдирд╛</string>
<string name="connect_button">рдЬреБрдбрд┐рдпреЗ</string>
<string name="your_contact_address">рдЖрдкрдХрд╛ рд╕рдВрдкрд░реНрдХ рдкрддрд╛</string>
<string name="smp_servers_add_to_another_device">рджреВрд╕рд░реЗ рдЙрдкрдХрд░рдг рдореЗрдВ рдЬреЛрдбрд╝реЗрдВ</string>
<string name="bold">рдирд┐рдбрд░</string>
<string name="answer_call">рдХреЙрд▓ рдХрд╛ рдЙрддреНрддрд░ рджреЗрдВ</string>
<string name="settings_section_title_you">рддреБрдо</string>
<string name="settings_section_title_settings">рд╕рдорд╛рдпреЛрдЬрди</string>
<string name="chat_item_ttl_month">1 рдорд╣реАрдирд╛</string>
<string name="rcv_group_event_member_connected">рдЬреБрдбрд╝реЗ рд╣реБрдП</string>
<string name="group_member_role_admin">рд╡реНрдпрд╡рд╕реНрдерд╛рдкрдХ</string>
<string name="all_group_members_will_remain_connected">рд╕рдореВрд╣ рдХреЗ рд╕рднреА рд╕рджрд╕реНрдп рдЬреБрдбрд╝реЗ рд░рд╣реЗрдВрдЧреЗред</string>
<string name="change_verb">рдкрд░рд┐рд╡рд░реНрддрди</string>
<string name="sending_via">рдорд╛рдзреНрдпрдо рд╕реЗ рднреЗрдЬрд╛ рдЬрд╛ рд░рд╣рд╛ рд╣реИ</string>
<string name="feature_off">рдмрдВрдж</string>
<string name="whats_new">рдирдпрд╛ рдХреНрдпрд╛ рд╣реИ</string>
<string name="v4_2_group_links_desc">рд╡реНрдпрд╡рд╕реНрдерд╛рдкрдХ рд╕рдореВрд╣реЛрдВ рдореЗрдВ рд╢рд╛рдорд┐рд▓ рд╣реЛрдиреЗ рдХреЗ рд▓рд┐рдП рд▓рд┐рдВрдХ рдмрдирд╛ рд╕рдХрддреЗ рд╣реИрдВред</string>
<string name="chat_item_ttl_day">1 рджрд┐рди</string>
<string name="chat_item_ttl_week">1 рд╕рдкреНрддрд╛рд╣</string>
<string name="about_simplex">рд╕рд┐рдВрдкрд▓рдПрдХреНрд╕ рдХреЗ рдмрд╛рд░реЗ рдореЗрдВ</string>
<string name="about_simplex_chat">рдмрд╛рд░реЗ рдореЗрдВ <xliff:g id="appNameFull">рд╕рд┐рдВрдкреНрд▓реЗрдХреНрд╕ рдЪреИрдЯ</xliff:g></string>
<string name="accept_call_on_lock_screen">рд╕реНрд╡реАрдХрд╛рд░ рдХрд░рдирд╛</string>
<string name="accept">рд╕реНрд╡реАрдХрд╛рд░ рдХрд░рдирд╛</string>
<string name="accept_feature">рд╕реНрд╡реАрдХрд╛рд░ рдХрд░рдирд╛</string>
<string name="accept_connection_request__question">рд╕рдВрдмрдВрдз рдЕрдиреБрд░реЛрдз рд╕реНрд╡реАрдХрд╛рд░ рдХрд░реЗрдВ\?</string>
<string name="callstatus_accepted">рд╕реНрд╡реАрдХреГрдд рдХреЙрд▓</string>
<string name="accept_contact_incognito_button">рдЧреБрдкреНрдд рд╕реНрд╡реАрдХрд╛рд░ рдХрд░реЗрдВ</string>
<string name="accept_requests">рдирд┐рд╡реЗрджрди рд╕реНрд╡реАрдХрд╛рд░ рдХрд░реЛ</string>
<string name="smp_servers_preset_add">рдкреВрд░реНрд╡рдирд┐рд░реНрдзрд╛рд░рд┐рдд рд╕рд░реНрд╡рд░ рдЬреЛрдбрд╝реЗрдВ</string>
<string name="users_add">рдкреНрд░реЛрдлрд╝рд╛рдЗрд▓ рдЬреЛрдбрд╝реЗрдВ</string>
<string name="smp_servers_add">рд╕рд░реНрд╡рд░ рдЬреЛрдбрд╝реЗтАж</string>
<string name="notifications_mode_service">рд╣рдореЗрд╢рд╛ рдмрдиреЗ рд░рд╣реЗрдВ</string>
<string name="attach">рд╕рдВрд▓рдЧреНрди рдХрд░рдирд╛</string>
<string name="network_settings">рдЙрдиреНрдирдд рд╕рдВрдЬрд╛рд▓ рд╕рдорд╛рдпреЛрдЬрди</string>
<string name="users_delete_all_chats_deleted">рд╕рднреА рдмрд╛рддрдЪреАрдд рдФрд░ рд╕рдВрджреЗрд╢ рд╣рдЯрд╛ рджрд┐рдП рдЬрд╛рдПрдВрдЧреЗ - рдЗрд╕реЗ рдкреВрд░реНрд╡рд╡рдд рдирд╣реАрдВ рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛!</string>
<string name="chat_preferences_always">рд╣рдореЗрд╢рд╛</string>
<string name="allow_verb">рдЕрдиреБрдорддрд┐ рджреЗрдирд╛</string>
<string name="appearance_settings">рджрд┐рдЦрд╛рд╡рдЯ</string>
<string name="cancel_verb">рд░рджреНрдж рдХрд░рдирд╛</string>
<string name="icon_descr_cancel_file_preview">рдлрд╝рд╛рдЗрд▓ рдкреВрд░реНрд╡рд╛рд╡рд▓реЛрдХрди рд░рджреНрдж рдХрд░реЗрдВ</string>
<string name="icon_descr_cancel_image_preview">рдЫрд╡рд┐ рдкреВрд░реНрд╡рд╛рд╡рд▓реЛрдХрди рд░рджреНрдж рдХрд░реЗрдВ</string>
<string name="clear_verb">рд╕рд╛рдлрд╝</string>
<string name="colored">рд░рдВрдЧреАрди</string>
<string name="callstate_connected">рдЬреБрдбрд╝реЗ рд╣реБрдП</string>
<string name="smp_server_test_connect">рдЬреБрдбрд┐рдпреЗ</string>
<string name="connect_via_link_verb">рдЬреБрдбрд┐рдпреЗ</string>
<string name="server_connected">рдЬреБрдбрд╝реЗ рд╣реБрдП</string>
<string name="group_member_role_owner">рд╕реНрд╡рд╛рдореА</string>
<string name="group_member_status_connected">рдЬреБрдбрд╝реЗ рд╣реБрдП</string>
<string name="notification_contact_connected">рдЬреБрдбрд╝реЗ рд╣реБрдП</string>
<string name="you_joined_this_group">рдЖрдк рдЗрд╕ рд╕рдореВрд╣ рдореЗрдВ рд╢рд╛рдорд┐рд▓ рд╣реЛ рдЧрдП</string>
<string name="group_info_member_you">рддреБрдо: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="you_are_invited_to_group">рдЖрдкрдХреЛ рд╕рдореВрд╣ рдореЗрдВ рдЖрдордВрддреНрд░рд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ</string>
<string name="snd_conn_event_switch_queue_phase_completed">рддреБрдордиреЗ рдкрддрд╛ рдмрджрд▓ рд▓рд┐рдпрд╛</string>
<string name="snd_group_event_user_left">рдЖрдк рдЪрд▓реЗ рдЧрдП</string>
<string name="unknown_error">рдЕрдЬреНрдЮрд╛рдд рддреНрд░реБрдЯрд┐</string>
<string name="chat_preferences_you_allow">рдЖрдк рдЖрдЬреНрдЮрд╛ рджреЗрдВ</string>
<string name="welcome">рд╕реНрд╡рд╛рдЧрдд!</string>
<string name="la_notice_turn_on">рдЪрд╛рд▓реВ рдХрд░реЛ</string>
<string name="section_title_welcome_message">рд╕реНрд╡рд╛рдЧрдд рд╕рдВрджреЗрд╢</string>
<string name="unknown_message_format">рдЕрдЬреНрдЮрд╛рдд рд╕рдВрджреЗрд╢ рдкреНрд░рд╛рд░реВрдк</string>
<string name="personal_welcome">рд╕реНрд╡рд╛рдЧрдд <xliff:g>%1$s</xliff:g>!</string>
<string name="callstate_starting">рд╢реБрд░реБрдЖрдд</string>
<string name="send_verb">рднреЗрдЬрдирд╛</string>
<string name="save_color">рд░рдВрдЧ рдмрдЪрд╛рдУ</string>
<string name="share_verb">рд╕рд╛рдЭрд╛ рдХрд░рдирд╛</string>
<string name="reject_contact_button">рдЕрд╕реНрд╡реАрдХрд╛рд░</string>
<string name="network_use_onion_hosts_required">рдЖрд╡рд╢реНрдпрдХ</string>
<string name="reject">рдЕрд╕реНрд╡реАрдХрд╛рд░</string>
<string name="open_verb">рдЦреБрд▓рд╛</string>
<string name="group_member_status_removed">рдирд┐рдХрд╛рд▓рд╛ рдЧрдпрд╛</string>
<string name="rcv_group_event_member_deleted">рдирд┐рдХрд╛рд▓рд╛ рдЧрдпрд╛ <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="reply_verb">рдЬрд╡рд╛рдм рджреЗ рджреЛ</string>
<string name="leave_group_button">рдЫреЛрдбрд╝рдирд╛</string>
<string name="mark_read">рдкрдврд╝рд╛ рд╣реБрдЖ рдЪрд┐рд╣реНрдирд┐рдд рдХрд░реЗрдВ</string>
<string name="icon_descr_more_button">рдЕрдзрд┐рдХ</string>
<string name="network_use_onion_hosts_no">рдирд╣реАрдВ</string>
<string name="chat_item_ttl_none">рдХрднреА рдирд╣реАрдВ</string>
<string name="group_member_status_invited">рдЖрдордВрддреНрд░рд┐рдд</string>
<string name="delete_after">рдмрд╛рдж рдорд┐рдЯрд╛ рджреЗрдВ</string>
<string name="display_name_invited_to_connect">рдЬреБрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдЖрдордВрддреНрд░рд┐рдд рдХрд┐рдпрд╛</string>
<string name="rcv_group_event_invited_via_your_group_link">рдЖрдкрдХреЗ рд╕рдореВрд╣ рд▓рд┐рдВрдХ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдЖрдордВрддреНрд░рд┐рдд рдХрд┐рдпрд╛ рдЧрдпрд╛</string>
<string name="rcv_group_event_member_added">рдЖрдордВрддреНрд░рд┐рдд <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="icon_descr_add_members">рд╕рджрд╕реНрдпреЛрдВ рдХреЛ рдЖрдордВрддреНрд░рд┐рдд рдХрд░реЛ</string>
<string name="v4_3_irreversible_message_deletion">рдЕрдкрд░рд┐рд╡рд░реНрддрдиреАрдп рд╕рдВрджреЗрд╢ рд╡рд┐рд▓реЛрдкрди</string>
<string name="button_add_members">рд╕рджрд╕реНрдпреЛрдВ рдХреЛ рдЖрдордВрддреНрд░рд┐рдд рдХрд░реЛ</string>
<string name="invite_to_group_button">рд╕рдореВрд╣ рдореЗрдВ рдЖрдордВрддреНрд░рд┐рдд рдХрд░реЗрдВ</string>
<string name="message_deletion_prohibited_in_chat">рдЗрд╕ рд╕рдореВрд╣ рдореЗрдВ рдЕрдкрд░рд┐рд╡рд░реНрддрдиреАрдп рд╕рдВрджреЗрд╢ рд╣рдЯрд╛рдирд╛ рдкреНрд░рддрд┐рдмрдВрдзрд┐рдд рд╣реИред</string>
<string name="italic">рддрд┐рд░рдЫрд╛</string>
<string name="join_group_button">рдЬреЛрдбрд╝рдирд╛</string>
<string name="join_group_incognito_button">рдЧреБрдкреНрдд рдореЗрдВ рд╢рд╛рдорд┐рд▓ рд╣реЛрдВ</string>
<string name="joining_group">рд╕рдореВрд╣ рдореЗрдВ рд╢рд╛рдорд┐рд▓ рд╣реЛрдирд╛</string>
<string name="join_group_question">рд╕рдореВрд╣ рдореЗрдВ рд╢рд╛рдорд┐рд▓ рд╣реЛрдВ\?</string>
<string name="thousand_abbreviation">рдХ</string>
<string name="keychain_error">рдЪрд╛рдмреА рдХрд╛ рдЧреБрдЪреНрдЫрд╛ рддреНрд░реБрдЯрд┐</string>
<string name="button_leave_group">рд╕рдореВрд╣ рдЫреЛрдбрд╝ рджреЗрдВ</string>
<string name="leave_group_question">рд╕рдореВрд╣ рдЫреЛрдбрд╝ рджреЗрдВ</string>
<string name="rcv_group_event_member_left">рдмрд╛рдПрдВ</string>
<string name="group_member_status_left">рдмрд╛рдПрдВ</string>
<string name="theme_light">рд░реЛрд╢рдиреА</string>
<string name="info_row_local_name">рд╕реНрдерд╛рдиреАрдп рдирд╛рдо</string>
<string name="users_delete_data_only">рдХреЗрд╡рд▓ рд╕реНрдерд╛рдиреАрдп рдкреНрд░реЛрдлрд╝рд╛рдЗрд▓ рдбреЗрдЯрд╛</string>
<string name="auth_log_in_using_credential">рдЕрдкрдиреЗ рдХреНрд░реЗрдбреЗрдВрд╢рд┐рдпрд▓ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд▓реЙрдЧ рдЗрди рдХрд░реЗрдВ</string>
<string name="make_private_connection">рдПрдХ рдирд┐рдЬреА рд╕рдВрдмрдВрдз рдмрдирд╛рдУ</string>
<string name="marked_deleted_description">рдорд┐рдЯрд╛рдпрд╛ рд╣реБрдЖ рдЪрд┐рд╣реНрдирд┐рдд рдХрд┐рдпрд╛ рдЧрдпрд╛</string>
<string name="mark_unread">рдЕрдкрдард┐рдд рдХреЛ рдЪрд┐рд╣реНрдирд┐рдд рдХрд░реЗрдВ</string>
<string name="v4_3_voice_messages_desc">рдЕрдзрд┐рдХрддрдо 40 рд╕реЗрдХрдВрдб, рддреБрд░рдиреНрдд рдкреНрд░рд╛рдкреНрдд рд╣реБрдЖред</string>
<string name="you_sent_group_invitation">рдЖрдкрдиреЗ рд╕рдореВрд╣ рдЖрдордВрддреНрд░рдг рднреЗрдЬрд╛</string>
<string name="message_delivery_error_desc">рд╕рдмрд╕реЗ рдЕрдзрд┐рдХ рд╕рдВрднрд╛рд╡рдирд╛ рд╣реИ рдХрд┐ рдЗрд╕ рд╕рдВрдкрд░реНрдХ рдиреЗ рдЖрдкрдХреЗ рд╕рд╛рде рд╕рдВрдмрдВрдз рд╣рдЯрд╛ рджрд┐рдпрд╛ рд╣реИред</string>
<string name="mute_chat">рдореВрдХ</string>
<string name="network_status">рдиреЗрдЯрд╡рд░реНрдХ рдХреА рд╕реНрдерд┐рддрд┐</string>
<string name="notification_new_contact_request">рдирдпрд╛ рд╕рдВрдкрд░реНрдХ рдЕрдиреБрд░реЛрдз</string>
<string name="delete_files_and_media_all">рд╕рднреА рдлрд╛рдЗрд▓реЛрдВ рдХреЛ рдорд┐рдЯрд╛ рджреЗрдВ</string>
<string name="delete_archive">рд╕рдВрдЧреНрд░рд╣ рд╣рдЯрд╛рдПрдВ</string>
<string name="new_database_archive">рдирдпрд╛ рдбреЗрдЯрд╛рдмреЗрд╕ рд╕рдВрдЧреНрд░рд╣</string>
<string name="new_member_role">рдирдП рд╕рджрд╕реНрдп рдХреА рднреВрдорд┐рдХрд╛</string>
<string name="settings_notifications_mode_title">рдЕрдзрд┐рд╕реВрдЪрдирд╛ рд╕реЗрд╡рд╛</string>
<string name="notification_preview_new_message">рдирдпрд╛ рд╕рдиреНрджреЗрд╢</string>
<string name="no_contacts_to_add">рдЬреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдХреЛрдИ рд╕рдВрдкрд░реНрдХ рдирд╣реАрдВ рд╣реИ</string>
<string name="chat_preferences_no">рдирд╣реАрдВ</string>
<string name="no_contacts_selected">рдХреЛрдИ рд╕рдВрдкрд░реНрдХ рдирд╣реАрдВ рдЪреБрдирд╛ рдЧрдпрд╛</string>
<string name="no_details">рдХреЛрдИ рд╡рд┐рд╡рд░рдг рдирд╣реАрдВ</string>
<string name="settings_notification_preview_title">рдЕрдзрд┐рд╕реВрдЪрдирд╛ рдкреВрд░реНрд╡рд╛рд╡рд▓реЛрдХрди</string>
<string name="notifications">рд╕реВрдЪрдирд╛рдПрдВ</string>
<string name="full_deletion">рд╕рднреА рдХреЗ рд▓рд┐рдП рд╣рдЯрд╛рдПрдВ</string>
<string name="delete_chat_archive_question">рдЪреИрдЯ рд╕рдВрдЧреНрд░рд╣ рдорд┐рдЯрд╛рдПрдВ\?</string>
<string name="delete_chat_profile_question">рдЪреИрдЯ рдкреНрд░реЛрдлрд╝рд╛рдЗрд▓ рд╣рдЯрд╛рдПрдВ\?</string>
<string name="users_delete_question">рдЪреИрдЯ рдкреНрд░реЛрдлрд╝рд╛рдЗрд▓ рд╣рдЯрд╛рдПрдВ\?</string>
<string name="users_delete_profile_for">рдХреЗ рд▓рд┐рдП рдЪреИрдЯ рдкреНрд░реЛрдлрд╝рд╛рдЗрд▓ рд╣рдЯрд╛рдПрдВ</string>
<string name="button_delete_contact">рд╕рдВрдкрд░реНрдХ рдорд┐рдЯрд╛ рджреЗрдВ</string>
<string name="deleted_description">рд╣рдЯрд╛рдП рдЧрдП</string>
<string name="delete_contact_question">рд╕рдВрдкрд░реНрдХ рдорд┐рдЯрд╛ рджреЗрдВ\?</string>
<string name="rcv_group_event_group_deleted">рд╣рдЯрд╛рдП рдЧрдП рд╕рдореВрд╣</string>
<string name="delete_image">рдЫрд╡рд┐ рд╣рдЯрд╛рдПрдВ</string>
<string name="button_delete_group">рд╕рдореВрд╣ рд╣рдЯрд╛рдПрдВ</string>
<string name="for_me_only">рдореЗрд░реЗ рд▓рд┐рдП рд╣рдЯрд╛рдПрдВ</string>
</resources>

Some files were not shown because too many files have changed in this diff Show More