Compare commits

...

148 Commits

Author SHA1 Message Date
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
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
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
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 7323bb4333 Merge branch 'master' into users 2023-01-11 18:38:55 +04:00
JRoberts 41e873d5ca core: multiple users view, tests (#1710) 2023-01-11 11:00:28 +04:00
JRoberts ad1b091b18 Merge branch 'master' into users 2023-01-09 17:02:38 +04:00
JRoberts bb0482104c core, ios, android: add UserId to api commands (#1696) 2023-01-05 20:38:31 +04: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
181 changed files with 47609 additions and 8242 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/
+26 -4
View File
@@ -7,7 +7,6 @@
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
[![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)
[![Follow on Twitter](https://img.shields.io/twitter/follow/SimpleXChat?style=social)](https://twitter.com/SimpleXChat)
[<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;
@@ -44,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)
@@ -219,19 +219,41 @@ If you are considering developing with SimpleX platform please get in touch for
## Join a user group
You can join a general English speaking group: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FcIS0gu1h0Y8pZpQkDaSz7HZGSHcKpMB9%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAKzzWAJYrVt1zdgRp4pD3FBst6eK7233DJeNElENLJRA%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%228mazMhefXoM5HxWBfZnvwQ%3D%3D%22%7D).
You can 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).
Groups in languages other than English, that we have app interface translated into: [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian speaking).
You can 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.
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:
- translate UI to your language - we are using [Weblate](https://hosted.weblate.org/projects/simplex-chat/) to translate the interface, please get in touch if you want to contribute!
- 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.
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 89
versionName "4.4.3"
versionCode 98
versionName "4.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
@@ -22,6 +22,7 @@ var TransformOperation;
TransformOperation["Decrypt"] = "decrypt";
})(TransformOperation || (TransformOperation = {}));
let activeCall;
let answerTimeout = 30000;
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stun:stun.simplex.im:443"] },
@@ -100,9 +101,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 +123,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 +131,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 +151,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 @@ 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
@@ -373,13 +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()
}
}
}
}
@@ -393,7 +387,7 @@ 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()
@@ -404,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")
@@ -90,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) {
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.call.*
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.usersettings.NotificationPreviewMode
@@ -35,6 +36,7 @@ import kotlin.time.*
class ChatModel(val controller: ChatController) {
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()
val userCreated = mutableStateOf<Boolean?>(null)
val chatRunning = mutableStateOf<Boolean?>(null)
val chatDbChanged = mutableStateOf<Boolean>(false)
@@ -42,6 +44,8 @@ class ChatModel(val controller: ChatController) {
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val chatDbDeleted = mutableStateOf(false)
val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
// current chat
val chatId = mutableStateOf<String?>(null)
@@ -81,19 +85,15 @@ class ChatModel(val controller: ChatController) {
// currently showing QR code
val connReqInv = mutableStateOf(null as String?)
var draft = mutableStateOf(null as ComposeState?)
var draftChatId = mutableStateOf(null as String?)
// working with external intents
val sharedContent = mutableStateOf(null as SharedContent?)
val filesToDelete = mutableSetOf<File>()
val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get())
fun updateUserProfile(profile: LocalProfile) {
val user = currentUser.value
if (user != null) {
currentUser.value = user.copy(profile = profile)
}
}
fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null
fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id }
fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
@@ -120,17 +120,8 @@ class ChatModel(val controller: ChatController) {
}
fun updateChats(newChats: List<Chat>) {
val mergedChats = arrayListOf<Chat>()
for (newChat in newChats) {
val i = getChatIndex(newChat.chatInfo.id)
if (i >= 0) {
mergedChats.add(newChat.copy(serverInfo = chats[i].serverInfo))
} else {
mergedChats.add(newChat)
}
}
chats.clear()
chats.addAll(mergedChats)
chats.addAll(newChats)
val cId = chatId.value
// If chat is null, it was deleted in background after apiGetChats call
@@ -139,14 +130,6 @@ class ChatModel(val controller: ChatController) {
}
}
fun updateNetworkStatus(id: ChatId, status: Chat.NetworkStatus) {
val i = getChatIndex(id)
if (i >= 0) {
val chat = chats[i]
chats[i] = chat.copy(serverInfo = chat.serverInfo.copy(networkStatus = status))
}
}
fun replaceChat(id: String, chat: Chat) {
val i = getChatIndex(id)
if (i >= 0) {
@@ -168,6 +151,7 @@ class ChatModel(val controller: ChatController) {
chatStats =
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
increaseUnreadCounter(currentUser.value!!)
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId)
}
else
@@ -256,6 +240,7 @@ class ChatModel(val controller: ChatController) {
// clear preview
val i = getChatIndex(cInfo.id)
if (i >= 0) {
decreaseUnreadCounter(currentUser.value!!, chats[i].chatStats.unreadCount)
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
}
// clear current chat
@@ -264,6 +249,19 @@ class ChatModel(val controller: ChatController) {
}
}
fun updateCurrentUser(newProfile: Profile, preferences: FullChatPreferences? = null) {
val current = currentUser.value ?: return
val updated = current.copy(
profile = newProfile.toLocalProfile(current.profile.profileId),
fullPreferences = preferences ?: current.fullPreferences
)
val indexInUsers = users.indexOfFirst { it.user.userId == current.userId }
if (indexInUsers != -1) {
users[indexInUsers] = UserInfo(updated, users[indexInUsers].unreadCount)
}
currentUser.value = updated
}
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
withContext(Dispatchers.Main) {
@@ -286,9 +284,11 @@ class ChatModel(val controller: ChatController) {
val chat = chats[chatIdx]
val lastId = chat.chatItems.lastOrNull()?.id
if (lastId != null) {
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIdx] = chat.copy(
chatStats = chat.chatStats.copy(
unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0,
unreadCount = unreadCount,
// Can't use minUnreadItemId currently since chat items can have unread items between read items
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
)
@@ -324,13 +324,30 @@ class ChatModel(val controller: ChatController) {
if (chatIndex == -1) return
val chat = chats[chatIndex]
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
chats[chatIndex] = chat.copy(
chatStats = chat.chatStats.copy(
unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0),
unreadCount = unreadCount,
)
)
}
fun increaseUnreadCounter(user: User) {
changeUnreadCounter(user, 1)
}
fun decreaseUnreadCounter(user: User, by: Int = 1) {
changeUnreadCounter(user, -by)
}
private fun changeUnreadCounter(user: User, by: Int) {
val i = users.indexOfFirst { it.user.userId == user.userId }
if (i != -1) {
users[i] = UserInfo(user, users[i].unreadCount + by)
}
}
// func popChat(_ id: String) {
// if let i = getChatIndex(id) {
// popChat_(i)
@@ -375,6 +392,20 @@ class ChatModel(val controller: ChatController) {
false
}
}
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
networkStatuses[contact.activeConn.agentConnId] = status
}
fun contactNetworkStatus(contact: Contact): NetworkStatus =
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
fun addTerminalItem(item: TerminalItem) {
if (terminalItems.size >= 500) {
terminalItems.removeAt(0)
}
terminalItems.add(item)
}
}
enum class ChatType(val type: String) {
@@ -410,6 +441,19 @@ data class User(
}
}
@Serializable
data class UserInfo(
val user: User,
val unreadCount: Int
) {
companion object {
val sampleData = UserInfo(
user = User.sampleData,
unreadCount = 1
)
}
}
typealias ChatId = String
interface NamedChat {
@@ -441,37 +485,12 @@ data class Chat (
val chatInfo: ChatInfo,
val chatItems: List<ChatItem>,
val chatStats: ChatStats = ChatStats(),
val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown())
) {
val id: String get() = chatInfo.id
@Serializable
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
@Serializable
data class ServerInfo(val networkStatus: NetworkStatus)
@Serializable
sealed class NetworkStatus {
val statusString: String get() =
when (this) {
is Connected -> generalGetString(R.string.server_connected)
is Error -> generalGetString(R.string.server_error)
else -> generalGetString(R.string.server_connecting)
}
val statusExplanation: String get() =
when (this) {
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
}
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@Serializable @SerialName("connected") class Connected: NetworkStatus()
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
}
companion object {
val sampleData = Chat(
chatInfo = ChatInfo.Direct.sampleData,
@@ -605,6 +624,27 @@ sealed class ChatInfo: SomeChat, NamedChat {
}
}
@Serializable
sealed class NetworkStatus {
val statusString: String get() =
when (this) {
is Connected -> generalGetString(R.string.server_connected)
is Error -> generalGetString(R.string.server_error)
else -> generalGetString(R.string.server_connecting)
}
val statusExplanation: String get() =
when (this) {
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
}
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
@Serializable @SerialName("connected") class Connected: NetworkStatus()
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
}
@Serializable
data class Contact(
val contactId: Long,
@@ -675,6 +715,8 @@ data class Contact(
@Serializable
class ContactRef(
val contactId: Long,
val agentConnId: String,
val connId: Long,
var localDisplayName: String
) {
val id: ChatId get() = "@$contactId"
@@ -689,6 +731,7 @@ class ContactSubStatus(
@Serializable
data class Connection(
val connId: Long,
val agentConnId: String,
val connStatus: ConnStatus,
val connLevel: Int,
val viaGroupLink: Boolean,
@@ -697,7 +740,7 @@ data class Connection(
) {
val id: ChatId get() = ":$connId"
companion object {
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
}
}
@@ -1535,11 +1578,11 @@ sealed class CIContent: ItemContent {
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
"offered ${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, TimedMessagesPreference.ttlText(param))
allowed != FeatureAllowed.NO ->
"offered ${feature.text}"
String.format(generalGetString(R.string.feature_offered_item), feature.text, TimedMessagesPreference.ttlText(param))
else ->
"cancelled ${feature.text}"
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, TimedMessagesPreference.ttlText(param))
}
}
}
@@ -30,7 +30,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
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
@@ -77,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),
@@ -87,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)
@@ -126,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) {
@@ -147,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)) {
@@ -182,9 +191,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
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))
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, contactId, true))
.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)
}
@@ -241,12 +250,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
}
private fun chatPendingIntent(intentAction: String, chatId: String? = null, broadcast: Boolean = false): 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, 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 if (!broadcast) {
TaskStackBuilder.create(context).run {
@@ -264,18 +274,25 @@ 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 = SimplexApp.context.chatModel.callInvitations[chatId]
val invitation = m.callInvitations[chatId]
if (invitation != null) {
SimplexApp.context.chatModel.callManager.endCall(invitation = invitation)
m.callManager.endCall(invitation = invitation)
}
}
else -> {
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 {
@@ -175,6 +115,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
val context = LocalContext.current
LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item ->
Text(
@@ -185,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,13 +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.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
) ?: 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)
}
}
}
@@ -126,6 +126,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
IncomingCallLockScreenAlertLayout(
invitation,
callOnLockScreen,
chatModel,
rejectCall = { cm.endCall(invitation = invitation) },
ignoreCall = {
chatModel.activeCallInvitation.value = null
@@ -135,6 +136,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
openApp = {
val intent = Intent(context, MainActivity::class.java)
.setAction(OpenChatAction)
.putExtra("userId", invitation.user.userId)
.putExtra("chatId", invitation.contact.id)
context.startActivity(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -149,6 +151,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
fun IncomingCallLockScreenAlertLayout(
invitation: RcvCallInvitation,
callOnLockScreen: CallOnLockScreen?,
chatModel: ChatModel,
rejectCall: () -> Unit,
ignoreCall: () -> Unit,
acceptCall: () -> Unit,
@@ -160,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)
@@ -217,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,6 +33,7 @@ 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
@@ -44,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) {
@@ -66,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))
@@ -101,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 = {}
@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
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
@@ -61,39 +62,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,8 +104,8 @@ 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?) {
@Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable data 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)
@@ -115,7 +116,7 @@ sealed class WCallResponse {
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
// 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 +151,7 @@ enum class VideoCamera {
}
@Serializable
class ConnectionState(
data class ConnectionState(
val connectionState: String,
val iceConnectionState: String,
val iceGatheringState: String,
@@ -54,10 +54,14 @@ fun ChatInfoView(
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,
@@ -149,6 +153,7 @@ fun ChatInfoLayout(
chat: Chat,
contact: Contact,
connStats: ConnectionStats?,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
@@ -200,9 +205,9 @@ fun ChatInfoLayout(
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()) {
@@ -314,7 +319,7 @@ fun LocalAliasEditor(
}
@Composable
private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -346,14 +351,14 @@ private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
}
@Composable
private 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)
}
@@ -446,14 +451,14 @@ 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 = {},
@@ -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)
@@ -204,7 +210,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
}
},
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) }
@@ -1,7 +1,8 @@
@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.*
@@ -20,7 +21,8 @@ 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.*
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Reply
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
@@ -32,28 +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.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
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
@@ -132,6 +131,9 @@ data class ComposeState(
}
}
val empty: Boolean
get() = message.isEmpty() && preview is ComposePreview.NoPreview
companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
save = { json.encodeToString(serializer(), it.value) },
@@ -152,15 +154,14 @@ sealed class RecordingState {
}
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
}
}
@@ -181,21 +182,12 @@ 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 ->
@@ -242,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? ->
@@ -252,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(
@@ -353,12 +343,14 @@ fun ComposeView(
}
recState.value = RecordingState.NotStarted
textStyle.value = smallFont
chosenContent.value = emptyList()
chosenAudio.value = null
chosenFile.value = null
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,
@@ -444,35 +436,33 @@ fun ComposeView(
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.ImagePreview -> {
chosenContent.value.forEachIndexed { index, it ->
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 (chosenContent.value.lastIndex == index) msgText else "", preview.images[index]))
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText 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())
AudioPlayer.stop(chosenAudioVal.first.toFile().absolutePath)
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", chosenAudioVal.second / 1000))
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 chosenFileVal = chosenFile.value
if (chosenFileVal != null) {
val file = saveFileFromUri(context, chosenFileVal)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
}
val file = saveFileFromUri(context, preview.uri)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
}
}
}
@@ -487,7 +477,7 @@ fun ComposeView(
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
)
}
if (sent == null && chosenContent.value.isNotEmpty()) {
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)
}
}
@@ -516,7 +506,6 @@ 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))
}
@@ -539,7 +528,6 @@ fun ComposeView(
fun cancelImages() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenContent.value = emptyList()
}
fun cancelVoice() {
@@ -551,12 +539,10 @@ fun ComposeView(
AudioPlayer.stop(filePath)
filePath?.let { File(it).delete() }
}
chosenAudio.value = null
}
fun cancelFile() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenFile.value = null
}
fun truncateToWords(s: String): String {
@@ -682,7 +668,7 @@ fun ComposeView(
}
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
LaunchedEffect(allowedVoiceByPrefs) {
if (!allowedVoiceByPrefs && chosenAudio.value != null) {
if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
// Voice was disabled right when this user records it, just cancel it
cancelVoice()
}
@@ -705,6 +691,13 @@ fun ComposeView(
}
}
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
@@ -714,6 +707,17 @@ fun ComposeView(
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()
}
@@ -73,11 +73,15 @@ fun SendMsgView(
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)
NativeKeyboard(composeState, textStyle, onMessageChange)
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) }
@@ -168,6 +172,7 @@ fun SendMsgView(
private fun NativeKeyboard(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
onMessageChange: (String) -> Unit
) {
val cs = composeState.value
@@ -245,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(
@@ -256,6 +262,16 @@ 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) }
@@ -601,7 +617,7 @@ 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) },
@@ -427,8 +427,7 @@ 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),
@@ -66,11 +66,9 @@ fun GroupMemberInfoView(
withApi {
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
if (c != null) {
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
val newChat = c.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
chatModel.addChat(newChat)
chatModel.addChat(c)
chatModel.chatItems.clear()
chatModel.chatId.value = newChat.id
chatModel.chatId.value = c.id
closeAll()
}
}
@@ -28,6 +28,7 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.datetime.Clock
import kotlin.math.min
val SentColorLight = Color(0x1E45B8FF)
val ReceivedColorLight = Color(0x20B1B0B5)
@@ -241,6 +242,7 @@ fun CIMarkdownText(
}
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
const val MAX_SAFE_WIDTH_HEIGHT = 100_000
@Composable
fun PriorityLayout(
@@ -259,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)
@@ -66,7 +66,7 @@ private fun typing(w: FontWeight = FontWeight.Light): AnnotatedString =
@Composable
fun MarkdownText (
text: String,
text: CharSequence,
formattedText: List<FormattedText>? = null,
sender: String? = null,
meta: CIMeta? = null,
@@ -78,6 +78,7 @@ fun MarkdownText (
senderBold: Boolean = false,
modifier: Modifier = Modifier,
linkMode: SimplexLinkMode,
inlineContent: Map<String, InlineTextContent>? = null,
onLinkLongClick: (link: String) -> Unit = {}
) {
val textLayoutDirection = remember (text) {
@@ -132,13 +133,14 @@ fun MarkdownText (
if (formattedText == null) {
val annotatedText = buildAnnotatedString {
appendSender(this, sender, senderBold)
append(text)
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 {
@@ -44,17 +44,19 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
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, linkMode) },
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, linkMode) },
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,
@@ -299,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
}
)
@@ -407,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)
}
@@ -625,8 +627,11 @@ fun PreviewChatListNavLinkDirect() {
),
chatStats = Chat.ChatStats()
),
null,
null,
false,
null,
null,
stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION
)
@@ -663,8 +668,11 @@ fun PreviewChatListNavLinkGroup() {
),
chatStats = Chat.ChatStats()
),
null,
null,
false,
null,
null,
stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION
)
@@ -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,16 +14,15 @@ 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.connectIfOpenedViaUri
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@@ -37,13 +37,14 @@ 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)) {
@@ -63,8 +64,10 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
}
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 = {
@@ -97,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)
@@ -111,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
@@ -156,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) {
@@ -189,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) {
@@ -219,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,16 +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.*
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
@@ -21,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, linkMode: SimplexLinkMode) {
fun ChatPreviewView(
chat: Chat,
chatModelDraft: ComposeState?,
chatModelDraftChatId: ChatId?,
chatModelIncognito: Boolean,
currentUserProfileDisplayName: String?,
contactNetworkStatus: NetworkStatus?,
stopped: Boolean,
linkMode: SimplexLinkMode
) {
val cInfo = chat.chatInfo
@Composable
@@ -67,6 +82,43 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
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) {
@@ -91,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(
if (!ci.meta.itemDeleted) ci.text else generalGetString(R.string.marked_deleted_description),
if (!ci.meta.itemDeleted) ci.formattedText else null,
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,
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 {
@@ -187,7 +254,7 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
Modifier.padding(top = 52.dp),
contentAlignment = Alignment.Center
) {
ChatStatusImage(chat)
ChatStatusImage(contactNetworkStatus)
}
}
}
@@ -210,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,
@@ -221,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)
@@ -241,6 +307,6 @@ fun ChatStatusImage(chat: Chat) {
@Composable
fun PreviewChatPreviewView() {
SimpleXTheme {
ChatPreviewView(Chat.sampleData, false, "", stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
ChatPreviewView(Chat.sampleData, null, null, false, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
}
}
@@ -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
@@ -87,6 +89,8 @@ fun DatabaseView(
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) },
@@ -136,6 +140,8 @@ fun DatabaseLayout(
privacyFullBackup: SharedPreference<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
chatItemTTL: MutableState<ChatItemTTL>,
currentUser: User?,
users: List<UserInfo>,
startChat: () -> Unit,
stopChatAlert: () -> Unit,
exportArchive: () -> Unit,
@@ -148,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)
}
@@ -224,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
)
}
@@ -696,6 +717,8 @@ fun PreviewDatabaseLayout() {
privacyFullBackup = SharedPreference({ true }, {}),
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) },
currentUser = User.sampleData,
users = listOf(UserInfo.sampleData),
startChat = {},
stopChatAlert = {},
exportArchive = {},
@@ -8,11 +8,13 @@ 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.DEFAULT_PADDING
import chat.simplex.app.ui.theme.*
class AlertManager {
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
@@ -44,15 +46,25 @@ class AlertManager {
fun showAlertDialogButtonsColumn(
title: String,
text: String? = null,
text: AnnotatedString? = null,
buttons: @Composable () -> Unit,
) {
showAlert {
Dialog(onDismissRequest = this::hideAlert) {
Column(Modifier.background(MaterialTheme.colors.background)) {
Text(title, Modifier.padding(DEFAULT_PADDING), fontSize = 18.sp)
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) {
Text(text)
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()
@@ -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()
}
}
}
)
}
}
@@ -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()
}
@@ -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,5 +1,6 @@
package chat.simplex.app.views.helpers
import android.app.Application
import android.content.Context
import android.media.*
import android.media.AudioManager.AudioPlaybackCallback
@@ -14,8 +15,6 @@ 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 {
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
@@ -26,6 +25,7 @@ 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"
}
private var recorder: MediaRecorder? = null
private var progressJob: Job? = null
@@ -50,8 +50,10 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
rec.setAudioEncodingBitRate(16000)
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
rec.setMaxFileSize(recordedBytesLimit)
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val path = getAppFilePath(SimplexApp.context, uniqueCombine(SimplexApp.context, getAppFilePath(SimplexApp.context, "voice_${timestamp}.m4a")))
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()
@@ -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.*
@@ -11,6 +12,7 @@ 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.*
@@ -99,6 +101,7 @@ fun SectionItemView(
@Composable
fun SectionItemViewSpaceBetween(
click: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
minHeight: Dp = 46.dp,
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
disabled: Boolean = false,
@@ -108,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
) {
@@ -157,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),
@@ -244,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)
@@ -331,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)
@@ -355,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 ->
@@ -390,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 {
@@ -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 = {},
@@ -21,7 +21,7 @@ enum class OnboardingStage {
}
@Composable
fun CreateProfile(chatModel: ChatModel) {
fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
@@ -34,7 +34,7 @@ fun CreateProfile(chatModel: ChatModel) {
.background(color = MaterialTheme.colors.background)
.padding(20.dp)
) {
CreateProfilePanel(chatModel)
CreateProfilePanel(chatModel, close)
LaunchedEffect(Unit) {
setLastVersionDefault(chatModel)
}
@@ -226,6 +226,48 @@ private val versionDescriptions: List<VersionDescription> = listOf(
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"
)
)
)
@@ -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
@@ -113,7 +114,7 @@ fun SharedPreferenceToggleWithIcon(
) {
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,
@@ -31,6 +31,7 @@ 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
@@ -40,6 +41,7 @@ fun NetworkAndServersView(
developerTools = developerTools,
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
sessionMode = sessionMode,
showModal = showModal,
showSettingsModal = showSettingsModal,
toggleSocksProxy = { enable ->
@@ -82,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)
@@ -96,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
}
}
}
}
)
}
@@ -104,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(),
@@ -123,10 +156,12 @@ fun NetworkAndServersView(
}
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)) {
@@ -185,7 +220,6 @@ private fun UseOnionHosts(
}
}
val onSelected = showModal {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
@@ -205,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,
@@ -232,7 +299,9 @@ fun PreviewNetworkAndServersLayout() {
showSettingsModal = { {} },
toggleSocksProxy = {},
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
useOnion = {},
updateSessionMode = {},
)
}
}
@@ -29,12 +29,8 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
val updatedProfile = m.controller.apiUpdateProfile(newProfile)
if (updatedProfile != null) {
val updatedUser = user.copy(
profile = updatedProfile.toLocalProfile(user.profile.profileId),
fullPreferences = preferences
)
m.updateCurrentUser(updatedProfile, preferences)
currentPreferences = preferences
m.currentUser.value = updatedUser
}
afterSave()
}
@@ -3,6 +3,7 @@ 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.*
@@ -15,6 +16,8 @@ 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.*
@@ -76,6 +79,7 @@ fun SMPServersView(m: ChatModel) {
serversUnchanged = serversUnchanged.value,
saveDisabled = saveDisabled.value,
allServersDisabled = allServersDisabled.value,
m.currentUser.value,
addServer = {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.smp_servers_add),
@@ -156,6 +160,7 @@ private fun SMPServersLayout(
serversUnchanged: Boolean,
saveDisabled: Boolean,
allServersDisabled: Boolean,
currentUser: User?,
addServer: () -> Unit,
testServers: () -> Unit,
resetServers: () -> Unit,
@@ -186,6 +191,17 @@ private fun SMPServersLayout(
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) {
@@ -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.*
@@ -44,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,
@@ -56,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)
}
)
}
}
}
},
)
}
}
@@ -65,16 +103,6 @@ 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,
@@ -88,8 +116,8 @@ fun SettingsLayout(
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())) {
@@ -113,11 +141,13 @@ 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()
ChatPreferencesItem(showCustomModal)
ChatPreferencesItem(showCustomModal, stopped = stopped)
}
SectionSpacer()
@@ -163,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)
}
}
}
@@ -240,17 +270,18 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit))) {
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) {
SettingsActionItem(
Icons.Outlined.ToggleOn,
stringResource(R.string.chat_preferences),
click = {
click = if (stopped) null else ({
withApi {
showCustomModal { m, close ->
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
}()
}
}
}),
disabled = stopped
)
}
@@ -345,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})")
}
}
@@ -408,7 +439,7 @@ fun SettingsPreferenceItemWithInfo(
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))
@@ -469,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,
@@ -490,8 +532,8 @@ fun PreviewSettingsLayout() {
showModal = { {} },
showSettingsModal = { {} },
showCustomModal = { {} },
showTerminal = {},
// showVideoChatPrototype = {}
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)
}
}
@@ -46,9 +46,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
withApi {
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
@@ -96,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,29 @@
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))
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, info.simplexmqCommit.substring(startIndex = 0, endIndex = 7)))
}
}
@@ -0,0 +1,958 @@
<?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">Error accepting contact request</string>
<string name="error_changing_address">Error changing address</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">Send</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">Cancel live message</string>
<string name="back">Back</string>
<string name="cancel_verb">Cancel</string>
<string name="reset_verb">Reset</string>
<string name="ok">OK</string>
<string name="no_details">no details</string>
<string name="add_contact">One-time invitation link</string>
<string name="copied">Copied to clipboard</string>
<string name="add_contact_or_create_group">Start new chat</string>
<string name="create_group">Create secret group</string>
<string name="to_share_with_your_contact">(to share with your contact)</string>
<string name="only_stored_on_members_devices">(only stored by group members)</string>
<string name="toast_permission_denied">Permission Denied!</string>
<string name="use_camera_button">Use Camera</string>
<string name="from_gallery_button">From Gallery</string>
<string name="choose_file">Choose file</string>
<string name="to_start_a_new_chat_help_header">To start a new chat</string>
<string name="chat_help_tap_button">Tap button</string>
<string name="above_then_preposition_continuation">above, then:</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Add new contact</b>: to create your one-time QR Code for your contact.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scan QR code</b>: to connect to your contact who shows QR code to you.</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">Clear chat\?</string>
<string name="clear_verb">Clear</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="paste_connection_link_below_to_connect">Paste the link you received into the box below to connect with your contact.</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">Create one-time invitation link</string>
<string name="one_time_link">One-time invitation link</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">Cibuloví hostitelé nebudou použiti.</string>
<string name="network_session_mode_user">Profil chatu</string>
<string name="network_session_mode_entity">Connection</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="create_address">Create address</string>
<string name="accept_automatically">Automatically</string>
<string name="section_title_welcome_message">WELCOME MESSAGE</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">Only client devices store user profiles, contacts, groups, and messages sent with <b>2-layer end-to-end encryption</b>.</string>
<string name="onboarding_notifications_mode_title">Private notifications</string>
<string name="onboarding_notifications_mode_periodic">Periodic</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="status_e2e_encrypted">e2e encrypted</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">Remove passphrase from Keystore\?</string>
<string name="notifications_will_be_hidden">Notifications will be delivered only until the app stops!</string>
<string name="remove_passphrase">Remove</string>
<string name="update_database">Update</string>
<string name="current_passphrase">Current passphrase…</string>
<string name="update_database_passphrase">Update database passphrase</string>
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
<string name="impossible_to_recover_passphrase"><b>Please note</b>: you will NOT be able to recover or change passphrase if you lose it.</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">Create queue</string>
<string name="smp_server_test_secure_queue">Secure queue</string>
<string name="service_notifications">Instant notifications!</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>It can be disabled via settings</b> notifications will still be shown while the app is running.</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">Confirm</string>
<string name="share_invitation_link">Share invitation link</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">New passphrase…</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">Sender may have deleted the connection request.</string>
<string name="error_smp_test_server_auth">Server requires authorization to create queues, check password</string>
<string name="smp_server_test_delete_queue">Delete queue</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">If you can\'t meet in person, <b>show QR code in the video call</b>, or share the link.</string>
<string name="scan_code">Scan code</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 cibuloví hostitelé.</string>
<string name="update_network_session_mode_question">Update transport isolation mode\?</string>
<string name="app_version_code">Sestavení aplikace: %s</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you. You won\'t lose your contacts if you later delete it.</string>
<string name="share_link">Share link</string>
<string name="delete_address">Delete address</string>
<string name="full_name__field">Full name:</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">To preserve your privacy, instead of push notifications the app has a <b><xliff:g id="appName">SimpleX</xliff:g> background service</b> it uses a few percent of the battery per day.</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">Please ask your contact to enable sending voice messages.</string>
<string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
<string name="share_one_time_link">Create one-time invitation link</string>
<string name="scan_QR_code">Scan QR code</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scan or paste from clipboard)</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="transfer_images_faster">Rychlejší přenos obrázků</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">Confirm new passphrase…</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">you</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">LIVE</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">Error deleting contact</string>
<string name="error_deleting_group">Error deleting group</string>
<string name="error_deleting_contact_request">Error deleting contact request</string>
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
<string name="error_smp_test_failed_at_step">Test failed at step %s.</string>
<string name="error_smp_test_certificate">Possibly, certificate fingerprint in server address is incorrect</string>
<string name="smp_server_test_connect">Connect</string>
<string name="smp_server_test_disconnect">Disconnect</string>
<string name="error_deleting_user">Error deleting user profile</string>
<string name="icon_descr_instant_notifications">Instant notifications</string>
<string name="service_notifications_disabled">Instant notifications are disabled!</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é chaty</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">Only group owners can enable voice messages.</string>
<string name="send_live_message">Send live message</string>
<string name="live_message">Live message!</string>
<string name="connect_via_link_or_qr">Connect via link / QR code</string>
<string name="thank_you_for_installing_simplex">Thank you for installing <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="you_can_connect_to_simplex_chat_founder">You can <font color="#0088ff">connect to <xliff:g id="appNameFull">SimpleX Chat</xliff:g> developers to ask any questions and to receive updates</font>.</string>
<string name="to_connect_via_link_title">To connect via link</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">If you received <xliff:g id="appName">SimpleX Chat</xliff:g> invitation link, you can open it in your browser:</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">If you choose to reject sender will NOT be notified.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobile: tap <b>Open in mobile app</b>, then tap <b>Connect</b> in the app.</string>
<string name="reject_contact_button">Reject</string>
<string name="clear_chat_button">Clear chat</string>
<string name="clear_chat_menu_action">Clear</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">If you cannot meet in person, you can <b>scan QR code in the video call</b>, or your contact can share an invitation link.</string>
<string name="connect_via_link">Connect via link</string>
<string name="connect_button">Connect</string>
<string name="paste_button">Paste</string>
<string name="this_string_is_not_a_connection_link">This string is not a connection link!</string>
<string name="you_can_also_connect_by_clicking_the_link">You can also connect by clicking the link. If it opens in the browser, click <b>Open in mobile app</b> button.</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 hosts 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">Cibuloví 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 cibuloví 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">A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.
\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</string>
<string name="appearance_settings">Appearance</string>
<string name="app_version_title">App version</string>
<string name="app_version_name">App version: v%s</string>
<string name="core_version">Core version: v%s</string>
<string name="core_build_timestamp">Core built at: %s</string>
<string name="delete_address__question">Delete address\?</string>
<string name="all_your_contacts_will_remain_connected">All your contacts will remain connected.</string>
<string name="contact_requests">Contact requests</string>
<string name="display_name__field">Display name:</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">To protect privacy, instead of user IDs used by all other platforms, <xliff:g id="appName">SimpleX</xliff:g> has identifiers for message queues, separate for each of your contacts.</string>
<string name="many_people_asked_how_can_it_deliver">Mnoho lidí se ptalo: <i>if <xliff:g id="appName">SimpleX</xliff:g> has no user identifiers, how can it deliver messages\?</i></string>
<string name="you_control_servers_to_receive_your_contacts_to_send">You control through which server(s) <b>to receive</b> the messages, your contacts the servers you use to message them.</string>
<string name="read_more_in_github">Read more in our GitHub repository.</string>
<string name="read_more_in_github_with_link">Read more in our <font color="#0088ff">GitHub repository</font>.</string>
<string name="use_chat">Use chat</string>
<string name="onboarding_notifications_mode_subtitle">It can be changed later via settings.</string>
<string name="onboarding_notifications_mode_off">When app is running</string>
<string name="onboarding_notifications_mode_service">Instant</string>
<string name="onboarding_notifications_mode_off_desc"><b>Best for battery</b>. You will receive notifications only when the app is running, background service will NOT be used.</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Good for battery</b>. Background service checks for new messages every 10 minutes. You may miss calls and urgent messages.</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">Error changing setting</string>
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
<string name="database_encrypted">Database encrypted!</string>
<string name="error_encrypting_database">Error encrypting database</string>
<string name="encrypt_database">Encrypt</string>
<string name="encrypted_with_random_passphrase">Database is encrypted using a random passphrase, you can change it.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications.</string>
<string name="you_have_to_enter_passphrase_every_time">You have to enter passphrase every time the app starts - it is not stored on the device.</string>
<string name="encrypt_database_question">Encrypt database\?</string>
<string name="change_database_passphrase_question">Change database passphrase\?</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">connected</string>
<string name="group_member_status_complete">kompletní</string>
<string name="group_member_status_creator">tvůrce</string>
<string name="group_member_status_connecting">connecting</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>
@@ -328,11 +328,12 @@
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Zeigen Sie Ihrem Kontakt den QR-Code aus der App zum Scannen.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs anzeigen</b> oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird\nan Ihren Kontakt gesendet.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird
\nan Ihren Kontakt gesendet</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.</string>
<string name="share_invitation_link">Einladungslink teilen</string>
<string name="paste_connection_link_below_to_connect">Fügen Sie den erhaltenen Link in das Feld unten ein, um sich mit Ihrem Kontakt zu verbinden.</string>
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet.</string>
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet</string>
<!-- PasteToConnect.kt -->
<string name="connect_via_link">Über einen Link verbinden</string>
<string name="connect_button">Verbinden</string>
@@ -426,7 +427,7 @@
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Angezeigter Name:</string>
<string name="full_name__field">"Vollständiger Name:</string>
<string name="your_chat_profile">Mein Chat-Profil</string>
<string name="your_current_profile">Mein aktuelles Chat-Profil</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
<string name="edit_image">Bild bearbeiten</string>
<string name="delete_image">Bild löschen</string>
@@ -598,20 +599,19 @@
<string name="error_stopping_chat">Fehler beim Beenden des Chats</string>
<string name="error_exporting_chat_database">Fehler beim Exportieren der Chat-Datenbank</string>
<string name="import_database_question">Chat-Datenbank importieren?</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.
\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
<string name="import_database_confirmation">Importieren</string>
<string name="error_deleting_database">Fehler beim Löschen der Chat-Datenbank</string>
<string name="error_importing_database">Fehler beim Importieren der Chat-Datenbank</string>
<string name="chat_database_imported">Chat-Datenbank importiert</string>
<string name="restart_the_app_to_use_imported_chat_database">Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden.</string>
<string name="delete_chat_profile_question">Chat-Profil löschen?</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
<string name="chat_database_deleted">Chat-Datenbank gelöscht</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Starten Sie die App neu, um ein neues Chat-Profil zu erstellen.</string>
<string name="you_must_use_the_most_recent_version_of_database">Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</string>
<string name="stop_chat_to_enable_database_actions">Chat beenden, um Datenbankaktionen zu erlauben.</string>
<string name="data_section">DATEN</string>
<string name="delete_files_and_media">Dateien \&amp; Medien löschen</string>
<string name="delete_files_and_media_question">Dateien und Medien löschen?</string>
<string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string>
<string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string>
@@ -990,4 +990,55 @@
<string name="v4_2_auto_accept_contact_requests_desc">Mit optionaler Begrüßungsmeldung.</string>
<string name="v4_3_irreversible_message_deletion_desc">Ihre Kontakte können die unwiederbringliche Löschung von Nachrichten erlauben.</string>
<string name="icon_descr_cancel_live_message">Livenachricht abbrechen</string>
<string name="feature_offered_item">beginne %s</string>
<string name="feature_offered_item_with_param">beginne %s: %2s</string>
<string name="feature_cancelled_item">beende %s</string>
<string name="core_simplexmq_version">simplexmq Version: v%s (%2s)</string>
<string name="delete_files_and_media_all">Alle Dateien löschen</string>
<string name="messages_section_title">Nachrichten</string>
<string name="app_version_code">App Build: %s</string>
<string name="app_version_title">App Version</string>
<string name="app_version_name">App Version: v%s</string>
<string name="core_build_timestamp">Core übersetzt am: %s</string>
<string name="core_version">Core Version: v%s</string>
<string name="users_add">Profil hinzufügen</string>
<string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string>
<string name="users_delete_profile_for">Chat-Profil löschen für</string>
<string name="network_option_ping_count">PING Zähler</string>
<string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string>
<string name="smp_servers_per_user">Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil</string>
<string name="files_and_media_section">Dateien &amp; Medien</string>
<string name="network_session_mode_transport_isolation">Transport-Isolation</string>
<string name="users_delete_question">Chat-Profil löschen\?</string>
<string name="error_deleting_user">Fehler beim Löschen des Benutzerprofils</string>
<string name="your_chat_profiles">Meine Chat-Profile</string>
<string name="network_session_mode_entity">Verbindung</string>
<string name="network_session_mode_user">Chat-Profil</string>
<string name="delete_files_and_media_for_all_users">Dateien für alle Chat-Profile löschen</string>
<string name="network_session_mode_entity_description"><b>Für jeden Kontakt und jedes Gruppenmitglied</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.
\n
\n<b>Bitte beachten Sie</b>: Wenn Sie viele Verbindung haben, kann der Batterieverbrauch und die Datennutzung wesentlich höher sein und einige Verbindungen können scheitern.</string>
<string name="network_session_mode_user_description"><b>Für jedes von Ihnen in der App genutzte Chat-Profil</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.</string>
<string name="users_delete_data_only">Nur lokale Profildaten</string>
<string name="users_delete_with_connections">Profil und Serververbindungen</string>
<string name="messages_section_description">Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil</string>
<string name="your_chat_profiles_stored_locally">Ihre Chat-Profile werden nur lokal auf Ihrem Endgerät gespeichert</string>
<string name="failed_to_create_user_duplicate_title">Doppelter Anzeigename!</string>
<string name="failed_to_create_user_title">Fehler beim Erstellen des Profils!</string>
<string name="failed_to_active_user_title">Fehler beim Umschalten des Profils!</string>
<string name="failed_to_create_user_duplicate_desc">Sie haben schon ein Chat-Profil mit dem gleichen Anzeigenamen. Bitte wählen Sie einen anderen Namen aus.</string>
<string name="v4_5_multiple_chat_profiles">Mehrere Chat-Profile</string>
<string name="v4_5_reduced_battery_usage">Reduzierter Batterieverbrauch</string>
<string name="v4_5_private_filenames">Neutrale Dateinamen</string>
<string name="v4_5_message_draft_descr">Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren.</string>
<string name="v4_5_transport_isolation_descr">Per Chat-Profil (Voreinstellung) oder per Verbindung (BETA).</string>
<string name="v4_5_italian_interface">Italienische Bedienoberfläche</string>
<string name="v4_4_french_interface">Französische Bedienoberfläche</string>
<string name="v4_5_message_draft">Nachrichtenentwurf</string>
<string name="v4_5_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string>
<string name="v4_5_multiple_chat_profiles_descr">Unterschiedliche Namen, Avatare und Transport-Isolation.</string>
<string name="v4_5_transport_isolation">Transport-Isolation</string>
<string name="v4_5_italian_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="v4_4_french_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="v4_5_private_filenames_descr">Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen.</string>
</resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -143,7 +143,7 @@
<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 pour commencer un nouveau chat</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 dimages !</string>
<string name="share_file">Partager le fichier…</string>
@@ -360,7 +360,7 @@
<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_chat_profile">Votre profil de chat</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>
@@ -519,7 +519,6 @@
<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="data_section">DONNÉES</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>
@@ -643,7 +642,6 @@
<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">"Supprimer les fichiers 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>
@@ -778,7 +776,7 @@
<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 pour suppression).</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>
@@ -786,7 +784,7 @@
<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">Envo via</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>
@@ -862,9 +860,9 @@
<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 pour suppression).</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 peuvent 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>
@@ -879,9 +877,9 @@
<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 pouvez tous deux supprimer de manière irréversible les messages envoyés.</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 pouvez tous deux supprimer de manière irréversible les messages envoyés.</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>
@@ -919,4 +917,40 @@
<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>
</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>
@@ -3,7 +3,7 @@
<string name="simplex_link_mode">Link di SimpleX</string>
<string name="network_error_desc">Controlla la tua connessione di rete con <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> e riprova.</string>
<string name="service_notifications_disabled">Le notifiche istantanee sono disattivate!</string>
<string name="contact_connection_pending">connessione…</string>
<string name="contact_connection_pending">in connessione…</string>
<string name="attach">Allega</string>
<string name="icon_descr_cancel_image_preview">Annulla anteprima immagine</string>
<string name="images_limit_desc">Possono essere inviate solo 10 immagini alla volta</string>
@@ -17,7 +17,7 @@
<string name="connect_via_link_verb">Connetti</string>
<string name="server_connected">connesso</string>
<string name="server_error">errore</string>
<string name="server_connecting">connessione</string>
<string name="server_connecting">in connessione</string>
<string name="connected_to_server_to_receive_messages_from_contact">Sei connesso al server usato per ricevere messaggi da questo contatto.</string>
<string name="trying_to_connect_to_server_to_receive_messages">Tentativo di connessione al server usato per ricevere messaggi da questo contatto.</string>
<string name="deleted_description">eliminato</string>
@@ -32,7 +32,7 @@
<string name="invalid_data">dati non validi</string>
<string name="display_name_connection_established">connessione stabilita</string>
<string name="display_name_invited_to_connect">invitato a connettersi</string>
<string name="display_name_connecting">connessione…</string>
<string name="display_name_connecting">in connessione…</string>
<string name="description_you_shared_one_time_link">hai condiviso un link una tantum</string>
<string name="description_you_shared_one_time_link_incognito">hai condiviso un link incognito una tantum</string>
<string name="description_via_group_link">via link di gruppo</string>
@@ -50,7 +50,7 @@
<string name="ensure_smp_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi dei server SMP siano nel formato giusto, uno per riga e non doppi.</string>
<string name="error_setting_network_config">Errore di aggiornamento della configurazione di rete</string>
<string name="failed_to_parse_chat_title">Caricamento conversazione fallito</string>
<string name="failed_to_parse_chats_title">Caricamento conversazioni fallito</string>
<string name="failed_to_parse_chats_title">Caricamento delle chat fallito</string>
<string name="contact_developers">Aggiorna l\'app e contatta gli sviluppatori.</string>
<string name="connection_timeout">Connessione scaduta</string>
<string name="connection_error">Errore di connessione</string>
@@ -104,7 +104,7 @@
<string name="notifications_mode_periodic_desc">Controlla messaggi nuovi ogni 10 minuti per massimo 1 minuto</string>
<string name="notification_preview_mode_message">Testo del messaggio</string>
<string name="notification_preview_mode_contact">Nome del contatto</string>
<string name="notification_preview_mode_hidden">Nascosto</string>
<string name="notification_preview_mode_hidden">Nascosta</string>
<string name="notification_preview_mode_message_desc">Mostra contatto e messaggio</string>
<string name="notification_preview_mode_contact_desc">Mostra solo il contatto</string>
<string name="notification_display_mode_hidden_desc">Nascondi contatto e messaggio</string>
@@ -145,13 +145,13 @@
<string name="personal_welcome">Benvenuto/a <xliff:g>%1$s</xliff:g>!</string>
<string name="welcome">Benvenuto/a!</string>
<string name="this_text_is_available_in_settings">Questo testo è disponibile nelle impostazioni</string>
<string name="your_chats">Le tue conversazioni</string>
<string name="your_chats">Le tue chat</string>
<string name="group_preview_you_are_invited">sei stato invitato in un gruppo</string>
<string name="group_preview_join_as">entra come %s</string>
<string name="group_connection_pending">connessione…</string>
<string name="group_connection_pending">in connessione…</string>
<string name="tap_to_start_new_chat">Tocca per iniziare una conversazione</string>
<string name="chat_with_developers">Scrivi agli sviluppatori</string>
<string name="you_have_no_chats">Non hai conversazioni</string>
<string name="you_have_no_chats">Non hai chat</string>
<string name="share_image">Condividi immagine…</string>
<string name="share_file">Condividi file…</string>
<string name="icon_descr_context">Icona contestuale</string>
@@ -265,7 +265,7 @@
<string name="chat_item_ttl_month">1 mese</string>
<string name="error_importing_database">Errore nell\'importazione del database della chat</string>
<string name="group_full_name_field">Nome completo del gruppo:</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Se non potete incontrarvi di persona, potete <b>scansionare il codice QR nella videochiamata</b>, oppure il tuo contatto può condividere un link di invito.</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Se non potete incontrarvi di persona, puoi <b>scansionare il codice QR nella videochiamata</b>, oppure il tuo contatto può condividere un link di invito.</string>
<string name="full_backup">Backup dei dati dell\'app</string>
<string name="keychain_is_storing_securely">Android Keystore è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica.</string>
<string name="allow_your_contacts_to_send_voice_messages">Permetti ai tuoi contatti di inviare messaggi vocali.</string>
@@ -290,7 +290,6 @@
<string name="delete_link">Elimina link</string>
<string name="create_address">Crea indirizzo</string>
<string name="button_create_group_link">Crea link</string>
<string name="data_section">DATI</string>
<string name="database_encryption_will_be_updated">La password di crittografia del database verrà aggiornata e conservata nel Keystore.</string>
<string name="encrypted_with_random_passphrase">Il database è crittografato con una password casuale, puoi cambiarla.</string>
<string name="database_passphrase_is_required">La password del database è necessaria per aprire la chat.</string>
@@ -485,7 +484,7 @@
<string name="feature_enabled_for_you">attivato per te</string>
<string name="group_preferences">Preferenze del gruppo</string>
<string name="v4_2_auto_accept_contact_requests">Auto-accetta richieste di contatto</string>
<string name="ttl_d">%dd</string>
<string name="ttl_d">%dg</string>
<string name="ttl_day">%d giorno</string>
<string name="ttl_days">%d giorni</string>
<string name="delete_after">Elimina dopo</string>
@@ -497,10 +496,10 @@
<string name="ttl_min">%d min</string>
<string name="ttl_month">%d mese</string>
<string name="ttl_months">%d mesi</string>
<string name="ttl_mth">%dmth</string>
<string name="ttl_mth">%dmese</string>
<string name="ttl_s">%ds</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_w">%dw</string>
<string name="ttl_w">%dset</string>
<string name="ttl_week">%d settimana</string>
<string name="ttl_weeks">%d settimane</string>
<string name="v4_2_group_links">Link del gruppo</string>
@@ -521,9 +520,8 @@
<string name="database_will_be_encrypted">Il database verrà crittografato.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">Il database verrà crittografato e la password conservata nel Keystore.</string>
<string name="delete_files_and_media_question">Eliminare i file e i multimediali\?</string>
<string name="delete_files_and_media">"Elimina file e multimediali"</string>
<string name="delete_messages">Elimina messaggi</string>
<string name="delete_messages_after">Elimina messaggio dopo</string>
<string name="delete_messages_after">Elimina messaggi dopo</string>
<string name="total_files_count_and_size">%d file con dimensione totale di %s</string>
<string name="enable_automatic_deletion_question">Attivare l\'eliminazione automatica dei messaggi\?</string>
<string name="encrypt_database_question">Crittografare il database\?</string>
@@ -584,7 +582,7 @@
<string name="smp_servers_invalid_address">Indirizzo del server non valido!</string>
<string name="markdown_help">Aiuto sul markdown</string>
<string name="markdown_in_messages">Markdown nei messaggi</string>
<string name="mark_code_verified">Segna come verificato</string>
<string name="mark_code_verified">Segna come verificato/a</string>
<string name="one_time_link">Link di invito una tantum</string>
<string name="paste_button">Incolla</string>
<string name="paste_connection_link_below_to_connect">Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto.</string>
@@ -600,8 +598,8 @@
<string name="smp_servers_test_failed">Test del server fallito!</string>
<string name="share_invitation_link">Condividi link di invito</string>
<string name="chat_lock">SimpleX Lock</string>
<string name="is_not_verified">%s non è verificato</string>
<string name="is_verified">%s è verificato</string>
<string name="is_not_verified">%s non è verificato/a</string>
<string name="is_verified">%s è verificato/a</string>
<string name="smp_servers">Server SMP</string>
<string name="smp_servers_test_some_failed">Alcuni server hanno fallito il test:</string>
<string name="smp_servers_test_server">Testa server</string>
@@ -663,9 +661,9 @@
<string name="callstate_waiting_for_confirmation">in attesa di conferma…</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Non memorizziamo nessuno dei tuoi contatti o messaggi (una volta recapitati) sui server.</string>
<string name="section_title_welcome_message">MESSAGGIO DI BENVENUTO</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Puoi utilizzare il markdown per formattare i messaggi:</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Puoi usare il markdown per formattare i messaggi:</string>
<string name="you_control_your_chat">Sei tu a controllare la tua chat!</string>
<string name="your_chat_profile">Il tuo profilo di chat</string>
<string name="your_current_profile">Il tuo profilo attuale</string>
<string name="your_profile_is_stored_on_your_device">Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.
\n
@@ -683,7 +681,7 @@
<string name="paste_the_link_you_received">Incolla il link ricevuto</string>
<string name="people_can_connect_only_via_links_you_share">Le persone possono connettersi a te solo tramite i link che condividi.</string>
<string name="onboarding_notifications_mode_periodic">Periodico</string>
<string name="privacy_redefined">La privacy ridefinita</string>
<string name="privacy_redefined">Privacy ridefinita</string>
<string name="onboarding_notifications_mode_title">Notifiche private</string>
<string name="read_more_in_github_with_link">Maggiori informazioni nel nostro <font color="#0088ff">repository GitHub</font>.</string>
<string name="read_more_in_github">Maggiori informazioni nel nostro repository GitHub.</string>
@@ -880,7 +878,7 @@
<string name="v4_3_irreversible_message_deletion">Eliminazione irreversibile del messaggio</string>
<string name="message_deletion_prohibited_in_chat">L\'eliminazione irreversibile dei messaggi è vietata in questo gruppo.</string>
<string name="v4_3_voice_messages_desc">Max 40 secondi, ricevuto istantaneamente.</string>
<string name="new_in_version">Novità in %s</string>
<string name="new_in_version">Novità nella %s</string>
<string name="only_you_can_send_voice">Solo tu puoi inviare messaggi vocali.</string>
<string name="only_your_contact_can_send_voice">Solo il tuo contatto può inviare messaggi vocali.</string>
<string name="prohibit_message_deletion">Proibisci l\'eliminazione irreversibile dei messaggi.</string>
@@ -919,4 +917,54 @@
<string name="you_must_use_the_most_recent_version_of_database">Devi usare la versione più recente del tuo database della chat SOLO su un dispositivo, altrimenti potresti non ricevere più i messaggi da alcuni contatti.</string>
<string name="database_is_not_encrypted">Il database della chat non è crittografato: imposta la password per proteggerlo.</string>
<string name="icon_descr_cancel_live_message">Annulla messaggio in diretta</string>
<string name="feature_offered_item">offerto %s</string>
<string name="feature_offered_item_with_param">offerto %s: %2s</string>
<string name="feature_cancelled_item">annullato %s</string>
<string name="network_option_ping_count">Conteggio PING</string>
<string name="app_version_title">Versione dell\'app</string>
<string name="core_version">Versione core: v%s</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="app_version_code">Build dell\'app: %s</string>
<string name="app_version_name">Versione app: v%s</string>
<string name="core_build_timestamp">Core compilato il: %s</string>
<string name="smp_servers_per_user">I server per le nuove connessioni del profilo di chat attuale</string>
<string name="users_add">Aggiungi profilo</string>
<string name="users_delete_question">Eliminare il profilo di chat\?</string>
<string name="network_session_mode_user_description">Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni profilo di chat presente nell\'app</b>.</string>
<string name="delete_files_and_media_all">Elimina tutti i file</string>
<string name="users_delete_data_only">Solo dati del profilo locale</string>
<string name="messages_section_title">Messaggi</string>
<string name="files_and_media_section">File e multimediali</string>
<string name="update_network_session_mode_question">Aggiornare la modalità di isolamento del trasporto\?</string>
<string name="users_delete_all_chats_deleted">Tutte le chat e i messaggi verranno eliminati. Non è reversibile!</string>
<string name="network_session_mode_user">Profilo di chat</string>
<string name="network_session_mode_entity_description">Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni contatto e membro del gruppo </b>.
\n<b> Nota: </b>: se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.</string>
<string name="network_session_mode_entity">Connessione</string>
<string name="messages_section_description">Questa impostazione si applica ai messaggi del profilo di chat attuale</string>
<string name="network_session_mode_transport_isolation">Isolamento del trasporto</string>
<string name="users_delete_profile_for">Elimina il profilo di chat per</string>
<string name="delete_files_and_media_for_all_users">Elimina i file per tutti i profili di chat</string>
<string name="failed_to_active_user_title">Errore nel cambio di profilo!</string>
<string name="failed_to_create_user_title">Errore nella creazione del profilo!</string>
<string name="your_chat_profiles_stored_locally">I tuoi profili di chat sono memorizzati localmente, solo sul tuo dispositivo</string>
<string name="error_deleting_user">Errore nell\'eliminazione del profilo utente</string>
<string name="users_delete_with_connections">Profilo e connessioni al server</string>
<string name="your_chat_profiles">I tuoi profili di chat</string>
<string name="failed_to_create_user_duplicate_desc">Hai già un profilo chat con lo stesso nome da mostrare. Scegli un altro nome.</string>
<string name="failed_to_create_user_duplicate_title">Nome da mostrare doppio!</string>
<string name="v4_5_italian_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="v4_4_french_interface">Interfaccia francese</string>
<string name="v4_5_italian_interface">Interfaccia italiana</string>
<string name="v4_5_message_draft">Bozza dei messaggi</string>
<string name="v4_5_message_draft_descr">Conserva la bozza dell\'ultimo messaggio, con gli allegati.</string>
<string name="v4_5_private_filenames">Nomi di file privati</string>
<string name="v4_5_transport_isolation_descr">Per profilo di chat (predefinito) o per connessione (BETA).</string>
<string name="v4_5_reduced_battery_usage_descr">Altri miglioramenti sono in arrivo!</string>
<string name="v4_5_multiple_chat_profiles">Profili di chat multipli</string>
<string name="v4_5_reduced_battery_usage">Consumo di batteria ridotto</string>
<string name="v4_4_french_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="v4_5_transport_isolation">Isolamento del trasporto</string>
<string name="v4_5_private_filenames_descr">Per proteggere il fuso orario, i file immagine/vocali usano UTC.</string>
<string name="v4_5_multiple_chat_profiles_descr">Nomi e avatar diversi, isolamento del trasporto.</string>
</resources>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="chat_item_ttl_day">1日</string>
<string name="chat_item_ttl_week">1週間</string>
<string name="callstatus_accepted">受けた通話</string>
<string name="smp_servers_preset_add">既存サーバを追加</string>
<string name="group_member_role_admin">管理者</string>
<string name="v4_2_group_links_desc">管理者はグループの参加リンクを発行できます。</string>
<string name="network_settings">ネットワーク詳細設定</string>
<string name="chat_item_ttl_month">1ヶ月</string>
<string name="about_simplex">SimpleXについて</string>
<string name="a_plus_b">a + b</string>
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>について</string>
<string name="color_primary">アクセント色</string>
<string name="accept_contact_button">承諾</string>
<string name="accept_connection_request__question">繋がりを承諾しますか?</string>
<string name="accept">承諾</string>
<string name="accept_feature">承諾</string>
<string name="accept_call_on_lock_screen">承諾</string>
<string name="accept_contact_incognito_button">シークレットモードで承諾</string>
<string name="v4_3_improved_server_configuration_desc">QRコードでサーバを追加</string>
<string name="smp_servers_add_to_another_device">別の端末に追加</string>
<string name="users_add">プロフィールを追加</string>
<string name="smp_servers_add">サーバを追加…</string>
<string name="network_enable_socks_info">SOCKSプロキシ(ポート9050)経由で接続しますか?(※設定する前にプロキシ起動が必要※)</string>
<string name="users_delete_all_chats_deleted">全チャットとメッセージが削除されます(※元に戻せません※)!</string>
</resources>
@@ -0,0 +1,407 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="callstatus_error">oproepfout</string>
<string name="callstatus_calling">bellen…</string>
<string name="call_on_lock_screen">Oproepen op het vergrendelingsscherm:</string>
<string name="callstatus_in_progress">gesprek gaande</string>
<string name="icon_descr_call_progress">Gesprek gaande</string>
<string name="settings_section_title_calls">OPROEPEN</string>
<string name="cancel_verb">Annuleren</string>
<string name="icon_descr_cancel_file_preview">Bestandsvoorbeeld annuleren</string>
<string name="icon_descr_cancel_image_preview">Afbeeldingsvoorbeeld annuleren</string>
<string name="feature_cancelled_item">geannuleerd %s</string>
<string name="icon_descr_cancel_live_message">Live bericht annuleren</string>
<string name="snd_conn_event_switch_queue_phase_changing">veranderen van adres…</string>
<string name="notifications_mode_service">altijd aan</string>
<string name="icon_descr_asked_to_receive">Gevraagd om de afbeelding te ontvangen</string>
<string name="change_verb">Wijzig</string>
<string name="network_settings">Geavanceerde netwerkinstellingen</string>
<string name="network_enable_socks_info">Toegang tot de servers via SOCKS proxy op poort 9050\? De proxy moet worden gestart voordat u deze optie inschakelt.</string>
<string name="alert_title_cant_invite_contacts">Kan contacten niet uitnodigen</string>
<string name="allow_direct_messages">Directe berichten sturen naar leden toestaan.</string>
<string name="allow_to_delete_messages">Onherroepelijk wissen van verzonden berichten toestaan.</string>
<string name="allow_to_send_voice">Sta toe om spraakberichten te versturen.</string>
<string name="chat_is_running">Chat is aktief</string>
<string name="clear_chat_menu_action">Clear</string>
<string name="chat_database_section">CHAT DATABASE</string>
<string name="chat_archive_section">CHAT ARCHIEF</string>
<string name="chat_console">Chat console</string>
<string name="chat_database_imported">Chat database geïmporteerd</string>
<string name="chat_database_deleted">Chat database verwijderd</string>
<string name="chat_item_ttl_week">1 week</string>
<string name="a_plus_b">a + b</string>
<string name="accept_contact_button">Accepteer</string>
<string name="accept_call_on_lock_screen">Accepteer</string>
<string name="color_primary">Accent</string>
<string name="accept">Accepteer</string>
<string name="accept_connection_request__question">Verbindingsverzoek accepteren\?</string>
<string name="callstatus_accepted">aanvaarde oproep</string>
<string name="accept_contact_incognito_button">Accepteer incognito</string>
<string name="smp_servers_preset_add">Vooraf ingestelde servers toevoegen</string>
<string name="users_add">Profiel toevoegen</string>
<string name="smp_servers_add">Server toevoegen…</string>
<string name="smp_servers_add_to_another_device">Toevoegen aan een ander apparaat</string>
<string name="v4_2_group_links_desc">Admins kunnen de links naar groepen aanmaken.</string>
<string name="v4_3_improved_server_configuration_desc">Servers toevoegen door QR-codes te scannen.</string>
<string name="group_member_role_admin">admin</string>
<string name="all_group_members_will_remain_connected">Alle groepsleden blijven verbonden.</string>
<string name="allow_verb">"Sta toe."</string>
<string name="chat_item_ttl_day">1 dag</string>
<string name="accept_feature">Accepteer</string>
<string name="incognito_random_profile_from_contact_description">Een willekeurig profiel wordt gestuurd naar het contact waarvan u deze link heeft ontvangen.</string>
<string name="network_session_mode_entity_description">Er wordt een aparte TCP-verbinding (en SOCKS-credential) gebruikt <b>voor elk contact en groepslid</b>.
\n<b>Let op</b>: als u veel verbindingen hebt, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen mislukken.</string>
<string name="icon_descr_audio_call">audio-oproep</string>
<string name="icon_descr_audio_on">Geluid aan</string>
<string name="settings_audio_video_calls">Audio- en videogesprekken</string>
<string name="auto_accept_images">Afbeeldingen automatisch accepteren</string>
<string name="auth_unavailable">Verificatie niet beschikbaar</string>
<string name="back">Terug</string>
<string name="v4_2_auto_accept_contact_requests">Automatisch contactverzoeken accepteren</string>
<string name="bold">vet</string>
<string name="incognito_random_profile_description">Een willekeurig profiel wordt naar uw contactpersoon gestuurd</string>
<string name="attach">Voeg toe</string>
<string name="allow_irreversible_message_deletion_only_if">Laat onomkeerbare verwijdering van berichten alleen toe als uw contactpersoon u dat toestaat.</string>
<string name="allow_to_send_disappearing">Laat verdwijnende berichten toe.</string>
<string name="allow_your_contacts_to_send_voice_messages">Laat uw contacten spraakberichten versturen.</string>
<string name="all_your_contacts_will_remain_connected">Al uw contacten blijven verbonden.</string>
<string name="allow_voice_messages_question">Spraakberichten toestaan\?</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Goed voor de batterij</b>. De achtergronddienst controleert elke 10 minuten op nieuwe berichten. U kunt oproepen en dringende berichten missen.</string>
<string name="integrity_msg_bad_hash">Onjuiste bericht-hash</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scan QR code</b>: om verbinding te maken met uw contactpersoon die de QR code aan u toont.</string>
<string name="integrity_msg_bad_id">Onjuiste bericht-ID</string>
<string name="call_already_ended">De oproep is al beëindigd!</string>
<string name="chat_item_ttl_month">1 maand</string>
<string name="about_simplex">Over SimpleX</string>
<string name="about_simplex_chat">About <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="above_then_preposition_continuation">hierboven, dan:</string>
<string name="accept_requests">Verzoeken accepteren</string>
<string name="users_delete_all_chats_deleted">Alle chats en berichten worden verwijderd - dit kan niet ongedaan worden gemaakt!</string>
<string name="clear_chat_warning">Alle berichten worden verwijderd - dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string>
<string name="allow_disappearing_messages_only_if">Laat verdwijnende berichten alleen toe als uw contact dat toestaat.</string>
<string name="allow_voice_messages_only_if">Sta spraakberichten alleen toe als uw contactpersoon ze toestaat.</string>
<string name="allow_your_contacts_irreversibly_delete">Laat uw contacten onherroepelijk verzonden berichten verwijderen.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Laat uw contacten verdwijnende berichten sturen.</string>
<string name="chat_preferences_always">altijd</string>
<string name="icon_descr_audio_off">Geluid uit</string>
<string name="full_backup">App gegevens back-up</string>
<string name="answer_call">Beantwoord de oproep</string>
<string name="keychain_is_storing_securely">Android Keystore wordt gebruikt om passphrase veilig op te slaan - het laat notificatiedienst werken.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore wordt gebruikt om de passphrase veilig op te slaan nadat u de app opnieuw hebt opgestart of de passphrase hebt gewijzigd - hiermee kunt u meldingen ontvangen.</string>
<string name="app_version_code">App build: %s</string>
<string name="notifications_mode_off_desc">App kan alleen meldingen ontvangen als hij draait, er wordt geen achtergronddienst gestart.</string>
<string name="appearance_settings">Uiterlijk</string>
<string name="settings_section_title_icon">APP ICON</string>
<string name="app_version_title">App versie</string>
<string name="app_version_name">App-versie: v%s</string>
<string name="network_session_mode_user_description">Er wordt een aparte TCP-verbinding (en SOCKS-credential) gebruikt <b>voor elk chatprofiel dat u in de app hebt</b>.</string>
<string name="audio_call_no_encryption">audio oproep (niet e2e versleuteld)</string>
<string name="accept_automatically">Automatisch</string>
<string name="notifications_mode_service_desc">De achtergronddienst draait altijd - meldingen worden getoond zodra de berichten beschikbaar zijn.</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Nieuw contact toevoegen</b>: om uw eenmalige QR-code voor uw contactpersoon aan te maken.</string>
<string name="icon_descr_call_ended">Oproep beëindigd</string>
<string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief en schakelt de achtergronddienst en periodieke verzoeken om nieuwe berichten uit. U kunt ze opnieuw inschakelen via de instellingen.</string>
<string name="onboarding_notifications_mode_off_desc"><b>Beste voor de batterij</b>. U ontvangt alleen meldingen als de app draait, de achtergronddienst wordt NIET gebruikt.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Het kan worden uitgeschakeld via instellingen</b> - meldingen worden nog steeds getoond terwijl de app draait.</string>
<string name="both_you_and_your_contacts_can_delete">Zowel u als uw contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contact kunnen verdwijnende berichten sturen.</string>
<string name="both_you_and_your_contact_can_send_voice">Zowel u als uw contactpersoon kunnen spraakberichten versturen.</string>
<string name="impossible_to_recover_passphrase"><b>Let op</b>: u kunt de wachtwoordzin NIET herstellen of wijzigen als u deze verliest.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Verbruikt meer batterij</b>! Achtergronddienst draait altijd - meldingen worden getoond zodra de berichten beschikbaar zijn.</string>
<string name="icon_descr_cancel_link_preview">link preview annuleren</string>
<string name="callstatus_ended">Oproep beëindigd <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="database_initialization_error_title">Kan de database niet initialiseren</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">adres wijzigen voor %s…</string>
<string name="invite_prohibited">Kan contact niet uitnodigen!</string>
<string name="cannot_access_keychain">Kan geen toegang krijgen tot Keystore om database wachtwoord op te slaan</string>
<string name="cannot_receive_file">Kan het bestand niet ontvangen</string>
<string name="change_role">Rol wijzigen</string>
<string name="rcv_conn_event_switch_queue_phase_changing">veranderen van adres…</string>
<string name="rcv_conn_event_switch_queue_phase_completed">veranderd adres voor jou</string>
<string name="rcv_group_event_changed_member_role">rol van %s veranderd in %s</string>
<string name="change_member_role_question">Groepsrol wijzigen\?</string>
<string name="chat_is_stopped">Chat is gestopt</string>
<string name="notifications_mode_periodic_desc">Controleert nieuwe berichten elke 10 minuten gedurende maximaal 1 minuut</string>
<string name="rcv_group_event_changed_your_role">uw rol veranderd in %s</string>
<string name="chat_archive_header">Chat archief</string>
<string name="change_database_passphrase_question">Database wachtwoord wijzigen\?</string>
<string name="chat_is_stopped_indication">Chat is gestopt</string>
<string name="chat_preferences">Chat voorkeuren</string>
<string name="network_session_mode_user">Chat profiel</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="chat_with_developers">Chat met de ontwikkelaars</string>
<string name="smp_servers_check_address">Controleer het serveradres en probeer het opnieuw.</string>
<string name="choose_file">Kies bestand</string>
<string name="clear_verb">Clear</string>
<string name="v4_4_verify_connection_security_desc">Vergelijk beveiligingscodes met je contacten.</string>
<string name="icon_descr_contact_checked">Contact gecontroleerd</string>
<string name="notification_contact_connected">Verbonden</string>
<string name="display_name_connecting">Verbinden…</string>
<string name="connection_local_display_name">verbinding <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="connection_error">Verbindingsfout</string>
<string name="group_member_status_introduced">verbinden (geïntroduceerd)</string>
<string name="group_member_status_intro_invitation">verbinden (introductie uitnodiging)</string>
<string name="display_name_connection_established">verbinding gemaakt</string>
<string name="connection_request_sent">Verbindingsverzoek verzonden!</string>
<string name="connection_timeout">Time-out verbinding</string>
<string name="share_one_time_link">"Maak een eenmalige uitnodigings link"</string>
<string name="create_address">Adres aanmaken</string>
<string name="create_group_link">Groeps link maken</string>
<string name="create_group">Maak een geheime groep aan</string>
<string name="database_will_be_encrypted">Database wordt versleuteld.</string>
<string name="group_member_status_creator">Starter</string>
<string name="delete_address__question">Adres verwijderen\?</string>
<string name="database_passphrase_and_export">Database wachtwoord zin &amp; exporteren</string>
<string name="passphrase_is_different">De wachtwoord zin van de database verschilt van de wachtwoord zin die is opgeslagen in de keystore.</string>
<string name="ttl_d">%dd</string>
<string name="delete_verb">Verwijderen</string>
<string name="delete_after">Verwijderen na</string>
<string name="connect_via_link_verb">Verbind</string>
<string name="server_connected">verbonden</string>
<string name="server_connecting">Verbinden</string>
<string name="connect_via_contact_link">Verbinden via contact link\?</string>
<string name="connect_via_group_link">Verbinden via groeps link\?</string>
<string name="connect_via_invitation_link">Verbinden via uitnodigings link\?</string>
<string name="notification_preview_mode_contact">Contact naam</string>
<string name="notification_preview_somebody">Contact verborgen:</string>
<string name="image_decoding_exception_title">Decodeerfout</string>
<string name="maximum_supported_file_size">De momenteel maximaal ondersteunde bestandsgrootte is <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact en alle berichten worden verwijderd - dit kan niet ongedaan worden gemaakt!</string>
<string name="icon_descr_server_status_connected">Verbonden</string>
<string name="confirm_verb">Bevestigen</string>
<string name="connect_via_link_or_qr">Maak verbinding via link / QR-code</string>
<string name="copied">Gekopieerd naar het klembord</string>
<string name="contribute">Bijdragen</string>
<string name="configure_ICE_servers">ICE-servers configureren</string>
<string name="network_session_mode_entity">Verbinding</string>
<string name="core_build_timestamp">Core gebouwd op: %s</string>
<string name="core_version">Core versie: v%s</string>
<string name="callstate_connected">verbonden</string>
<string name="callstate_connecting">Verbinden…</string>
<string name="decentralized">Gedecentraliseerd</string>
<string name="create_your_profile">Maak je profiel aan</string>
<string name="ttl_day">%d dag</string>
<string name="ttl_days">%d dagen</string>
<string name="encrypted_with_random_passphrase">De database is versleuteld met een willekeurige wachtwoord zin, u kunt deze wijzigen.</string>
<string name="database_encryption_will_be_updated">De wachtwoord zin voor database codering wordt bijgewerkt en opgeslagen in de sleutel kluis.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">De database wordt gecodeerd en de wachtwoord zin wordt opgeslagen in de Keystore.</string>
<string name="database_passphrase_will_be_updated">De wachtwoordzin voor databasecodering wordt bijgewerkt.</string>
<string name="database_error">Database fout</string>
<string name="database_passphrase_is_required">Databases wachtwoord zin is vereist om de chat te openen.</string>
<string name="contact_already_exists">Contact bestaat al</string>
<string name="icon_descr_call_connecting">Oproep verbinden</string>
<string name="button_create_group_link">Maak link</string>
<string name="smp_server_test_connect">Verbind</string>
<string name="connection_error_auth">Verbindingsfout (AUTH)</string>
<string name="smp_server_test_create_queue">Maak een wachtrij</string>
<string name="auth_confirm_credential">Bevestig uw inloggegevens</string>
<string name="contact_connection_pending">Verbinden…</string>
<string name="group_connection_pending">Verbinden…</string>
<string name="icon_descr_context">Context icon</string>
<string name="copy_verb">Kopiëren</string>
<string name="clear_chat_question">Wis gesprek</string>
<string name="icon_descr_close_button">Sluiten</string>
<string name="clear_chat_button">Chat wissen</string>
<string name="alert_title_contact_connection_pending">Contact is nog niet verbonden!</string>
<string name="delete_contact_menu_action">Verwijderen</string>
<string name="delete_group_menu_action">Verwijderen</string>
<string name="clear_verification">Verwijderd verificatie</string>
<string name="connect_button">Verbind</string>
<string name="connect_via_link">Maak verbinding via link</string>
<string name="create_one_time_link">Maak een eenmalige uitnodigings link</string>
<string name="colored">gekleurd</string>
<string name="callstatus_connecting">Oproep verbinden…</string>
<string name="contact_requests">Contact verzoeken</string>
<string name="create_profile_button">Maak</string>
<string name="create_profile">Maak een profiel aan</string>
<string name="delete_address">Adres verwijderen</string>
<string name="connect_calls_via_relay">Verbinden via relais</string>
<string name="status_contact_has_e2e_encryption">contact heeft e2e encryptie</string>
<string name="status_contact_has_no_e2e_encryption">contact heeft geen e2e-encryptie</string>
<string name="set_password_to_export_desc">De database is versleuteld met een willekeurige wachtwoordzin. Wijzig dit voordat u exporteert.</string>
<string name="database_passphrase">Database-wachtwoordzin</string>
<string name="confirm_new_passphrase">Bevestig nieuwe wachtwoordzin…</string>
<string name="current_passphrase">Huidige wachtwoordzin…</string>
<string name="database_encrypted">Database versleuteld!</string>
<string name="rcv_group_event_member_connected">verbonden</string>
<string name="archive_created_on_ts">Gemaakt op <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="group_member_status_complete">compleet</string>
<string name="clear_contacts_selection_button">Duidelijk</string>
<string name="group_member_status_connected">verbonden</string>
<string name="group_member_status_connecting">Verbinden</string>
<string name="group_member_status_accepted">verbinden (geaccepteerd)</string>
<string name="group_member_status_announced">verbinden (aangekondigd)</string>
<string name="info_row_connection">Verbinding</string>
<string name="create_secret_group_title">Maak een geheime groep aan</string>
<string name="info_row_database_id">Database ID</string>
<string name="chat_preferences_contact_allows">Contact staat toe</string>
<string name="contact_preferences">Contact voorkeuren</string>
<string name="contacts_can_mark_messages_for_deletion">Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken.</string>
<string name="theme_dark">Donker</string>
<string name="chat_preferences_default">standaard (%s)</string>
<string name="delete_chat_archive_question">Chat archief verwijderen\?</string>
<string name="delete_archive">Archief verwijderen</string>
<string name="delete_contact_question">Verwijder contact\?</string>
<string name="delete_chat_profile_question">Chat profiel verwijderen\?</string>
<string name="full_deletion">Verwijderen voor iedereen</string>
<string name="delete_link">Link verwijderen</string>
<string name="conn_level_desc_direct">direct</string>
<string name="settings_section_title_develop">ONTWIKKELEN</string>
<string name="settings_section_title_device">APPARAAT</string>
<string name="delete_files_and_media_all">Verwijder alle bestanden</string>
<string name="delete_messages_after">Berichten verwijderen na</string>
<string name="direct_messages">Privéberichten</string>
<string name="ttl_month">%d maand</string>
<string name="delete_image">Verwijder afbeelding</string>
<string name="delete_database">Database verwijderen</string>
<string name="rcv_group_event_group_deleted">verwijderde groep</string>
<string name="delete_files_and_media_question">Bestanden en media verwijderen\?</string>
<string name="delete_group_question">Groep verwijderen\?</string>
<string name="delete_message__question">Verwijder bericht\?</string>
<string name="delete_messages">Verwijder berichten</string>
<string name="smp_server_test_delete_queue">Wachtrij verwijderen</string>
<string name="delete_files_and_media_for_all_users">Verwijder bestanden voor alle chatprofielen</string>
<string name="for_me_only">Verwijder voor mij</string>
<string name="button_delete_group">Groep verwijderen</string>
<string name="delete_link_question">Link verwijderen\?</string>
<string name="delete_pending_connection__question">Wachtende verbinding verwijderen\?</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scan weergegeven QR-code vanuit de app, via <b>Scan QR-code</b>.</string>
<string name="settings_developer_tools">Ontwikkel gereedschap</string>
<string name="auth_device_authentication_is_disabled_turning_off">Apparaatverificatie is uitgeschakeld. SimpleX Lock uitschakelen.</string>
<string name="display_name">Weergavenaam</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Lock inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld.</string>
<string name="direct_messages_are_prohibited_in_chat">Privéberichten tussen leden zijn in deze groep verboden.</string>
<string name="total_files_count_and_size">%d bestand(en) met een totale grootte van %s</string>
<string name="ttl_hour">%d uur</string>
<string name="no_call_on_lock_screen">Uitzetten</string>
<string name="v4_4_disappearing_messages">Verdwijnende berichten</string>
<string name="disappearing_prohibited_in_this_chat">Verdwijnende berichten zijn verboden in deze chat.</string>
<string name="auth_disable_simplex_lock">SimpleX Lock uitschakelen</string>
<string name="timed_messages">Verdwijnende berichten</string>
<string name="smp_server_test_disconnect">verbinding verbreken</string>
<string name="icon_descr_server_status_disconnected">verbinding verbroken</string>
<string name="display_name__field">Weergavenaam:</string>
<string name="display_name_cannot_contain_whitespace">Weergavenaam mag geen spatie bevatten.</string>
<string name="ttl_min">%d min</string>
<string name="ttl_months">%d maanden</string>
<string name="failed_to_create_user_title">Fout bij aanmaken van profiel!</string>
<string name="ttl_s">%ds</string>
<string name="button_delete_contact">Verwijder contact</string>
<string name="smp_servers_delete_server">Server verwijderen</string>
<string name="disappearing_messages_are_prohibited">Verdwijnende berichten zijn verboden in deze groep.</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_m">%dm</string>
<string name="ttl_mth">%dmth</string>
<string name="ttl_hours">%d uren</string>
<string name="ttl_h">%dh</string>
<string name="users_delete_question">Chat profiel verwijderen\?</string>
<string name="users_delete_profile_for">Chat profiel verwijderen voor</string>
<string name="deleted_description">verwijderd</string>
<string name="simplex_link_mode_description">Beschrijving</string>
<string name="error_receiving_file">Fout bij ontvangen van bestand</string>
<string name="error_joining_group">Fout bij lid worden van groep</string>
<string name="error_deleting_group">Fout bij verwijderen van groep</string>
<string name="error_deleting_contact">Fout bij het verwijderen van contact</string>
<string name="error_deleting_contact_request">Fout bij verwijderen van contact verzoek</string>
<string name="full_name__field">Volledige naam:</string>
<string name="error_importing_database">Fout bij het importeren van de chat database</string>
<string name="encrypt_database_question">Database versleutelen\?</string>
<string name="alert_title_no_group">Groep niet gevonden!</string>
<string name="group_display_name_field">Weergave naam groep:</string>
<string name="failed_to_create_user_duplicate_title">Dubbele weergavenaam!</string>
<string name="error_sending_message">Fout bij verzenden van bericht</string>
<string name="failed_to_active_user_title">Fout bij wisselen van profiel!</string>
<string name="error_changing_address">Fout bij wijzigen van adres</string>
<string name="error_deleting_pending_contact_connection">Fout bij het verwijderen van in behandeling zijnde contact verbinding</string>
<string name="error_deleting_user">Fout bij het verwijderen van gebruikers profiel</string>
<string name="auth_enable_simplex_lock">SimpleX Lock inschakelen</string>
<string name="hide_verb">Verbergen</string>
<string name="icon_descr_edited">bewerkt</string>
<string name="for_everybody">Voor iedereen</string>
<string name="icon_descr_server_status_error">Fout</string>
<string name="icon_descr_email">Email</string>
<string name="edit_image">Bewerk afbeelding</string>
<string name="exit_without_saving">Afsluiten zonder op te slaan</string>
<string name="full_name_optional__prompt">Volledige naam (optioneel)</string>
<string name="encrypted_video_call">e2e versleuteld videogesprek</string>
<string name="allow_accepting_calls_from_lock_screen">Schakel oproepen vanaf het vergrendelscherm in via Instellingen.</string>
<string name="icon_descr_hang_up">Ophangen</string>
<string name="settings_section_title_help">HELP</string>
<string name="settings_experimental_features">Experimentele functies</string>
<string name="error_starting_chat">Fout bij het starten van de chat</string>
<string name="export_database">Database exporteren</string>
<string name="error_deleting_database">Fout bij het verwijderen van de chat database</string>
<string name="error_exporting_chat_database">Fout bij het exporteren van de chat database</string>
<string name="error_stopping_chat">Fout bij het stoppen van de chat</string>
<string name="files_and_media_section">Bestanden en media</string>
<string name="error_changing_message_deletion">Fout bij wijzigen van instelling</string>
<string name="error_encrypting_database">Fout bij het versleutelen van de database</string>
<string name="file_with_path">Bestand: %s</string>
<string name="enter_passphrase">Voer wachtwoordzin in…</string>
<string name="icon_descr_group_inactive">Groep inactief</string>
<string name="alert_message_group_invitation_expired">Groeps uitnodiging is niet meer geldig, deze is verwijderd door de afzender.</string>
<string name="snd_group_event_group_profile_updated">groeps profiel bijgewerkt</string>
<string name="group_member_status_group_deleted">groep verwijderd</string>
<string name="icon_descr_expand_role">Vouw de rolselectie uit</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Groep wordt verwijderd voor alle leden - dit kan niet ongedaan worden gemaakt!</string>
<string name="error_creating_link_for_group">Fout bij maken van groeps link</string>
<string name="error_deleting_link_for_group">Fout bij verwijderen groeps link</string>
<string name="group_link">Groeps link</string>
<string name="error_changing_role">Fout bij wisselen van rol</string>
<string name="error_removing_member">Fout bij verwijderen van lid</string>
<string name="info_row_group">Groep</string>
<string name="group_full_name_field">Volledige naam groep:</string>
<string name="group_preferences">Groeps voorkeuren</string>
<string name="feature_enabled">ingeschakeld</string>
<string name="feature_enabled_for_contact">ingeschakeld voor contact</string>
<string name="feature_enabled_for_you">voor u ingeschakeld</string>
<string name="group_members_can_delete">Groeps leden kunnen verzonden berichten onherroepelijk verwijderen.</string>
<string name="group_members_can_send_dms">Groeps leden kunnen directe berichten sturen.</string>
<string name="group_members_can_send_voice">Groeps leden kunnen spraak berichten verzenden.</string>
<string name="v4_5_transport_isolation_descr">Per chatprofiel (standaard) of per verbinding (BETA).</string>
<string name="v4_5_multiple_chat_profiles_descr">Verschillende namen, avatars en transportisolatie.</string>
<string name="v4_4_french_interface">Franse interface</string>
<string name="error_saving_group_profile">Fout bij opslaan van groeps profiel</string>
<string name="encrypted_audio_call">e2e versleutelde audio-oproep</string>
<string name="status_e2e_encrypted">e2e versleuteld</string>
<string name="edit_verb">Bewerk</string>
<string name="enable_automatic_deletion_question">Automatisch verwijderen van berichten aanzetten\?</string>
<string name="enter_correct_passphrase">Voer de juiste wachtwoordzin in.</string>
<string name="button_edit_group_profile">Groepsprofiel bewerken</string>
<string name="network_option_enable_tcp_keep_alive">Schakel TCP-keep-alive in</string>
<string name="encrypt_database">Versleutelen</string>
<string name="error_adding_members">Fout bij het toevoegen van lid (leden)</string>
<string name="smp_servers_enter_manually">Voer de server handmatig in</string>
<string name="error_accepting_contact_request">Fout bij het accepteren van een contactverzoek</string>
<string name="group_invitation_expired">Groeps uitnodiging verlopen</string>
<string name="icon_descr_file">Bestand</string>
<string name="section_title_for_console">VOOR CONSOLE</string>
<string name="group_profile_is_stored_on_members_devices">Groeps proces wordt opgeslagen op de apparaten van de leden, niet op de servers.</string>
<string name="notification_preview_mode_hidden">Verborgen</string>
<string name="delete_group_for_self_cannot_undo_warning">De groep wordt voor u verwijderd - dit kan niet ongedaan worden gemaakt!</string>
<string name="hide_notification">Verbergen</string>
<string name="server_error">fout</string>
<string name="file_will_be_received_when_contact_is_online">Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!</string>
<string name="error_saving_file">Fout bij opslaan van bestand</string>
<string name="file_not_found">Bestand niet gevonden</string>
<string name="file_saved">Bestand opgeslagen</string>
<string name="from_gallery_button">Van Galerij</string>
<string name="error_saving_ICE_servers">Fout bij opslaan van ICE-servers</string>
<string name="callstate_ended">geëindigd</string>
<string name="group_members_can_send_disappearing">Groeps leden kunnen verdwijnende berichten sturen.</string>
<string name="ttl_week">%d week</string>
<string name="ttl_w">%dw</string>
<string name="ttl_weeks">%d weken</string>
<string name="v4_2_group_links">Groeps links</string>
<string name="encrypted_database">Versleutelde database</string>
<string name="error_with_info">Fout: %s</string>
<string name="error_creating_address">Fout bij aanmaken van adres</string>
<string name="icon_descr_help">help</string>
<string name="icon_descr_flip_camera">Flip-camera</string>
<string name="error_saving_smp_servers">Fout bij opslaan van SMP-servers</string>
<string name="error_setting_network_config">Fout bij updaten van netwerk configuratie</string>
<string name="failed_to_parse_chat_title">Kan chat niet laden</string>
<string name="failed_to_parse_chats_title">Kan chats niet laden</string>
<string name="simplex_link_mode_full">Volledige link</string>
<string name="integrity_msg_duplicate">dubbel bericht</string>
</resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -423,7 +423,7 @@
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Имя профиля:</string>
<string name="full_name__field">"Полное имя:</string>
<string name="your_chat_profile">Ваш профиль</string>
<string name="your_current_profile">Ваш активный профиль</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профиль хранится на вашем устройстве и отправляется только вашим контактам.\n\n<xliff:g id="appName">SimpleX</xliff:g> серверы не могут получить доступ к вашему профилю.</string>
<string name="edit_image">Поменять аватар</string>
<string name="delete_image">Удалить аватар</string>
@@ -612,8 +612,8 @@
<string name="restart_the_app_to_create_a_new_chat_profile">Перезапустите приложение, чтобы создать новый профиль.</string>
<string name="you_must_use_the_most_recent_version_of_database">Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе вы можете перестать получать сообщения от некоторых контактов.</string>
<string name="stop_chat_to_enable_database_actions">Остановите чат, чтобы разблокировать операции с архивом чата.</string>
<string name="data_section">ДАННЫЕ</string>
<string name="delete_files_and_media">Удалить файлы и медиа</string>
<string name="delete_files_and_media_for_all_users">Удалить файлы во всех профилях чата</string>
<string name="delete_files_and_media_all">Удалить все файлы</string>
<string name="delete_files_and_media_question">Удалить файлы и медиа?</string>
<string name="delete_files_and_media_desc">Это действие нельзя отменить — все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении.</string>
<string name="no_received_app_files">Нет полученных или отправленных файлов</string>
@@ -990,4 +990,53 @@
<string name="v4_2_group_links_desc">Админы могут создать ссылки для вступления в группу.</string>
<string name="v4_2_auto_accept_contact_requests">Автоматически принимать запросы контактов</string>
<string name="v4_2_auto_accept_contact_requests_desc">С опциональным авто-ответом.</string>
<string name="feature_offered_item">предложил(a) %s</string>
<string name="feature_offered_item_with_param">предложил(a) %s: %2s</string>
<string name="feature_cancelled_item">отменил(a) %s</string>
<string name="icon_descr_cancel_live_message">Отменить живое сообщение</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="network_option_ping_count">Количество PING</string>
<string name="users_delete_with_connections">Профиль и соединения на сервере</string>
<string name="app_version_title">Версия приложения</string>
<string name="network_session_mode_user">Профиль чата</string>
<string name="network_session_mode_entity">Соединение</string>
<string name="users_add">Добавить профиль</string>
<string name="error_deleting_user">Ошибка удаления профиля пользователя</string>
<string name="files_and_media_section">Файлы и медиа</string>
<string name="users_delete_data_only">Только локальные данные профиля</string>
<string name="messages_section_title">Сообщения</string>
<string name="smp_servers_per_user">Серверы для новых соединений вашего текущего профиля чата</string>
<string name="your_chat_profiles_stored_locally">Ваши профили чата хранятся локально, только на вашем устройстве</string>
<string name="your_chat_profiles">Ваши профили чата</string>
<string name="users_delete_all_chats_deleted">Все чаты и сообщения будут удалены - это нельзя отменить!</string>
<string name="app_version_code">Сборка приложения: %s</string>
<string name="app_version_name">Версия приложения: v%s</string>
<string name="network_session_mode_entity_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого контакта и члена группы</b>.
\n<b>Обратите внимание</b>: если у вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.</string>
<string name="network_session_mode_user_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого профиля чата, который вы имеете в приложении</b>.</string>
<string name="core_build_timestamp">Ядро скомпилировано: %s</string>
<string name="core_version">Версия ядра: v%s</string>
<string name="users_delete_question">Удалить профиль чата\?</string>
<string name="users_delete_profile_for">Удалить профиль чата для</string>
<string name="messages_section_description">Эта настройка применяется к сообщениям в вашем текущем профиле чата</string>
<string name="network_session_mode_transport_isolation">Отдельные сессии для</string>
<string name="update_network_session_mode_question">Обновить режим отдельных сессий\?</string>
<string name="failed_to_create_user_duplicate_title">Имя профиля уже используется</string>
<string name="failed_to_create_user_title">Ошибка создания профиля!</string>
<string name="failed_to_create_user_duplicate_desc">У вас уже есть профиль с таким именем. Пожалуйста, выберите другое имя.</string>
<string name="failed_to_active_user_title">Ошибка выбора профиля!</string>
<string name="v4_5_transport_isolation_descr">По профилю чата или по соединению (БЕТА)</string>
<string name="v4_4_french_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
<string name="v4_5_multiple_chat_profiles_descr">Разные имена, аватары и транспортные сессии.</string>
<string name="v4_5_italian_interface">Итальянский интерфейс</string>
<string name="v4_5_message_draft">Черновик сообщения</string>
<string name="v4_5_multiple_chat_profiles">Много профилей чата</string>
<string name="v4_5_message_draft_descr">Сохранить последний черновик, вместе с вложениями.</string>
<string name="v4_5_private_filenames">Защищенные имена файлов</string>
<string name="v4_5_italian_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
<string name="v4_5_private_filenames_descr">Чтобы защитить ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC.</string>
<string name="v4_4_french_interface">Французский интерфейс</string>
<string name="v4_5_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string>
<string name="v4_5_reduced_battery_usage">Уменьшенное потребление батареи</string>
<string name="v4_5_transport_isolation">Отдельные транспортные сессии</string>
</resources>
@@ -0,0 +1,226 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="a_plus_b">a + b</string>
<string name="chat_item_ttl_day">1天</string>
<string name="about_simplex">关于 SimpleX</string>
<string name="all_group_members_will_remain_connected">所有群组成员将保持连接。</string>
<string name="about_simplex_chat">关于 <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="above_then_preposition_continuation">以上,然后:</string>
<string name="accept_contact_button">接受</string>
<string name="accept_call_on_lock_screen">接受</string>
<string name="accept_feature">接受</string>
<string name="chat_item_ttl_month">1月</string>
<string name="chat_item_ttl_week">1周</string>
<string name="color_primary">强化</string>
<string name="callstatus_accepted">已接受通话</string>
<string name="accept">接受</string>
<string name="network_enable_socks_info">通过 SOCKS 代理访问服务器在端口9050?允许该选项前必须开始代理。</string>
<string name="smp_servers_add">添加服务器…</string>
<string name="smp_servers_add_to_another_device">添加另一设备</string>
<string name="group_member_role_admin">管理员</string>
<string name="v4_3_improved_server_configuration_desc">扫描二维码来添加服务器。</string>
<string name="network_settings">高级网络设置</string>
<string name="accept_connection_request__question">接受连接请求?</string>
<string name="accept_contact_incognito_button">接受隐身聊天</string>
<string name="v4_2_group_links_desc">管理员可以创建链接以加入群组。</string>
<string name="accept_requests">接受请求</string>
<string name="smp_servers_preset_add">添加预设服务器</string>
<string name="connect_via_link">通过链接连接</string>
<string name="display_name_connection_established">已建立连接</string>
<string name="connection_local_display_name">连接 <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="connection_error">连接错误</string>
<string name="connection_timeout">连接超时</string>
<string name="contact_already_exists">联系人已存在</string>
<string name="connection_error_auth">连接错误(AUTH</string>
<string name="answer_call">接听来电</string>
<string name="delete_chat_profile_question">删除聊天资料?</string>
<string name="delete_files_and_media_all">删除所有文件</string>
<string name="messages_section_title">消息</string>
<string name="delete_messages_after">在此后删除消息</string>
<string name="settings_section_title_messages">消息</string>
<string name="users_add">添加资料</string>
<string name="users_delete_all_chats_deleted">所有聊天记录和消息将被删除——这一行为无法撤销!</string>
<string name="clear_chat_warning">所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</string>
<string name="allow_to_send_voice">允许发送语音消息。</string>
<string name="allow_voice_messages_question">允许语音消息?</string>
<string name="delete_verb">删除</string>
<string name="delete_contact_menu_action">删除</string>
<string name="delete_group_menu_action">删除</string>
<string name="delete_address__question">删除地址?</string>
<string name="delete_after">在此后删除</string>
<string name="delete_archive">删除档案</string>
<string name="deleted_description">已删除</string>
<string name="delete_files_and_media_question">删除文件和媒体文件?</string>
<string name="full_deletion">为所有人删除</string>
<string name="for_me_only">为我删除</string>
<string name="delete_files_and_media_for_all_users">为所有聊天资料删除文件</string>
<string name="button_delete_group">删除群组</string>
<string name="delete_group_question">删除群组?</string>
<string name="delete_link">删除链接</string>
<string name="delete_link_question">删除链接?</string>
<string name="network_session_mode_entity">连接</string>
<string name="connection_request_sent">已发送连接请求!</string>
<string name="delete_message__question">删除消息?</string>
<string name="delete_messages">删除消息</string>
<string name="info_row_connection">连接</string>
<string name="connect_via_invitation_link">通过邀请链接连接?</string>
<string name="connect_via_contact_link">通过联系人链接连接?</string>
<string name="connect_via_group_link">通过群组链接连接?</string>
<string name="connect_via_link_or_qr">通过群组链接/二维码连接</string>
<string name="connect_calls_via_relay">通过继电器连接</string>
<string name="allow_your_contacts_irreversibly_delete">允许您的联系人不可撤回地删除已发送消息。</string>
<string name="chat_preferences_contact_allows">联系人允许</string>
<string name="allow_voice_messages_only_if">仅有您的联系人许可后才允许语音消息。</string>
<string name="group_info_member_you">您: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="allow_your_contacts_to_send_voice_messages">允许您的联系人发送语音消息。</string>
<string name="chat_preferences_always">一直</string>
<string name="notifications_mode_service">一直开启</string>
<string name="allow_your_contacts_to_send_disappearing_messages">允许您的联系人发送限时消息。</string>
<string name="app_version_code">应用程序构建:%s</string>
<string name="all_your_contacts_will_remain_connected">所有联系人会保持连接。</string>
<string name="allow_verb">允许</string>
<string name="allow_direct_messages">允许直接发送消息给成员。</string>
<string name="allow_to_send_disappearing">允许发送限时消息。</string>
<string name="delete_address">删除地址</string>
<string name="delete_chat_archive_question">删除聊天档案?</string>
<string name="users_delete_question">删除聊天资料?</string>
<string name="button_delete_contact">删除联系人</string>
<string name="delete_contact_question">删除联系人?</string>
<string name="rcv_group_event_group_deleted">已删除群组</string>
<string name="delete_image">删除图片</string>
<string name="allow_disappearing_messages_only_if">仅有您的联系人许可后才允许限时消息。</string>
<string name="allow_irreversible_message_deletion_only_if">仅有您的联系人许可后才允许不可撤回消息移除。</string>
<string name="allow_to_delete_messages">允许不可撤回地删除已发送消息。</string>
<string name="users_delete_profile_for">为此删除聊天资料</string>
<string name="delete_database">删除数据库</string>
<string name="keychain_allows_to_receive_ntfs">在您重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。</string>
<string name="keychain_is_storing_securely">安卓密钥库系统用来安全地保存密码——来确保通知服务运作。</string>
<string name="appearance_settings">外观</string>
<string name="app_version_title">应用程序版本</string>
<string name="full_backup">应用程序数据备份</string>
<string name="settings_section_title_icon">应用程序图标</string>
<string name="incognito_random_profile_from_contact_description">一个随机资料将被发送到收到您链接的联系人那里</string>
<string name="app_version_name">应用程序版本:v%s</string>
<string name="notifications_mode_off_desc">仅在运行时应用程序可以接受通知,不会启动后台服务</string>
<string name="incognito_random_profile_description">一个随机资料将发送给您的联系人</string>
<string name="auth_unavailable">身份验证不可用</string>
<string name="auto_accept_images">自动接受图像</string>
<string name="attach">附件</string>
<string name="icon_descr_audio_call">语音通话</string>
<string name="audio_call_no_encryption">语音通话(非端到端加密)</string>
<string name="v4_2_auto_accept_contact_requests">自动接受联系人请求</string>
<string name="integrity_msg_bad_hash">错误消息散列</string>
<string name="integrity_msg_bad_id">错误消息 ID</string>
<string name="settings_audio_video_calls">语音和视频通话</string>
<string name="accept_automatically">自动地</string>
<string name="turning_off_service_and_periodic">激活电池优化,关闭了后台服务和新消息的定期请求。您可以通过设置重新启用它们。</string>
<string name="notifications_mode_service_desc">后台服务一直在运行——一旦有消息,就会显示通知。</string>
<string name="icon_descr_audio_off">关闭音频</string>
<string name="icon_descr_audio_on">开启音频</string>
<string name="icon_descr_asked_to_receive">已要求接收图片</string>
<string name="network_session_mode_user_description">一个单独的TCP连接(和SOCKS凭证)将被用于<b>,用于您在应用程序中的每个聊天资料</b></string>
<string name="network_session_mode_entity_description">每个联系人和群组成员&lt;/b&gt; 将使用单独的 TCP 连接(和 SOCKS 凭证)&lt;b&gt;
\n&lt;b&gt;请注意&lt;/b&gt;:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。</string>
<string name="back">返回</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>添加新联系人</b>:为您的联系人创建一次性二维码。</string>
<string name="onboarding_notifications_mode_off_desc"><b> 最适合电池 </b>。您只会在应用程序运行时收到通知,不会使用后台服务。</string>
<string name="onboarding_notifications_mode_periodic_desc"><b> 适合于电池 </b>。后台服务每 10 分钟检查一次新消息。您可能会错过来电和紧急信息。</string>
<string name="bold">加粗</string>
<string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以不可逆转地删除已发送的消息。</string>
<string name="both_you_and_your_contact_can_send_disappearing">您和您的联系人都可以发送限时消息。</string>
<string name="both_you_and_your_contact_can_send_voice">您和您的联系人都可以发送语音消息。</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b> 可以通过设置禁用它 </b> - 应用程序运行时仍会显示通知。</string>
<string name="onboarding_notifications_mode_service_desc"><b> 使用更多电池 </b>!后台服务一直在运行——一旦收到消息,就会显示通知。</string>
<string name="impossible_to_recover_passphrase"><b>请注意</b>:如果您丢失密码,您将无法恢复或者更改密码。</string>
<string name="call_already_ended">通话已经结束!</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>扫描二维码</b> :与向您展示二维码的联系人联系。</string>
<string name="alert_title_cant_invite_contacts">无法邀请联系人!</string>
<string name="invite_prohibited">无法邀请联系人!</string>
<string name="cancel_verb">取消</string>
<string name="callstatus_ended">通话结束 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="change_verb">更换</string>
<string name="icon_descr_call_ended">通话结束</string>
<string name="change_database_passphrase_question">更改数据库密码?</string>
<string name="callstatus_error">通话错误</string>
<string name="rcv_conn_event_switch_queue_phase_completed">为您更改地址</string>
<string name="callstatus_in_progress">通话中</string>
<string name="icon_descr_call_progress">通话进行中</string>
<string name="callstatus_calling">呼叫中…</string>
<string name="icon_descr_cancel_live_message">取消实时消息</string>
<string name="settings_section_title_calls">通话</string>
<string name="call_on_lock_screen">在锁定屏幕上通话:</string>
<string name="icon_descr_cancel_image_preview">取消图片预览</string>
<string name="feature_cancelled_item">已取消 %s</string>
<string name="icon_descr_cancel_file_preview">取消文件预览</string>
<string name="cannot_access_keychain">无法访问密钥库来保存数据库密码</string>
<string name="cannot_receive_file">无法接收文件</string>
<string name="database_initialization_error_title">无法初始化数据库</string>
<string name="rcv_group_event_changed_member_role">将 %s 的角色更改为 %s</string>
<string name="rcv_group_event_changed_your_role">将您的角色更改为 %s</string>
<string name="change_role">改变角色</string>
<string name="change_member_role_question">更改群组角色?</string>
<string name="icon_descr_cancel_link_preview">取消链接预览</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">为 %s 更改地址…</string>
<string name="rcv_conn_event_switch_queue_phase_changing">更改地址…</string>
<string name="snd_conn_event_switch_queue_phase_changing">更改地址…</string>
<string name="create_your_profile">创建您的资料</string>
<string name="chat_database_deleted">聊天数据库已删除</string>
<string name="chat_database_imported">聊天数据库已导入</string>
<string name="keychain_error">钥匙串错误</string>
<string name="chat_archive_section">聊天档案</string>
<string name="chat_archive_header">聊天档案</string>
<string name="chat_console">聊天控制台</string>
<string name="chat_database_section">聊天数据库</string>
<string name="chat_is_stopped_indication">聊天已停止</string>
<string name="chat_is_running">聊天进行中</string>
<string name="chat_is_stopped">聊天已停止</string>
<string name="contact_preferences">联系人偏好设置</string>
<string name="your_preferences">您的偏好设置</string>
<string name="group_preferences">群组偏好设置</string>
<string name="only_group_owners_can_change_prefs">只有群主可以改变群组偏好设置。</string>
<string name="save_preferences_question">保存偏好设置?</string>
<string name="set_group_preferences">设置群组偏好设置</string>
<string name="privacy_redefined">重新定义隐私</string>
<string name="v4_3_improved_privacy_and_security">改进的隐私和安全</string>
<string name="incognito">隐身聊天</string>
<string name="joining_group">加入群组</string>
<string name="join_group_incognito_button">加入隐身聊天</string>
<string name="settings_section_title_incognito">隐身聊天模式</string>
<string name="group_unsupported_incognito_main_profile_sent">这里不支持隐身聊天模式——您的主要资料将被发送给群组成员</string>
<string name="tap_to_start_new_chat">点击开始一个新聊天</string>
<string name="incognito_random_profile">您的随机资料</string>
<string name="description_via_contact_address_link_incognito">通过联系人地址链接隐身聊天</string>
<string name="description_via_group_link_incognito">通过群组链接隐身聊天</string>
<string name="description_you_shared_one_time_link_incognito">您分享了一次性链接隐身聊天</string>
<string name="group_invitation_tap_to_join_incognito">点击以加入隐身聊天</string>
<string name="group_main_profile_sent">您的聊天资料将被发送给群组成员</string>
<string name="invite_prohibited_description">您正在尝试邀请与您共享隐身聊天资料的联系人加入您使用主要资料的群组</string>
<string name="incognito_info_protects">隐身聊天模式可以保护您的主要资料名和头像的隐私——为每个新联系人创建一个新的随机资料。</string>
<string name="alert_title_cant_invite_contacts_descr">您正在为该群组使用隐身聊天资料——为防止共享您的主要资料,邀请联系人是不允许的</string>
<string name="your_profile_will_be_sent">您的聊天资料将被发送给您的联系人</string>
<string name="description_via_one_time_link_incognito">通过一次性链接隐身聊天</string>
<string name="only_group_owners_can_enable_voice">只有群主可以启用语音信息。</string>
<string name="your_privacy">您的隐私设置</string>
<string name="privacy_and_security">隐私和安全</string>
<string name="smp_servers_save">保存服务器</string>
<string name="incognito_info_allows">它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。</string>
<string name="incognito_info_find">要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。</string>
<string name="incognito_info_share">当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。</string>
<string name="v4_3_improved_server_configuration">改进的服务器配置</string>
<string name="icon_descr_email">电邮</string>
<string name="edit_image">编辑图片</string>
<string name="button_edit_group_profile">编辑群组资料</string>
<string name="error_encrypting_database">加密数据库错误</string>
<string name="error_exporting_chat_database">导出聊天数据库错误</string>
<string name="error_importing_database">导入聊天数据库错误</string>
<string name="error_joining_group">加入群组错误</string>
<string name="error_deleting_user">删除用户资料错误</string>
<string name="passphrase_is_different">数据库密码不同于保存在密钥库中的密码。</string>
<string name="database_encryption_will_be_updated">数据库加密密码将被更新并存储在密钥库中。</string>
<string name="database_will_be_encrypted_and_passphrase_stored">数据库将被加密,密码存储在密钥库中。</string>
<string name="restore_passphrase_not_found_desc">在密匙库中没有找到密码,请手动输入。如果你使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。</string>
<string name="remove_passphrase_from_keychain">从密钥库中删除密码?</string>
<string name="save_passphrase_in_keychain">在密钥库中保存密码</string>
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> 服务</string>
<string name="settings_notifications_mode_title">通知服务</string>
</resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -63,6 +63,10 @@
<string name="failed_to_parse_chat_title">Failed to load chat</string>
<string name="failed_to_parse_chats_title">Failed to load chats</string>
<string name="contact_developers">Please update the app and contact developers.</string>
<string name="failed_to_create_user_title">Error creating profile!</string>
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
<string name="failed_to_active_user_title">Error switching profile!</string>
<!-- API Error Responses - SimpleXAPI.kt -->
<string name="connection_timeout">Connection timeout</string>
@@ -96,6 +100,7 @@
<string name="smp_server_test_secure_queue">Secure queue</string>
<string name="smp_server_test_delete_queue">Delete queue</string>
<string name="smp_server_test_disconnect">Disconnect</string>
<string name="error_deleting_user">Error deleting user profile</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Instant notifications</string>
@@ -414,6 +419,7 @@
<!-- settings - SettingsView.kt -->
<string name="your_settings">Your settings</string>
<string name="your_simplex_contact_address">Your <xliff:g id="appName">SimpleX</xliff:g> contact address</string>
<string name="your_chat_profiles">Your chat profiles</string>
<string name="database_passphrase_and_export">Database passphrase &amp; export</string>
<string name="about_simplex_chat">About <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">How to use it</string>
@@ -443,6 +449,7 @@
<string name="smp_servers_invalid_address">Invalid server address!</string>
<string name="smp_servers_check_address">Check server address and try again.</string>
<string name="smp_servers_delete_server">Delete server</string>
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
<string name="star_on_github">Star on GitHub</string>
<string name="contribute">Contribute</string>
@@ -478,7 +485,19 @@
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string>
<string name="network_session_mode_transport_isolation">Transport isolation</string>
<string name="network_session_mode_user">Chat profile</string>
<string name="network_session_mode_entity">Connection</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">A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</string>
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
<string name="appearance_settings">Appearance</string>
<string name="app_version_title">App version</string>
<string name="app_version_name">App version: v%s</string>
<string name="app_version_code">App build: %s</string>
<string name="core_version">Core version: v%s</string>
<string name="core_build_timestamp">Core built at: %s</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<!-- Address Items - UserAddressView.kt -->
<string name="create_address">Create address</string>
@@ -497,7 +516,7 @@
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Display name:</string>
<string name="full_name__field">"Full name:</string>
<string name="your_chat_profile">Your chat profile</string>
<string name="your_current_profile">Your current profile</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts.\n\n<xliff:g id="appName">SimpleX</xliff:g> servers cannot see your profile.</string>
<string name="edit_image">Edit image</string>
<string name="delete_image">Delete image</string>
@@ -708,8 +727,9 @@
<string name="restart_the_app_to_create_a_new_chat_profile">Restart the app to create a new chat profile.</string>
<string name="you_must_use_the_most_recent_version_of_database">You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</string>
<string name="stop_chat_to_enable_database_actions">Stop chat to enable database actions.</string>
<string name="data_section">DATA</string>
<string name="delete_files_and_media">Delete files \&amp; media</string>
<string name="files_and_media_section">Files &amp; media</string>
<string name="delete_files_and_media_for_all_users">Delete files for all chat profiles</string>
<string name="delete_files_and_media_all">Delete all files</string>
<string name="delete_files_and_media_question">Delete files and media?</string>
<string name="delete_files_and_media_desc">This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</string>
<string name="no_received_app_files">No received or sent files</string>
@@ -719,6 +739,8 @@
<string name="chat_item_ttl_week">1 week</string>
<string name="chat_item_ttl_month">1 month</string>
<string name="chat_item_ttl_seconds">%s second(s)</string>
<string name="messages_section_title">Messages</string>
<string name="messages_section_description">This setting applies to messages in your current chat profile</string>
<string name="delete_messages_after">Delete messages after</string>
<string name="enable_automatic_deletion_question">Enable automatic message deletion?</string>
<string name="enable_automatic_deletion_message">This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</string>
@@ -952,6 +974,7 @@
<string name="network_option_tcp_connection_timeout">TCP connection timeout</string>
<string name="network_option_protocol_timeout">Protocol timeout</string>
<string name="network_option_ping_interval">PING interval</string>
<string name="network_option_ping_count">PING count</string>
<string name="network_option_enable_tcp_keep_alive">Enable TCP keep-alive</string>
<string name="network_options_revert">Revert</string>
<string name="network_options_save">Save</string>
@@ -959,6 +982,15 @@
<string name="updating_settings_will_reconnect_client_to_all_servers">Updating settings will re-connect the client to all servers.</string>
<string name="update_network_settings_confirmation">Update</string>
<!-- UserProfilesView.kt -->
<string name="your_chat_profiles_stored_locally">Your chat profiles are stored locally, only on your device</string>
<string name="users_add">Add profile</string>
<string name="users_delete_question">Delete chat profile?</string>
<string name="users_delete_all_chats_deleted">All chats and messages will be deleted - this cannot be undone!</string>
<string name="users_delete_profile_for">Delete chat profile for</string>
<string name="users_delete_with_connections">Profile and server connections</string>
<string name="users_delete_data_only">Local profile data only</string>
<!-- Incognito mode -->
<string name="incognito">Incognito</string>
<string name="incognito_random_profile">Your random profile</string>
@@ -1060,6 +1092,9 @@
<string name="ttl_week">%d week</string>
<string name="ttl_weeks">%d weeks</string>
<string name="ttl_w">%dw</string>
<string name="feature_offered_item">offered %s</string>
<string name="feature_offered_item_with_param">offered %s: %2s</string>
<string name="feature_cancelled_item">cancelled %s</string>
<!-- WhatsNewView.kt -->
<string name="whats_new">What\'s new</string>
@@ -1084,4 +1119,18 @@
<string name="v4_4_live_messages_desc">Recipients see updates as you type them.</string>
<string name="v4_4_verify_connection_security">Verify connection security</string>
<string name="v4_4_verify_connection_security_desc">Compare security codes with your contacts.</string>
<string name="v4_4_french_interface">French interface</string>
<string name="v4_4_french_interface_descr">Thanks to the users contribute via Weblate!</string>
<string name="v4_5_multiple_chat_profiles">Multiple chat profiles</string>
<string name="v4_5_multiple_chat_profiles_descr">Different names, avatars and transport isolation.</string>
<string name="v4_5_message_draft">Message draft</string>
<string name="v4_5_message_draft_descr">Preserve the last message draft, with attachments.</string>
<string name="v4_5_transport_isolation">Transport isolation</string>
<string name="v4_5_transport_isolation_descr">By chat profile (default) or by connection (BETA).</string>
<string name="v4_5_private_filenames">Private filenames</string>
<string name="v4_5_private_filenames_descr">To protect timezone, image/voice files use UTC.</string>
<string name="v4_5_reduced_battery_usage">Reduced battery usage</string>
<string name="v4_5_reduced_battery_usage_descr">More improvements are coming soon!</string>
<string name="v4_5_italian_interface">Italian interface</string>
<string name="v4_5_italian_interface_descr">Thanks to the users contribute via Weblate!</string>
</resources>
+5 -1
View File
@@ -50,7 +50,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
m.tokenStatus = .active
} catch {
if let cr = error as? ChatResponse, case .chatCmdError(.errorAgent(.NTF(.AUTH))) = cr {
if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr {
m.tokenStatus = .expired
}
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")
@@ -77,6 +77,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) {
logger.debug("AppDelegate: applicationWillTerminate")
ChatModel.shared.filesToDelete.forEach {
removeFile($0)
}
ChatModel.shared.filesToDelete = []
terminateChat()
}
+3 -1
View File
@@ -64,7 +64,9 @@ struct ContentView: View {
alertManager.showAlert(laNoticeAlert())
} else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showWhatsNew = shouldShowWhatsNew()
if !showWhatsNew {
showWhatsNew = shouldShowWhatsNew()
}
}
}
prefShowLANotice = true
+98 -63
View File
@@ -16,6 +16,7 @@ final class ChatModel: ObservableObject {
@Published var onboardingStage: OnboardingStage?
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
@Published var currentUser: User?
@Published var users: [UserInfo] = []
@Published var chatInitialized = false
@Published var chatRunning: Bool?
@Published var chatDbChanged = false
@@ -23,6 +24,8 @@ final class ChatModel: ObservableObject {
@Published var chatDbStatus: DBMigrationResult?
// list of chat "previews"
@Published var chats: [Chat] = []
// map of connections network statuses, key is agent connection id
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
// current chat
@Published var chatId: String?
@Published var reversedChatItems: [ChatItem] = []
@@ -54,10 +57,14 @@ final class ChatModel: ObservableObject {
@Published var connReqInv: String?
// audio recording and playback
@Published var stopPreviousRecPlay: Bool = false // value is not taken into account, only the fact it switches
@Published var draft: ComposeState?
@Published var draftChatId: String?
var callWebView: WKWebView?
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
var filesToDelete: [String] = []
static let shared = ChatModel()
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
@@ -127,17 +134,9 @@ final class ChatModel: ObservableObject {
}
}
func updateNetworkStatus(_ id: ChatId, _ status: Chat.NetworkStatus) {
if let i = getChatIndex(id) {
chats[i].serverInfo.networkStatus = status
}
}
func replaceChat(_ id: String, _ chat: Chat) {
if let i = getChatIndex(id) {
let serverInfo = chats[i].serverInfo
chats[i] = chat
chats[i].serverInfo = serverInfo
} else {
// invalid state, correcting
chats.insert(chat, at: 0)
@@ -163,7 +162,7 @@ final class ChatModel: ObservableObject {
addChat(Chat(c), at: i)
}
}
NtfManager.shared.setNtfBadgeCount(totalUnreadCount())
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
}
// func addGroup(_ group: SimpleXChat.Group) {
@@ -176,7 +175,7 @@ final class ChatModel: ObservableObject {
chats[i].chatItems = [cItem]
if case .rcvNew = cItem.meta.itemStatus {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
NtfManager.shared.incNtfBadgeCount()
increaseUnreadCounter(user: currentUser!)
}
if i > 0 {
if chatId == nil {
@@ -257,9 +256,6 @@ final class ChatModel: ObservableObject {
// remove from current chat
if chatId == cInfo.id {
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
if reversedChatItems[i].isRcvNew {
NtfManager.shared.decNtfBadgeCount()
}
_ = withAnimation {
self.reversedChatItems.remove(at: i)
}
@@ -283,6 +279,18 @@ final class ChatModel: ObservableObject {
return nil
}
func updateCurrentUser(_ newProfile: Profile, _ preferences: FullPreferences? = nil) {
if let current = currentUser {
currentUser?.profile = toLocalProfile(current.profile.profileId, newProfile, "")
if let preferences = preferences {
currentUser?.fullPreferences = preferences
}
if let current = currentUser, let i = users.firstIndex(where: { $0.user.userId == current.userId }) {
users[i].user = current
}
}
}
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
let cItem = ChatItem.liveDummy(chatInfo.chatType)
withAnimation {
@@ -308,7 +316,7 @@ final class ChatModel: ObservableObject {
func markChatItemsRead(_ cInfo: ChatInfo) {
// update preview
_updateChat(cInfo.id) { chat in
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
chat.chatStats = ChatStats()
}
// update current chat
@@ -341,8 +349,8 @@ final class ChatModel: ObservableObject {
// update preview
let markedCount = chat.chatStats.unreadCount - unreadBelow
if markedCount > 0 {
NtfManager.shared.decNtfBadgeCount(by: markedCount)
chat.chatStats.unreadCount -= markedCount
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
}
}
}
@@ -360,7 +368,7 @@ final class ChatModel: ObservableObject {
func clearChat(_ cInfo: ChatInfo) {
// clear preview
if let chat = getChat(cInfo.id) {
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
chat.chatItems = []
chat.chatStats = ChatStats()
chat.chatInfo = cInfo
@@ -394,11 +402,29 @@ final class ChatModel: ObservableObject {
func decreaseUnreadCounter(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) {
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
decreaseUnreadCounter(user: currentUser!)
}
}
func totalUnreadCount() -> Int {
chats.reduce(0, { count, chat in count + chat.chatStats.unreadCount })
func increaseUnreadCounter(user: User) {
changeUnreadCounter(user: user, by: 1)
NtfManager.shared.incNtfBadgeCount()
}
func decreaseUnreadCounter(user: User, by: Int = 1) {
changeUnreadCounter(user: user, by: -by)
NtfManager.shared.decNtfBadgeCount(by: by)
}
private func changeUnreadCounter(user: User, by: Int) {
if let i = users.firstIndex(where: { $0.user.id == user.id }) {
users[i].unreadCount += by
}
}
func totalUnreadCountForAllUsers() -> Int {
chats.filter { $0.chatInfo.ntfsEnabled }.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) +
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
}
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
@@ -479,6 +505,21 @@ final class ChatModel: ObservableObject {
while i < maxIx && inView(i) { i += 1 }
return reversedChatItems[min(i - 1, maxIx)]
}
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
networkStatuses[contact.activeConn.agentConnId] = status
}
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
}
func addTerminalItem(_ item: TerminalItem) {
if terminalItems.count >= 500 {
terminalItems.remove(at: 0)
}
terminalItems.append(item)
}
}
struct UnreadChatItemCounts {
@@ -490,62 +531,18 @@ final class Chat: ObservableObject, Identifiable {
@Published var chatInfo: ChatInfo
@Published var chatItems: [ChatItem]
@Published var chatStats: ChatStats
@Published var serverInfo = ServerInfo(networkStatus: .unknown)
var created = Date.now
struct ServerInfo: Decodable {
var networkStatus: NetworkStatus
}
enum NetworkStatus: Decodable, Equatable {
case unknown
case connected
case disconnected
case error(String)
var statusString: LocalizedStringKey {
get {
switch self {
case .connected: return "connected"
case .error: return "error"
default: return "connecting"
}
}
}
var statusExplanation: LocalizedStringKey {
get {
switch self {
case .connected: return "You are connected to the server used to receive messages from this contact."
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
default: return "Trying to connect to the server used to receive messages from this contact."
}
}
}
var imageName: String {
get {
switch self {
case .unknown: return "circle.dotted"
case .connected: return "circle.fill"
case .disconnected: return "ellipsis.circle.fill"
case .error: return "exclamationmark.circle.fill"
}
}
}
}
init(_ cData: ChatData) {
self.chatInfo = cData.chatInfo
self.chatItems = cData.chatItems
self.chatStats = cData.chatStats
}
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats(), serverInfo: ServerInfo = ServerInfo(networkStatus: .unknown)) {
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats()) {
self.chatInfo = chatInfo
self.chatItems = chatItems
self.chatStats = chatStats
self.serverInfo = serverInfo
}
var id: ChatId { get { chatInfo.id } }
@@ -554,3 +551,41 @@ final class Chat: ObservableObject, Identifiable {
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
}
enum NetworkStatus: Decodable, Equatable {
case unknown
case connected
case disconnected
case error(String)
var statusString: LocalizedStringKey {
get {
switch self {
case .connected: return "connected"
case .error: return "error"
default: return "connecting"
}
}
}
var statusExplanation: LocalizedStringKey {
get {
switch self {
case .connected: return "You are connected to the server used to receive messages from this contact."
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
default: return "Trying to connect to the server used to receive messages from this contact."
}
}
}
var imageName: String {
get {
switch self {
case .unknown: return "circle.dotted"
case .connected: return "circle.fill"
case .disconnected: return "ellipsis.circle.fill"
case .error: return "exclamationmark.circle.fill"
}
}
}
}
+2 -2
View File
@@ -165,8 +165,7 @@ func saveFileFromURL(_ url: URL) -> String? {
}
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
let fileName = uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
return fileName
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
}
private func uniqueCombine(_ fileName: String) -> String {
@@ -191,6 +190,7 @@ private func getTimestamp() -> String {
df = DateFormatter()
df.dateFormat = "yyyyMMdd_HHmmss"
df.locale = Locale(identifier: "US")
df.timeZone = TimeZone(secondsFromGMT: 0)
tsFormatter = df
}
return df.string(from: Date())
+22 -12
View File
@@ -28,7 +28,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
private var granted = false
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
// Handle notification when app is in background
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
@@ -38,8 +37,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
let chatModel = ChatModel.shared
let action = response.actionIdentifier
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
if let userId = content.userInfo["userId"] as? Int64,
userId != chatModel.currentUser?.userId {
changeActiveUser(userId)
}
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
let chatId = content.userInfo["chatId"] as? String {
let chatId = content.userInfo["chatId"] as? String {
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
Task { await acceptContactRequest(contactRequest) }
} else {
@@ -83,15 +86,22 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
if UIApplication.shared.applicationState == .active {
switch content.categoryIdentifier {
case ntfCategoryMessageReceived:
let recent = recentInTheSameChat(content)
if model.chatId == nil {
// in the chat list
return recentInTheSameChat(content) ? [] : [.sound, .list]
// in the chat list...
if model.currentUser?.userId == (content.userInfo["userId"] as? Int64) {
// ... of the current user
return recent ? [] : [.sound, .list]
} else {
// ... of different user
return recent ? [.banner] : [.sound, .banner, .list]
}
} else if model.chatId == content.targetContentIdentifier {
// in the current chat
return recentInTheSameChat(content) ? [] : [.sound, .list]
return recent ? [] : [.sound, .list]
} else {
// in another chat
return recentInTheSameChat(content) ? [.banner, .list] : [.sound, .banner, .list]
return recent ? [.banner, .list] : [.sound, .banner, .list]
}
// this notification is deliverd from the notifications server
// when the app is in foreground it does not need to be shown
@@ -189,20 +199,20 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
center.delegate = self
}
func notifyContactRequest(_ contactRequest: UserContactRequest) {
func notifyContactRequest(_ user: User, _ contactRequest: UserContactRequest) {
logger.debug("NtfManager.notifyContactRequest")
addNotification(createContactRequestNtf(contactRequest))
addNotification(createContactRequestNtf(user, contactRequest))
}
func notifyContactConnected(_ contact: Contact) {
func notifyContactConnected(_ user: User, _ contact: Contact) {
logger.debug("NtfManager.notifyContactConnected")
addNotification(createContactConnectedNtf(contact))
addNotification(createContactConnectedNtf(user, contact))
}
func notifyMessageReceived(_ cInfo: ChatInfo, _ cItem: ChatItem) {
func notifyMessageReceived(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) {
logger.debug("NtfManager.notifyMessageReceived")
if cInfo.ntfsEnabled {
addNotification(createMessageReceivedNtf(cInfo, cItem))
addNotification(createMessageReceivedNtf(user, cInfo, cItem))
}
}
+290 -161
View File
@@ -94,8 +94,8 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
}
DispatchQueue.main.async {
ChatModel.shared.terminalItems.append(.cmd(.now, cmd.obfuscated))
ChatModel.shared.terminalItems.append(.resp(.now, resp))
ChatModel.shared.addTerminalItem(.cmd(.now, cmd.obfuscated))
ChatModel.shared.addTerminalItem(.resp(.now, resp))
}
return resp
}
@@ -120,7 +120,7 @@ func apiGetActiveUser() throws -> User? {
let r = chatSendCmdSync(.showActiveUser)
switch r {
case let .activeUser(user): return user
case .chatCmdError(.error(.noActiveUser)): return nil
case .chatCmdError(_, .error(.noActiveUser)): return nil
default: throw r
}
}
@@ -131,6 +131,26 @@ func apiCreateActiveUser(_ p: Profile) throws -> User {
throw r
}
func listUsers() throws -> [UserInfo] {
let r = chatSendCmdSync(.listUsers)
if case let .usersList(users) = r {
return users.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending }
}
throw r
}
func apiSetActiveUser(_ userId: Int64) throws -> User {
let r = chatSendCmdSync(.apiSetActiveUser(userId: userId))
if case let .activeUser(user) = r { return user }
throw r
}
func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool) throws {
let r = chatSendCmdSync(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues))
if case .cmdOk = r { return }
throw r
}
func apiStartChat() throws -> Bool {
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true))
switch r {
@@ -189,20 +209,21 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
}
func apiGetChats() throws -> [ChatData] {
let r = chatSendCmdSync(.apiGetChats)
if case let .apiChats(chats) = r { return chats }
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetChats: no current user") }
let r = chatSendCmdSync(.apiGetChats(userId: userId))
if case let .apiChats(_, chats) = r { return chats }
throw r
}
func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat {
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search))
if case let .apiChat(chat) = r { return Chat.init(chat) }
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
throw r
}
func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] {
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
if case let .apiChat(chat) = r { return chat.chatItems }
if case let .apiChat(_, chat) = r { return chat.chatItems }
throw r
}
@@ -227,7 +248,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
var cItem: ChatItem!
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
r = await chatSendCmd(cmd, bgTask: false)
if case let .newChatItem(aChatItem) = r {
if case let .newChatItem(_, aChatItem) = r {
cItem = aChatItem.chatItem
chatModel.messageDelivery[cItem.id] = endTask
return cItem
@@ -239,7 +260,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
return nil
} else {
r = await chatSendCmd(cmd, bgDelay: msgDelay)
if case let .newChatItem(aChatItem) = r {
if case let .newChatItem(_, aChatItem) = r {
return aChatItem.chatItem
}
sendMessageErrorAlert(r)
@@ -257,13 +278,13 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem {
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay)
if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem }
if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem }
throw r
}
func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) {
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
if case let .chatItemDeleted(deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
throw r
}
@@ -271,7 +292,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
let r = chatSendCmdSync(.apiGetNtfToken)
switch r {
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
case .chatCmdError(.errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
default:
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
return (nil, nil, .off)
@@ -310,18 +331,21 @@ func apiDeleteToken(token: DeviceToken) async throws {
}
func getUserSMPServers() throws -> ([ServerCfg], [String]) {
let r = chatSendCmdSync(.getUserSMPServers)
if case let .userSMPServers(smpServers, presetServers) = r { return (smpServers, presetServers) }
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getUserSMPServers: no current user") }
let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId))
if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) }
throw r
}
func setUserSMPServers(smpServers: [ServerCfg]) async throws {
try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers))
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setUserSMPServers: no current user") }
try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers))
}
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
let r = await chatSendCmd(.testSMPServer(smpServer: smpServer))
if case let .smpTestResult(testFailure) = r {
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
let r = await chatSendCmd(.testSMPServer(userId: userId, smpServer: smpServer))
if case let .smpTestResult(_, testFailure) = r {
if let t = testFailure {
return .failure(t)
}
@@ -331,13 +355,15 @@ func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure>
}
func getChatItemTTL() throws -> ChatItemTTL {
let r = chatSendCmdSync(.apiGetChatItemTTL)
if case let .chatItemTTL(chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getChatItemTTL: no current user") }
let r = chatSendCmdSync(.apiGetChatItemTTL(userId: userId))
if case let .chatItemTTL(_, chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
throw r
}
func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws {
try await sendCommandOkResp(.apiSetChatItemTTL(seconds: chatItemTTL.seconds))
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setChatItemTTL: no current user") }
try await sendCommandOkResp(.apiSetChatItemTTL(userId: userId, seconds: chatItemTTL.seconds))
}
func getNetworkConfig() async throws -> NetCfg? {
@@ -358,13 +384,13 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
if case let .contactInfo(_, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
throw r
}
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
throw r
}
@@ -378,44 +404,52 @@ func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
if case let .contactCode(contact, connectionCode) = r { return (contact, connectionCode) }
if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) }
throw r
}
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
if case let .groupMemberCode(_, member, connectionCode) = r { return (member, connectionCode) }
if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) }
throw r
}
func apiVerifyContact(_ contactId: Int64, connectionCode: String?) -> (Bool, String)? {
let r = chatSendCmdSync(.apiVerifyContact(contactId: contactId, connectionCode: connectionCode))
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
logger.error("apiVerifyContact error: \(String(describing: r))")
return nil
}
func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCode: String?) -> (Bool, String)? {
let r = chatSendCmdSync(.apiVerifyGroupMember(groupId: groupId, groupMemberId: groupMemberId, connectionCode: connectionCode))
if case let .connectionVerified(verified, expectedCode) = r { return (verified, expectedCode) }
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
logger.error("apiVerifyGroupMember error: \(String(describing: r))")
return nil
}
func apiAddContact() async -> String? {
let r = await chatSendCmd(.addContact, bgTask: false)
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiAddContact: no current user")
return nil
}
let r = await chatSendCmd(.apiAddContact(userId: userId), bgTask: false)
if case let .invitation(_, connReqInvitation) = r { return connReqInvitation }
connectionErrorAlert(r)
return nil
}
func apiConnect(connReq: String) async -> ConnReqType? {
let r = await chatSendCmd(.connect(connReq: connReq))
guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnect: no current user")
return nil
}
let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq))
let am = AlertManager.shared
switch r {
case .sentConfirmation: return .invitation
case .sentInvitation: return .contact
case let .contactAlreadyExists(contact):
case let .contactAlreadyExists(_, contact):
let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) {
await MainActor.run { m.chatId = c.id }
@@ -425,19 +459,19 @@ func apiConnect(connReq: String) async -> ConnReqType? {
message: "You are already connected to \(contact.displayName)."
)
return nil
case .chatCmdError(.error(.invalidConnReq)):
case .chatCmdError(_, .error(.invalidConnReq)):
am.showAlertMsg(
title: "Invalid connection link",
message: "Please check that you used the correct link or ask your contact to send you another one."
)
return nil
case .chatCmdError(.errorAgent(.SMP(.AUTH))):
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))):
am.showAlertMsg(
title: "Connection error (AUTH)",
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
)
return nil
case let .chatCmdError(.errorAgent(.INTERNAL(internalErr))):
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
if internalErr == "SEUniqueID" {
am.showAlertMsg(
title: "Already connected?",
@@ -484,7 +518,7 @@ func deleteChat(_ chat: Chat) async {
func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo {
let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false)
if case let .chatCleared(updatedChatInfo) = r { return updatedChatInfo }
if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo }
throw r
}
@@ -499,64 +533,70 @@ func clearChat(_ chat: Chat) async {
}
func apiListContacts() throws -> [Contact] {
let r = chatSendCmdSync(.listContacts)
if case let .contactsList(contacts) = r { return contacts }
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiListContacts: no current user") }
let r = chatSendCmdSync(.apiListContacts(userId: userId))
if case let .contactsList(_, contacts) = r { return contacts }
throw r
}
func apiUpdateProfile(profile: Profile) async throws -> Profile? {
let r = await chatSendCmd(.apiUpdateProfile(profile: profile))
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiUpdateProfile: no current user") }
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
switch r {
case .userProfileNoChange: return nil
case let .userProfileUpdated(_, toProfile): return toProfile
case let .userProfileUpdated(_, _, toProfile): return toProfile
default: throw r
}
}
func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? {
let r = await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences))
if case let .contactPrefsUpdated(_, toContact) = r { return toContact }
if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact }
throw r
}
func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? {
let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias))
if case let .contactAliasUpdated(toContact) = r { return toContact }
if case let .contactAliasUpdated(_, toContact) = r { return toContact }
throw r
}
func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> PendingContactConnection? {
let r = await chatSendCmd(.apiSetConnectionAlias(connId: connId, localAlias: localAlias))
if case let .connectionAliasUpdated(toConnection) = r { return toConnection }
if case let .connectionAliasUpdated(_, toConnection) = r { return toConnection }
throw r
}
func apiCreateUserAddress() async throws -> String {
let r = await chatSendCmd(.createMyAddress)
if case let .userContactLinkCreated(connReq) = r { return connReq }
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiCreateUserAddress: no current user") }
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
if case let .userContactLinkCreated(_, connReq) = r { return connReq }
throw r
}
func apiDeleteUserAddress() async throws {
let r = await chatSendCmd(.deleteMyAddress)
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiDeleteUserAddress: no current user") }
let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId))
if case .userContactLinkDeleted = r { return }
throw r
}
func apiGetUserAddress() throws -> UserContactLink? {
let r = chatSendCmdSync(.showMyAddress)
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetUserAddress: no current user") }
let r = chatSendCmdSync(.apiShowMyAddress(userId: userId))
switch r {
case let .userContactLink(contactLink): return contactLink
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
case let .userContactLink(_, contactLink): return contactLink
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
default: throw r
}
}
func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? {
let r = await chatSendCmd(.addressAutoAccept(autoAccept: autoAccept))
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("userAddressAutoAccept: no current user") }
let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept))
switch r {
case let .userContactLinkUpdated(contactLink): return contactLink
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
case let .userContactLinkUpdated(_, contactLink): return contactLink
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
default: throw r
}
}
@@ -565,8 +605,8 @@ func apiAcceptContactRequest(contactReqId: Int64) async -> Contact? {
let r = await chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
let am = AlertManager.shared
if case let .acceptingContactRequest(contact) = r { return contact }
if case .chatCmdError(.errorAgent(.SMP(.AUTH))) = r {
if case let .acceptingContactRequest(_, contact) = r { return contact }
if case .chatCmdError(_, .errorAgent(.SMP(.AUTH))) = r {
am.showAlertMsg(
title: "Connection error (AUTH)",
message: "Sender may have deleted the connection request."
@@ -595,17 +635,17 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
}
func receiveFile(fileId: Int64) async {
func receiveFile(user: User, fileId: Int64) async {
let inline = privacyTransferImagesInlineGroupDefault.get()
if let chatItem = await apiReceiveFile(fileId: fileId, inline: inline) {
DispatchQueue.main.async { chatItemSimpleUpdate(chatItem) }
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
}
}
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
let am = AlertManager.shared
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
if case .rcvFileAcceptedSndCancelled = r {
am.showAlertMsg(
title: "Cannot receive file",
@@ -614,7 +654,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
} else if !networkErrorAlert(r) {
logger.error("apiReceiveFile error: \(String(describing: r))")
switch r {
case .chatCmdError(.error(.fileAlreadyReceiving)):
case .chatCmdError(_, .error(.fileAlreadyReceiving)):
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
default:
am.showAlertMsg(
@@ -629,13 +669,13 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
func networkErrorAlert(_ r: ChatResponse) -> Bool {
let am = AlertManager.shared
switch r {
case let .chatCmdError(.errorAgent(.BROKER(addr, .TIMEOUT))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
am.showAlertMsg(
title: "Connection timeout",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
)
return true
case let .chatCmdError(.errorAgent(.BROKER(addr, .NETWORK))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
am.showAlertMsg(
title: "Connection error",
message: "Please check your network connection with \(serverHostname(addr)) and try again."
@@ -748,14 +788,15 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
}
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
let r = chatSendCmdSync(.newGroup(groupProfile: p))
if case let .groupCreated(groupInfo) = r { return groupInfo }
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiNewGroup: no current user") }
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
throw r
}
func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole))
if case let .sentGroupInvitation(_, _, member) = r { return member }
if case let .sentGroupInvitation(_, _, _, member) = r { return member }
throw r
}
@@ -768,22 +809,22 @@ enum JoinGroupResult {
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
let r = await chatSendCmd(.apiJoinGroup(groupId: groupId))
switch r {
case let .userAcceptedGroupSent(groupInfo, _): return .joined(groupInfo: groupInfo)
case .chatCmdError(.errorAgent(.SMP(.AUTH))): return .invitationRemoved
case .chatCmdError(.errorStore(.groupNotFound)): return .groupNotFound
case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo)
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): return .invitationRemoved
case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound
default: throw r
}
}
func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember {
let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false)
if case let .userDeletedMember(_, member) = r { return member }
if case let .userDeletedMember(_, _, member) = r { return member }
throw r
}
func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
let r = await chatSendCmd(.apiMemberRole(groupId: groupId, memberId: memberId, memberRole: memberRole), bgTask: false)
if case let .memberRoleUser(_, member, _, _) = r { return member }
if case let .memberRoleUser(_, _, member, _, _) = r { return member }
throw r
}
@@ -798,19 +839,19 @@ func leaveGroup(_ groupId: Int64) async {
func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo {
let r = await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false)
if case let .leftMemberUser(groupInfo) = r { return groupInfo }
if case let .leftMemberUser(_, groupInfo) = r { return groupInfo }
throw r
}
func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
let r = await chatSendCmd(.apiListMembers(groupId: groupId))
if case let .groupMembers(group) = r { return group.members }
if case let .groupMembers(_, group) = r { return group.members }
return []
}
func apiListMembersSync(_ groupId: Int64) -> [GroupMember] {
let r = chatSendCmdSync(.apiListMembers(groupId: groupId))
if case let .groupMembers(group) = r { return group.members }
if case let .groupMembers(_, group) = r { return group.members }
return []
}
@@ -824,13 +865,13 @@ func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] {
func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws -> GroupInfo {
let r = await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile))
if case let .groupUpdated(toGroup) = r { return toGroup }
if case let .groupUpdated(_, toGroup) = r { return toGroup }
throw r
}
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
if case let .groupLinkCreated(_, connReq) = r { return connReq }
if case let .groupLinkCreated(_, _, connReq) = r { return connReq }
throw r
}
@@ -843,14 +884,20 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
switch r {
case let .groupLink(_, connReq):
case let .groupLink(_, _, connReq):
return connReq
case .chatCmdError(chatError: .errorStore(storeError: .groupLinkNotFound)):
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
return nil
default: throw r
}
}
func apiGetVersion() throws -> CoreVersionInfo {
let r = chatSendCmdSync(.showVersion)
if case let .versionInfo(info) = r { return info }
throw r
}
func initializeChat(start: Bool, dbKey: String? = nil) throws {
logger.debug("initializeChat")
let m = ChatModel.shared
@@ -878,20 +925,17 @@ func startChat() throws {
let m = ChatModel.shared
try setNetworkConfig(getNetCfg())
let justStarted = try apiStartChat()
m.users = try listUsers()
if justStarted {
m.userAddress = try apiGetUserAddress()
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
m.chatItemTTL = try getChatItemTTL()
let chats = try apiGetChats()
m.chats = chats.map { Chat.init($0) }
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount())
try getUserChatData()
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
try refreshCallInvitations()
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
if let token = m.deviceToken {
registerToken(token: token)
}
withAnimation {
m.onboardingStage = m.onboardingStage == .step2_CreateProfile
m.onboardingStage = m.onboardingStage == .step2_CreateProfile && m.users.count == 1
? .step3_SetNotificationsMode
: .onboardingComplete
}
@@ -901,6 +945,30 @@ func startChat() throws {
chatLastStartGroupDefault.set(Date.now)
}
func changeActiveUser(_ userId: Int64) {
do {
try changeActiveUser_(userId)
} catch let error {
logger.error("Unable to set active user: \(responseError(error))")
}
}
func changeActiveUser_(_ userId: Int64) throws {
let m = ChatModel.shared
m.currentUser = try apiSetActiveUser(userId)
m.users = try listUsers()
try getUserChatData()
}
func getUserChatData() throws {
let m = ChatModel.shared
m.userAddress = try apiGetUserAddress()
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
m.chatItemTTL = try getChatItemTTL()
let chats = try apiGetChats()
m.chats = chats.map { Chat.init($0) }
}
class ChatReceiver {
private var receiveLoop: Task<Void, Never>?
private var receiveMessages = true
@@ -941,28 +1009,36 @@ class ChatReceiver {
func processReceivedMsg(_ res: ChatResponse) async {
let m = ChatModel.shared
await MainActor.run {
m.terminalItems.append(.resp(.now, res))
m.addTerminalItem(.resp(.now, res))
logger.debug("processReceivedMsg: \(res.responseType)")
switch res {
case let .newContactConnection(connection):
m.updateContactConnection(connection)
case let .contactConnectionDeleted(connection):
m.removeChat(connection.id)
case let .contactConnected(contact, _):
if contact.directOrUsed {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
m.updateNetworkStatus(contact.id, .connected)
NtfManager.shared.notifyContactConnected(contact)
case let .newContactConnection(user, connection):
if active(user) {
m.updateContactConnection(connection)
}
case let .contactConnecting(contact):
if contact.directOrUsed {
case let .contactConnectionDeleted(user, connection):
if active(user) {
m.removeChat(connection.id)
}
case let .contactConnected(user, contact, _):
if active(user) && contact.directOrUsed {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
}
case let .receivedContactRequest(contactRequest):
if contact.directOrUsed {
NtfManager.shared.notifyContactConnected(user, contact)
}
m.setContactNetworkStatus(contact, .connected)
case let .contactConnecting(user, contact):
if active(user) && contact.directOrUsed {
m.updateContact(contact)
m.dismissConnReqView(contact.activeConn.id)
m.removeChat(contact.activeConn.id)
}
case let .receivedContactRequest(user, contactRequest):
if !active(user) { return }
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
if m.hasChat(contactRequest.id) {
m.updateChatInfo(cInfo)
@@ -971,15 +1047,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
chatInfo: cInfo,
chatItems: []
))
NtfManager.shared.notifyContactRequest(contactRequest)
NtfManager.shared.notifyContactRequest(user, contactRequest)
}
case let .contactUpdated(toContact):
let cInfo = ChatInfo.direct(contact: toContact)
if m.hasChat(toContact.id) {
case let .contactUpdated(user, toContact):
if active(user) && m.hasChat(toContact.id) {
let cInfo = ChatInfo.direct(contact: toContact)
m.updateChatInfo(cInfo)
}
case let .contactsMerged(intoContact, mergedContact):
if m.hasChat(mergedContact.id) {
case let .contactsMerged(user, intoContact, mergedContact):
if active(user) && m.hasChat(mergedContact.id) {
if m.chatId == mergedContact.id {
m.chatId = intoContact.id
}
@@ -989,21 +1065,30 @@ func processReceivedMsg(_ res: ChatResponse) async {
updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubError(contact, chatError):
case let .contactSubError(user, contact, chatError):
if active(user) {
m.updateContact(contact)
}
processContactSubError(contact, chatError)
case let .contactSubSummary(contactSubscriptions):
case let .contactSubSummary(user, contactSubscriptions):
for sub in contactSubscriptions {
if active(user) {
m.updateContact(sub.contact)
}
if let err = sub.contactError {
processContactSubError(sub.contact, err)
} else {
m.updateContact(sub.contact)
m.updateNetworkStatus(sub.contact.id, .connected)
m.setContactNetworkStatus(sub.contact, .connected)
}
}
case let .newChatItem(aChatItem):
case let .newChatItem(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
m.addChatItem(cInfo, cItem)
if active(user) {
m.addChatItem(cInfo, cItem)
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
m.increaseUnreadCounter(user: user)
}
if let file = cItem.file,
let mc = cItem.content.msgContent,
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV {
@@ -1011,73 +1096,99 @@ func processReceivedMsg(_ res: ChatResponse) async {
if (mc.isImage && acceptImages)
|| (mc.isVoice && ((file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND && acceptImages) || cInfo.chatType == .group)) {
Task {
await receiveFile(fileId: file.fileId) // TODO check inlineFileMode != IFMSent
await receiveFile(user: user, fileId: file.fileId) // TODO check inlineFileMode != IFMSent
}
}
}
if cItem.showNotification {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
case let .chatItemStatusUpdated(aChatItem):
case let .chatItemStatusUpdated(user, aChatItem):
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
var res = false
if !cItem.isDeletedContent {
res = m.upsertChatItem(cInfo, cItem)
if !cItem.isDeletedContent && (!active(user) || m.upsertChatItem(cInfo, cItem)) {
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
}
if res {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
} else if let endTask = m.messageDelivery[cItem.id] {
if let endTask = m.messageDelivery[cItem.id] {
switch cItem.meta.itemStatus {
case .sndSent: endTask()
case .sndErrorAuth: endTask()
case .sndError: endTask()
default: break
default: ()
}
}
case let .chatItemUpdated(aChatItem):
chatItemSimpleUpdate(aChatItem)
case let .chatItemDeleted(deletedChatItem, toChatItem, _):
case let .chatItemUpdated(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
if !active(user) {
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
m.decreaseUnreadCounter(user: user)
}
return
}
if let toChatItem = toChatItem {
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
} else {
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
}
case let .receivedGroupInvitation(groupInfo, _, _):
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
case let .userAcceptedGroupSent(groupInfo, hostContact):
case let .receivedGroupInvitation(user, groupInfo, _, _):
if active(user) {
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
}
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
if !active(user) { return }
m.updateGroup(groupInfo)
if let hostContact = hostContact {
m.dismissConnReqView(hostContact.activeConn.id)
m.removeChat(hostContact.activeConn.id)
}
case let .joinedGroupMemberConnecting(groupInfo, _, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .deletedMemberUser(groupInfo, _): // TODO update user member
m.updateGroup(groupInfo)
case let .deletedMember(groupInfo, _, deletedMember):
_ = m.upsertGroupMember(groupInfo, deletedMember)
case let .leftMember(groupInfo, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .groupDeleted(groupInfo, _): // TODO update user member
m.updateGroup(groupInfo)
case let .userJoinedGroup(groupInfo):
m.updateGroup(groupInfo)
case let .joinedGroupMember(groupInfo, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .connectedToGroupMember(groupInfo, member):
_ = m.upsertGroupMember(groupInfo, member)
case let .groupUpdated(toGroup):
m.updateGroup(toGroup)
case let .rcvFileStart(aChatItem):
chatItemSimpleUpdate(aChatItem)
case let .rcvFileComplete(aChatItem):
chatItemSimpleUpdate(aChatItem)
case let .sndFileStart(aChatItem, _):
chatItemSimpleUpdate(aChatItem)
case let .sndFileComplete(aChatItem, _):
chatItemSimpleUpdate(aChatItem)
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
if active(user) {
m.updateGroup(groupInfo)
}
case let .deletedMember(user, groupInfo, _, deletedMember):
if active(user) {
_ = m.upsertGroupMember(groupInfo, deletedMember)
}
case let .leftMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .groupDeleted(user, groupInfo, _): // TODO update user member
if active(user) {
m.updateGroup(groupInfo)
}
case let .userJoinedGroup(user, groupInfo):
if active(user) {
m.updateGroup(groupInfo)
}
case let .joinedGroupMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .connectedToGroupMember(user, groupInfo, member):
if active(user) {
_ = m.upsertGroupMember(groupInfo, member)
}
case let .groupUpdated(user, toGroup):
if active(user) {
m.updateGroup(toGroup)
}
case let .rcvFileStart(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileStart(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
let cItem = aChatItem.chatItem
let mc = cItem.content.msgContent
if aChatItem.chatInfo.chatType == .direct,
@@ -1101,7 +1212,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
// logger.debug("reportNewIncomingVoIPPushPayload success for \(contact.id)")
// }
// }
case let .callOffer(contact, callType, offer, sharedKey, _):
case let .callOffer(_, contact, callType, offer, sharedKey, _):
withCall(contact) { call in
call.callState = .offerReceived
call.peerMedia = callType.media
@@ -1119,16 +1230,16 @@ func processReceivedMsg(_ res: ChatResponse) async {
relay: useRelay
)
}
case let .callAnswer(contact, answer):
case let .callAnswer(_, contact, answer):
withCall(contact) { call in
call.callState = .answerReceived
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
}
case let .callExtraInfo(contact, extraInfo):
case let .callExtraInfo(_, contact, extraInfo):
withCall(contact) { _ in
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
}
case let .callEnded(contact):
case let .callEnded(_, contact):
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
CallController.shared.reportCallRemoteEnded(invitation: invitation)
}
@@ -1152,32 +1263,38 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
}
func chatItemSimpleUpdate(_ aChatItem: AChatItem) {
func active(_ user: User) -> Bool {
user.id == ChatModel.shared.currentUser?.id
}
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) {
let m = ChatModel.shared
let cInfo = aChatItem.chatInfo
let cItem = aChatItem.chatItem
if m.upsertChatItem(cInfo, cItem) {
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
let notify = { NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) }
if !active(user) {
notify()
} else if m.upsertChatItem(cInfo, cItem) {
notify()
}
}
func updateContactsStatus(_ contactRefs: [ContactRef], status: Chat.NetworkStatus) {
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) {
let m = ChatModel.shared
for c in contactRefs {
m.updateNetworkStatus(c.id, status)
m.networkStatuses[c.agentConnId] = status
}
}
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
let m = ChatModel.shared
m.updateContact(contact)
var err: String
switch chatError {
case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network"
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
default: err = String(describing: chatError)
}
m.updateNetworkStatus(contact.id, .error(err))
m.setContactNetworkStatus(contact, .error(err))
}
func refreshCallInvitations() throws {
@@ -1209,3 +1326,15 @@ private struct UserResponse: Decodable {
var user: User?
var error: String?
}
struct RuntimeError: Error {
let message: String
init(_ message: String) {
self.message = message
}
public var localizedDescription: String {
return message
}
}
+1 -1
View File
@@ -60,7 +60,7 @@ struct SimpleXApp: App {
enteredBackground = ProcessInfo.processInfo.systemUptime
}
doAuthenticate = false
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCount())
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
case .active:
if chatModel.chatRunning == true {
ChatReceiver.shared.start()
@@ -29,6 +29,10 @@ struct IncomingCallView: View {
private func incomingCall(_ invitation: RcvCallInvitation) -> some View {
VStack(alignment: .leading, spacing: 6) {
HStack {
if m.users.count > 1 {
ProfileImage(imageStr: invitation.user.image, color: .white)
.frame(width: 24, height: 24)
}
Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
Text(invitation.callTypeText)
}
@@ -82,6 +86,8 @@ struct IncomingCallView: View {
struct IncomingCallView_Previews: PreviewProvider {
static var previews: some View {
CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData
return IncomingCallView()
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return IncomingCallView().environmentObject(m)
}
}
@@ -263,14 +263,14 @@ struct ChatInfoView: View {
.foregroundColor(.accentColor)
.font(.system(size: 14))
Spacer()
Text(chat.serverInfo.networkStatus.statusString)
Text(chatModel.contactNetworkStatus(contact).statusString)
.foregroundColor(.secondary)
serverImage()
}
}
private func serverImage() -> some View {
let status = chat.serverInfo.networkStatus
let status = chatModel.contactNetworkStatus(contact)
return Image(systemName: status.imageName)
.foregroundColor(status == .connected ? .green : .secondary)
.font(.system(size: 12))
@@ -337,7 +337,7 @@ struct ChatInfoView: View {
private func networkStatusAlert() -> Alert {
Alert(
title: Text("Network status"),
message: Text(chat.serverInfo.networkStatus.statusExplanation)
message: Text(chatModel.contactNetworkStatus(contact).statusExplanation)
)
}
@@ -63,7 +63,9 @@ struct CIFileView: View {
if fileSizeValid() {
Task {
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
await receiveFile(fileId: file.fileId)
if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: file.fileId)
}
}
} else {
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE)
@@ -35,7 +35,9 @@ struct CIImageView: View {
switch file.fileStatus {
case .rcvInvitation:
Task {
await receiveFile(fileId: file.fileId)
if let user = ChatModel.shared.currentUser {
await receiveFile(user: user, fileId: file.fileId)
}
// TODO image accepted alert?
}
case .rcvAccepted:
@@ -17,25 +17,28 @@ struct CIVoiceView: View {
@State var playbackTime: TimeInterval?
var body: some View {
VStack (
alignment: chatItem.chatDir.sent ? .trailing : .leading,
spacing: 6
) {
HStack {
if chatItem.chatDir.sent {
playerTime()
.frame(width: 50, alignment: .leading)
player()
} else {
player()
playerTime()
.frame(width: 50, alignment: .leading)
Group {
if chatItem.chatDir.sent {
VStack (alignment: .trailing, spacing: 6) {
HStack {
playerTime()
player()
}
.frame(alignment: .trailing)
metaView().padding(.trailing, 10)
}
} else {
VStack (alignment: .leading, spacing: 6) {
HStack {
player()
playerTime()
}
.frame(alignment: .leading)
metaView().padding(.leading, -6)
}
}
CIMetaView(chatItem: chatItem)
.padding(.leading, chatItem.chatDir.sent ? 0 : 12)
.padding(.trailing, chatItem.chatDir.sent ? 12 : 0)
}
.padding([.top, .horizontal], 4)
.padding(.bottom, 8)
}
@@ -58,6 +61,10 @@ struct CIVoiceView: View {
)
.foregroundColor(.secondary)
}
private func metaView() -> some View {
CIMetaView(chatItem: chatItem)
}
}
struct VoiceMessagePlayerTime: View {
@@ -66,13 +73,16 @@ struct VoiceMessagePlayerTime: View {
@Binding var playbackTime: TimeInterval?
var body: some View {
switch playbackState {
case .noPlayback:
Text(voiceMessageTime(recordingTime))
case .playing:
Text(voiceMessageTime_(playbackTime))
case .paused:
Text(voiceMessageTime_(playbackTime))
ZStack(alignment: .leading) {
Text(String("66:66")).foregroundColor(.clear)
switch playbackState {
case .noPlayback:
Text(voiceMessageTime(recordingTime))
case .playing:
Text(voiceMessageTime_(playbackTime))
case .paused:
Text(voiceMessageTime_(playbackTime))
}
}
}
}
@@ -84,7 +84,7 @@ struct MsgContentView: View {
}
}
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, preview: Bool = false) -> Text {
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text {
let s = text
var res: Text
if let ft = formattedText, ft.count > 0 {
@@ -98,6 +98,10 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
res = Text(s)
}
if let i = icon {
res = Text(Image(systemName: i)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + res
}
if let s = sender {
let t = Text(s)
return (preview ? t : t.fontWeight(.medium)) + Text(": ") + res
+17 -17
View File
@@ -15,6 +15,7 @@ private let memberImageSize: CGFloat = 34
struct ChatView: View {
@EnvironmentObject var chatModel: ChatModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@State @ObservedObject var chat: Chat
@State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false
@@ -51,7 +52,7 @@ struct ChatView: View {
}
Spacer(minLength: 0)
ComposeView(
chat: chat,
composeState: $composeState,
@@ -62,30 +63,30 @@ struct ChatView: View {
.padding(.top, 1)
.navigationTitle(cInfo.chatViewName)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(true)
.onAppear {
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
composeState = draft
}
if chat.chatStats.unreadChat {
Task {
await markChatUnread(chat, unreadChat: false)
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
chatModel.chatId = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if chatModel.chatId == nil {
chatModel.reversedChatItems = []
}
}
} label: {
HStack(spacing: 0) {
Image(systemName: "chevron.backward")
Text("Chats")
.onChange(of: chatModel.chatId) { _ in
if chatModel.chatId == nil { dismiss() }
}
.onDisappear {
if chatModel.chatId == cInfo.id {
chatModel.chatId = nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if chatModel.chatId == nil {
chatModel.reversedChatItems = []
}
}
}
}
.toolbar {
ToolbarItem(placement: .principal) {
if case let .direct(contact) = cInfo {
Button {
@@ -177,7 +178,7 @@ struct ChatView: View {
}
}
}
private func searchToolbar() -> some View {
HStack {
HStack {
@@ -233,7 +234,6 @@ struct ChatView: View {
if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) {
Task {
await apiMarkChatItemRead(cInfo, ci)
NtfManager.shared.decNtfBadgeCount()
}
}
}
@@ -14,9 +14,9 @@ import PhotosUI
enum ComposePreview {
case noPreview
case linkPreview(linkPreview: LinkPreview?)
case imagePreviews(imagePreviews: [String])
case imagePreviews(imagePreviews: [(String, UploadContent?)])
case voicePreview(recordingFileName: String, duration: Int)
case filePreview(fileName: String)
case filePreview(fileName: String, file: URL)
}
enum ComposeContextItem {
@@ -161,6 +161,10 @@ struct ComposeState {
default: return true
}
}
var empty: Bool {
message == "" && noPreview
}
}
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
@@ -171,11 +175,12 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
case let .link(_, preview: preview):
chatItemPreview = .linkPreview(linkPreview: preview)
case let .image(_, image):
chatItemPreview = .imagePreviews(imagePreviews: [image])
chatItemPreview = .imagePreviews(imagePreviews: [(image, nil)])
case let .voice(_, duration):
chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
case .file:
chatItemPreview = .filePreview(fileName: chatItem.file?.fileName ?? "")
let fileName = chatItem.file?.fileName ?? ""
chatItemPreview = .filePreview(fileName: fileName, file: getAppFilePath(fileName))
default:
chatItemPreview = .noPreview
}
@@ -229,7 +234,6 @@ struct ComposeView: View {
@State private var showTakePhoto = false
@State var chosenImages: [UploadContent] = []
@State private var showFileImporter = false
@State var chosenFile: URL? = nil
@State private var audioRecorder: AudioRecorder?
@State private var voiceMessageRecordingTime: TimeInterval?
@@ -337,10 +341,10 @@ struct ComposeView: View {
}
.onChange(of: chosenImages) { images in
Task {
var imgs: [String] = []
var imgs: [(String, UploadContent)] = []
for image in images {
if let img = resizeImageToStrSize(image.uiImage, maxDataSize: 14000) {
imgs.append(img)
imgs.append((img, image))
await MainActor.run {
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs))
}
@@ -367,9 +371,8 @@ struct ComposeView: View {
}
fileURL.stopAccessingSecurityScopedResource()
if let fileSize = fileSize,
fileSize <= MAX_FILE_SIZE {
chosenFile = fileURL
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent))
fileSize <= MAX_FILE_SIZE {
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL))
} else {
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE)
AlertManager.shared.showAlertMsg(
@@ -383,12 +386,18 @@ struct ComposeView: View {
}
}
.onDisappear {
if let fileName = composeState.voiceMessageRecordingFileName {
cancelVoiceMessageRecording(fileName)
}
if composeState.liveMessage != nil && (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
if composeState.liveMessage != nil
&& (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
cancelCurrentVoiceRecording()
clearCurrentDraft()
sendMessage()
resetLinkPreview()
} else if !composeState.empty {
saveCurrentDraft()
} else {
cancelCurrentVoiceRecording()
clearCurrentDraft()
clearState()
}
chatModel.removeLiveDummy(animated: false)
}
@@ -405,6 +414,12 @@ struct ComposeView: View {
if !vmAllowed && composeState.voicePreview,
let fileName = composeState.voiceMessageRecordingFileName {
cancelVoiceMessageRecording(fileName)
clearState()
}
}
.onAppear {
if case let .voicePreview(_, duration) = composeState.preview {
voiceMessageRecordingTime = TimeInterval(duration)
}
}
}
@@ -471,7 +486,7 @@ struct ComposeView: View {
ComposeLinkView(linkPreview: preview, cancelPreview: cancelLinkPreview)
case let .imagePreviews(imagePreviews: images):
ComposeImageView(
images: images,
images: images.map { (img, _) in img },
cancelImage: {
composeState = composeState.copy(preview: .noPreview)
chosenImages = []
@@ -482,16 +497,18 @@ struct ComposeView: View {
recordingFileName: recordingFileName,
recordingTime: $voiceMessageRecordingTime,
recordingState: $composeState.voiceMessageRecordingState,
cancelVoiceMessage: { cancelVoiceMessageRecording($0) },
cancelVoiceMessage: {
cancelVoiceMessageRecording($0)
clearState()
},
cancelEnabled: !composeState.editing,
stopPlayback: $stopPlayback
)
case let .filePreview(fileName: fileName):
case let .filePreview(fileName, _):
ComposeFileView(
fileName: fileName,
cancelFile: {
composeState = composeState.copy(preview: .noPreview)
chosenFile = nil
},
cancelEnabled: !composeState.editing)
}
@@ -548,25 +565,23 @@ struct ComposeView: View {
case .linkPreview:
sent = await send(checkLinkPreview(), quoted: quoted, live: live)
case let .imagePreviews(imagePreviews: images):
let last = min(chosenImages.count, images.count) - 1
for i in 0..<last {
if let savedFile = saveAnyImage(chosenImages[i]) {
_ = await send(.image(text: "", image: images[i]), quoted: nil, file: savedFile)
let last = images.count - 1
if last >= 0 {
for i in 0..<last {
sent = await sendImage(images[i])
_ = try? await Task.sleep(nanoseconds: 100_000000)
}
_ = try? await Task.sleep(nanoseconds: 100_000000)
}
if let savedFile = saveAnyImage(chosenImages[last]) {
sent = await send(.image(text: msgText, image: images[last]), quoted: quoted, file: savedFile, live: live)
sent = await sendImage(images[last], text: msgText, quoted: quoted, live: live)
}
if sent == nil {
sent = await send(.text(msgText), quoted: quoted, live: live)
}
case let .voicePreview(recordingFileName, duration):
stopPlayback.toggle()
chatModel.filesToDelete.removeAll { $0 == recordingFileName }
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName)
case .filePreview:
if let fileURL = chosenFile,
let savedFile = saveFileFromURL(fileURL) {
case let .filePreview(_, file):
if let savedFile = saveFileFromURL(file) {
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live)
}
}
@@ -621,6 +636,14 @@ struct ComposeView: View {
}
}
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false) async -> ChatItem? {
let (image, data) = imageData
if let data = data, let savedFile = saveAnyImage(data) {
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live)
}
return nil
}
func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false) async -> ChatItem? {
if let chatItem = await apiSendMessage(
type: chat.chatInfo.chatType,
@@ -731,11 +754,16 @@ struct ComposeView: View {
)
}
private func cancelCurrentVoiceRecording() {
if let fileName = composeState.voiceMessageRecordingFileName {
cancelVoiceMessageRecording(fileName)
}
}
private func cancelVoiceMessageRecording(_ fileName: String) {
stopPlayback.toggle()
audioRecorder?.stop()
removeFile(fileName)
clearState()
}
private func clearState(live: Bool = false) {
@@ -747,12 +775,29 @@ struct ComposeView: View {
resetLinkPreview()
}
chosenImages = []
chosenFile = nil
audioRecorder = nil
voiceMessageRecordingTime = nil
startingRecording = false
}
private func saveCurrentDraft() {
if case .recording = composeState.voiceMessageRecordingState {
finishVoiceMessageRecording()
if let fileName = composeState.voiceMessageRecordingFileName {
chatModel.filesToDelete.append(fileName)
}
}
chatModel.draft = composeState
chatModel.draftChatId = chat.id
}
private func clearCurrentDraft() {
if chatModel.draftChatId == chat.id {
chatModel.draft = nil
chatModel.draftChatId = nil
}
}
private func showLinkPreview(_ s: String) {
prevLinkUrl = linkUrl
linkUrl = parseMessage(s)
@@ -16,13 +16,11 @@ enum VoiceMessagePlaybackState {
}
func voiceMessageTime(_ time: TimeInterval) -> String {
let min = Int(time / 60)
let sec = Int(time.truncatingRemainder(dividingBy: 60))
return String(format: "%02d:%02d", min, sec)
durationText(Int(time))
}
func voiceMessageTime_(_ time: TimeInterval?) -> String {
return voiceMessageTime(time ?? TimeInterval(0))
durationText(Int(time ?? 0))
}
struct ComposeVoiceView: View {
@@ -76,45 +76,20 @@ struct SendMessageView: View {
}
}
if (composeState.inProgress) {
if composeState.inProgress {
ProgressView()
.scaleEffect(1.4)
.frame(width: 31, height: 31, alignment: .center)
.padding([.bottom, .trailing], 3)
} else {
let vmrs = composeState.voiceMessageRecordingState
if showVoiceMessageButton
&& composeState.message.isEmpty
&& !composeState.editing
&& composeState.liveMessage == nil
&& ((composeState.noPreview && vmrs == .noRecording)
|| (vmrs == .recording && holdingVMR)) {
HStack {
if voiceMessageAllowed {
RecordVoiceMessageButton(
startVoiceMessageRecording: startVoiceMessageRecording,
finishVoiceMessageRecording: finishVoiceMessageRecording,
holdingVMR: $holdingVMR,
disabled: composeState.disabled
)
} else {
voiceMessageNotAllowedButton()
}
if let send = sendLiveMessage,
let update = updateLiveMessage,
case .noContextItem = composeState.contextItem {
startLiveMessageButton(send: send, update: update)
}
VStack(alignment: .trailing) {
if teHeight > 100 {
deleteTextButton()
Spacer()
}
} else if vmrs == .recording && !holdingVMR {
finishVoiceMessageRecordingButton()
} else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty {
cancelLiveMessageButton {
cancelLiveMessage?()
}
} else {
sendMessageButton()
composeActionButtons()
}
.frame(height: teHeight, alignment: .bottom)
}
}
@@ -125,6 +100,52 @@ struct SendMessageView: View {
.padding(.vertical, 8)
}
@ViewBuilder private func composeActionButtons() -> some View {
let vmrs = composeState.voiceMessageRecordingState
if showVoiceMessageButton
&& composeState.message.isEmpty
&& !composeState.editing
&& composeState.liveMessage == nil
&& ((composeState.noPreview && vmrs == .noRecording)
|| (vmrs == .recording && holdingVMR)) {
HStack {
if voiceMessageAllowed {
RecordVoiceMessageButton(
startVoiceMessageRecording: startVoiceMessageRecording,
finishVoiceMessageRecording: finishVoiceMessageRecording,
holdingVMR: $holdingVMR,
disabled: composeState.disabled
)
} else {
voiceMessageNotAllowedButton()
}
if let send = sendLiveMessage,
let update = updateLiveMessage,
case .noContextItem = composeState.contextItem {
startLiveMessageButton(send: send, update: update)
}
}
} else if vmrs == .recording && !holdingVMR {
finishVoiceMessageRecordingButton()
} else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty {
cancelLiveMessageButton {
cancelLiveMessage?()
}
} else {
sendMessageButton()
}
}
private func deleteTextButton() -> some View {
Button {
composeState.message = ""
} label: {
Image(systemName: "multiply.circle.fill")
}
.foregroundColor(Color(uiColor: .tertiaryLabel))
.padding([.top, .trailing], 4)
}
@ViewBuilder private func sendMessageButton() -> some View {
let v = Button(action: sendMessage) {
Image(systemName: composeState.editing || composeState.liveMessage != nil
@@ -145,12 +145,6 @@ struct GroupChatInfoView: View {
}
}
private func serverImage() -> some View {
let status = chat.serverInfo.networkStatus
return Image(systemName: status.imageName)
.foregroundColor(status == .connected ? .green : .secondary)
}
private func memberView(_ member: GroupMember, user: Bool = false) -> some View {
HStack{
ProfileImage(imageStr: member.image)
@@ -155,8 +155,6 @@ struct GroupMemberInfoView: View {
Button {
do {
let chat = try apiGetChat(type: .direct, id: contactId)
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
chat.serverInfo = Chat.ServerInfo(networkStatus: .connected)
chatModel.addChat(chat)
dismissAllSheets(animated: true)
DispatchQueue.main.async {
@@ -416,9 +416,9 @@ struct ErrorAlert {
func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
switch error as? ChatResponse {
case let .chatCmdError(.errorAgent(.BROKER(addr, .TIMEOUT))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
case let .chatCmdError(.errorAgent(.BROKER(addr, .NETWORK))):
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
default:
return ErrorAlert(title: title, message: "Error: \(responseError(error))")
@@ -11,25 +11,40 @@ import SimpleXChat
struct ChatListView: View {
@EnvironmentObject var chatModel: ChatModel
// not really used in this view
@State private var showSettings = false
@State private var searchText = ""
@State private var showAddChat = false
@State var userPickerVisible = false
var body: some View {
NavigationView {
VStack {
if chatModel.chats.isEmpty {
onboardingButtons()
}
if chatModel.chats.count > 8 {
chatList.searchable(text: $searchText)
} else {
chatList
ZStack(alignment: .topLeading) {
NavStackCompat(
isActive: Binding(
get: { ChatModel.shared.chatId != nil },
set: { _ in }
),
destination: chatView
) {
VStack {
if chatModel.chats.isEmpty {
onboardingButtons()
}
if chatModel.chats.count > 8 {
chatList.searchable(text: $searchText)
} else {
chatList
}
}
}
if userPickerVisible {
Rectangle().fill(.white.opacity(0.001)).onTapGesture {
withAnimation {
userPickerVisible.toggle()
}
}
}
UserPicker(showSettings: $showSettings, userPickerVisible: $userPickerVisible)
}
.navigationViewStyle(.stack)
}
var chatList: some View {
@@ -48,13 +63,35 @@ struct ChatListView: View {
}
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
.onAppear() { connectViaUrl() }
.onDisappear() { withAnimation { userPickerVisible = false } }
.offset(x: -8)
.listStyle(.plain)
.navigationTitle("Your chats")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
SettingsButton()
Button {
if chatModel.users.count > 1 {
withAnimation {
userPickerVisible.toggle()
}
} else {
showSettings = true
}
} label: {
let user = chatModel.currentUser ?? User.sampleData
ZStack(alignment: .topTrailing) {
ProfileImage(imageStr: user.image, color: Color(uiColor: .quaternaryLabel))
.frame(width: 32, height: 32)
.padding(.trailing, 4)
let allRead = chatModel.users
.filter { !$0.user.activeUser }
.allSatisfy { u in u.unreadCount == 0 }
if !allRead {
unreadBadge(size: 12)
}
}
}
}
ToolbarItem(placement: .principal) {
if (chatModel.incognito) {
@@ -65,6 +102,8 @@ struct ChatListView: View {
}
Image(systemName: "theatermasks").frame(maxWidth: 24, maxHeight: 24, alignment: .center).foregroundColor(.indigo)
}
} else {
Text("Your chats").font(.headline)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
@@ -75,15 +114,15 @@ struct ChatListView: View {
}
}
}
.background(
NavigationLink(
destination: chatView(),
isActive: Binding(
get: { chatModel.chatId != nil },
set: { _ in }
)
) { EmptyView() }
)
.sheet(isPresented: $showSettings) {
SettingsView(showSettings: $showSettings)
}
}
private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View {
Circle()
.frame(width: size, height: size)
.foregroundColor(.accentColor)
}
private func onboardingButtons() -> some View {
@@ -40,7 +40,7 @@ struct ChatPreviewView: View {
.padding(.horizontal, 8)
ZStack(alignment: .topTrailing) {
chatPreviewText(cItem)
chatMessagePreview(cItem)
if case .direct = chat.chatInfo {
chatStatusImage()
.padding(.top, 24)
@@ -106,31 +106,70 @@ struct ChatPreviewView: View {
.kerning(-2)
}
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View {
if let cItem = cItem {
let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemFormattedText = !cItem.meta.itemDeleted ? cItem.formattedText : nil
ZStack(alignment: .topTrailing) {
(itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true))
.lineLimit(2)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, 8)
.padding(.trailing, 36)
let s = chat.chatStats
if s.unreadCount > 0 || s.unreadChat {
unreadCountText(s.unreadCount)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary)
.cornerRadius(10)
} else if !chat.chatInfo.ntfsEnabled {
Image(systemName: "speaker.slash.fill")
.foregroundColor(.secondary)
}
private func chatPreviewLayout(_ text: Text) -> some View {
ZStack(alignment: .topTrailing) {
text
.lineLimit(2)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .topLeading)
.padding(.leading, 8)
.padding(.trailing, 36)
let s = chat.chatStats
if s.unreadCount > 0 || s.unreadChat {
unreadCountText(s.unreadCount)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary)
.cornerRadius(10)
} else if !chat.chatInfo.ntfsEnabled {
Image(systemName: "speaker.slash.fill")
.foregroundColor(.secondary)
}
}
}
private func messageDraft(_ draft: ComposeState) -> Text {
let msg = draft.message
return image("rectangle.and.pencil.and.ellipsis", color: .accentColor)
+ attachment()
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true)
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
}
func attachment() -> Text {
switch draft.preview {
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ")
case .imagePreviews: return image("photo")
case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration))
default: return Text("")
}
}
}
func chatItemPreview(_ cItem: ChatItem) -> Text {
let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text")
let itemFormattedText = !cItem.meta.itemDeleted ? cItem.formattedText : nil
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true)
func attachment() -> String? {
switch cItem.content.msgContent {
case .file: return "doc.fill"
case .image: return "photo"
case .voice: return "play.fill"
default: return nil
}
}
}
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View {
if chatModel.draftChatId == chat.id, let draft = chatModel.draft {
chatPreviewLayout(messageDraft(draft))
} else if let cItem = cItem {
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem))
} else {
switch (chat.chatInfo) {
case let .direct(contact):
@@ -179,16 +218,21 @@ struct ChatPreviewView: View {
}
@ViewBuilder private func chatStatusImage() -> some View {
switch chat.serverInfo.networkStatus {
case .connected: EmptyView()
case .error:
Image(systemName: "exclamationmark.circle")
.resizable()
.scaledToFit()
.frame(width: 17, height: 17)
.foregroundColor(.secondary)
switch chat.chatInfo {
case let .direct(contact):
switch (chatModel.contactNetworkStatus(contact)) {
case .connected: EmptyView()
case .error:
Image(systemName: "exclamationmark.circle")
.resizable()
.scaledToFit()
.frame(width: 17, height: 17)
.foregroundColor(.secondary)
default:
ProgressView()
}
default:
ProgressView()
EmptyView()
}
}
}
@@ -220,8 +264,7 @@ struct ChatPreviewView_Previews: PreviewProvider {
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.direct,
chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)],
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0),
serverInfo: Chat.ServerInfo(networkStatus: .error("status"))
chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0)
))
ChatPreviewView(chat: Chat(
chatInfo: ChatInfo.sampleData.group,
@@ -0,0 +1,169 @@
//
// Created by Avently on 16.01.2023.
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255))
private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255))
struct UserPicker: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme
@Binding var showSettings: Bool
@Binding var userPickerVisible: Bool
@State var scrollViewContentSize: CGSize = .zero
@State var disableScrolling: Bool = true
private let menuButtonHeight: CGFloat = 68
@State var chatViewNameWidth: CGFloat = 0
var fillColor: Color {
colorScheme == .dark ? fillColorDark : fillColorLight
}
var body: some View {
VStack {
Spacer().frame(height: 1)
VStack(spacing: 0) {
ScrollView {
ScrollViewReader { sp in
let users = m.users.sorted { u, _ in u.user.activeUser }
VStack(spacing: 0) {
ForEach(users) { u in
userView(u)
Divider()
if u.user.activeUser { Divider() }
}
}
.overlay {
GeometryReader { geo -> Color in
DispatchQueue.main.async {
scrollViewContentSize = geo.size
let scenes = UIApplication.shared.connectedScenes
if let windowScene = scenes.first as? UIWindowScene {
let layoutFrame = windowScene.windows[0].safeAreaLayoutGuide.layoutFrame
disableScrolling = scrollViewContentSize.height + menuButtonHeight + 10 < layoutFrame.height
}
}
return Color.clear
}
}
.onChange(of: userPickerVisible) { visible in
if visible, let u = users.first {
sp.scrollTo(u.id)
}
}
}
}
.simultaneousGesture(DragGesture(minimumDistance: disableScrolling ? 0 : 10000000))
.frame(maxHeight: scrollViewContentSize.height)
menuButton("Settings", icon: "gearshape") {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
}
}
}
.clipShape(RoundedRectangle(cornerRadius: 16))
.background(
Rectangle()
.fill(fillColor)
.cornerRadius(16)
.shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0)
)
.onPreferenceChange(DetermineWidth.Key.self) { chatViewNameWidth = $0 }
.frame(maxWidth: chatViewNameWidth > 0 ? min(300, chatViewNameWidth + 130) : 300)
.padding(8)
.opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear {
do {
m.users = try listUsers()
} catch let error {
logger.error("Error updating users \(responseError(error))")
}
}
}
private func userView(_ u: UserInfo) -> some View {
let user = u.user
return Button(action: {
if user.activeUser {
showSettings = true
withAnimation {
userPickerVisible.toggle()
}
} else {
do {
try changeActiveUser_(user.userId)
userPickerVisible = false
} catch {
AlertManager.shared.showAlertMsg(
title: "Error switching profile!",
message: "Error: \(responseError(error))"
)
}
}
}, label: {
HStack(spacing: 0) {
ProfileImage(imageStr: user.image, color: Color(uiColor: .tertiarySystemFill))
.frame(width: 44, height: 44)
.padding(.trailing, 12)
Text(user.chatViewName)
.fontWeight(user.activeUser ? .medium : .regular)
.foregroundColor(.primary)
.overlay(DetermineWidth())
Spacer()
if user.activeUser {
Image(systemName: "checkmark")
} else if u.unreadCount > 0 {
unreadCounter(u.unreadCount)
}
}
.padding(.trailing)
.padding([.leading, .vertical], 12)
})
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
}
private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
HStack(spacing: 0) {
Text(title)
.overlay(DetermineWidth())
Spacer()
Image(systemName: icon)
// .frame(width: 24, alignment: .center)
}
.padding(.horizontal)
.padding(.vertical, 22)
.frame(height: menuButtonHeight)
}
.buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill)))
}
}
func unreadCounter(_ unread: Int) -> some View {
unreadCountText(unread)
.font(.caption)
.foregroundColor(.white)
.padding(.horizontal, 4)
.frame(minWidth: 18, minHeight: 18)
.background(Color.accentColor)
.cornerRadius(10)
}
struct UserPicker_Previews: PreviewProvider {
static var previews: some View {
let m = ChatModel()
m.users = [UserInfo.sampleData, UserInfo.sampleData]
return UserPicker(
showSettings: Binding.constant(false),
userPickerVisible: Binding.constant(true)
)
.environmentObject(m)
}
}
@@ -152,7 +152,7 @@ struct DatabaseEncryptionView: View {
await operationEnded(.databaseEncrypted)
}
} catch let error {
if case .chatCmdError(.errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
await operationEnded(.currentPassphraseError)
} else {
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
@@ -67,6 +67,23 @@ struct DatabaseView: View {
private func chatDatabaseView() -> some View {
List {
let stopped = m.chatRunning == false
Section {
Picker("Delete messages after", selection: $chatItemTTL) {
ForEach(ChatItemTTL.values) { ttl in
Text(ttl.deleteAfterText).tag(ttl)
}
if case .seconds = chatItemTTL {
Text(chatItemTTL.deleteAfterText).tag(chatItemTTL)
}
}
.frame(height: 36)
.disabled(m.chatDbChanged || progressIndicator)
} header: {
Text("Messages")
} footer: {
Text("This setting applies to messages in your current chat profile **\(m.currentUser?.displayName ?? "")**.")
}
Section {
settingsRow(
stopped ? "exclamationmark.octagon.fill" : "play.fill",
@@ -157,22 +174,12 @@ struct DatabaseView: View {
}
Section {
Picker("Delete messages after", selection: $chatItemTTL) {
ForEach(ChatItemTTL.values) { ttl in
Text(ttl.deleteAfterText).tag(ttl)
}
if case .seconds = chatItemTTL {
Text(chatItemTTL.deleteAfterText).tag(chatItemTTL)
}
}
.frame(height: 36)
.disabled(m.chatDbChanged || progressIndicator)
Button("Delete files & media", role: .destructive) {
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
alert = .deleteFilesAndMedia
}
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
} header: {
Text("Data")
Text("Files & media")
} footer: {
if let (fileCount, size) = appFilesCountAndSize {
if fileCount == 0 {
@@ -0,0 +1,43 @@
//
// NavStackCompat.swift
// SimpleX (iOS)
//
// Created by Evgeny on 23/01/2023.
// Copyright © 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct NavStackCompat <C: View, D: View>: View {
let isActive: Binding<Bool>
let destination: () -> D
let content: () -> C
var body: some View {
if #available(iOS 16, *) {
NavigationStack(path: Binding(
get: { isActive.wrappedValue ? [true] : [] },
set: { _ in }
)) {
ZStack {
NavigationLink(value: true) { EmptyView() }
content()
}
.navigationDestination(for: Bool.self) { show in
if show { destination() }
}
}
} else {
NavigationView {
ZStack {
NavigationLink(
destination: destination(),
isActive: isActive
) { EmptyView() }
content()
}
}
.navigationViewStyle(.stack)
}
}
}
@@ -0,0 +1,15 @@
//
// Created by Avently on 16.01.2023.
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
//
import SwiftUI
struct PressedButtonStyle: ButtonStyle {
var defaultColor: Color
var pressedColor: Color
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.background(configuration.isPressed ? pressedColor : defaultColor)
}
}
@@ -11,10 +11,24 @@ import SimpleXChat
struct CreateProfile: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss
@State private var displayName: String = ""
@State private var fullName: String = ""
@FocusState private var focusDisplayName
@FocusState private var focusFullName
@State private var alert: CreateProfileAlert?
private enum CreateProfileAlert: Identifiable {
case duplicateUserError
case createUserError(error: LocalizedStringKey)
var id: String {
switch self {
case .duplicateUserError: return "duplicateUserError"
case .createUserError: return "createUserError"
}
}
}
var body: some View {
VStack(alignment: .leading) {
@@ -50,13 +64,17 @@ struct CreateProfile: View {
Spacer()
HStack {
Button {
hideKeyboard()
withAnimation { m.onboardingStage = .step1_SimpleXInfo }
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
if m.users.isEmpty {
Button {
hideKeyboard()
withAnimation {
m.onboardingStage = .step1_SimpleXInfo
}
} label: {
HStack {
Image(systemName: "lessthan")
Text("About SimpleX")
}
}
}
@@ -77,6 +95,12 @@ struct CreateProfile: View {
focusDisplayName = true
setLastVersionDefault()
}
.alert(item: $alert) { a in
switch a {
case .duplicateUserError: return duplicateUserAlert
case let .createUserError(err): return creatUserErrorAlert(err)
}
}
.padding()
}
@@ -96,17 +120,52 @@ struct CreateProfile: View {
)
do {
m.currentUser = try apiCreateActiveUser(profile)
try startChat()
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
} catch {
fatalError("Failed to create user or start chat: \(responseError(error))")
if m.users.isEmpty {
try startChat()
withAnimation { m.onboardingStage = .step3_SetNotificationsMode }
} else {
dismiss()
m.users = try listUsers()
try getUserChatData()
}
} catch let error {
switch error as? ChatResponse {
case .chatCmdError(_, .errorStore(.duplicateName)),
.chatCmdError(_, .error(.userExists)):
if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert)
} else {
alert = .duplicateUserError
}
default:
let err: LocalizedStringKey = "Error: \(responseError(error))"
if m.currentUser == nil {
AlertManager.shared.showAlert(creatUserErrorAlert(err))
} else {
alert = .createUserError(error: err)
}
}
logger.error("Failed to create user or start chat: \(responseError(error))")
}
}
func canCreateProfile() -> Bool {
displayName != "" && validDisplayName(displayName)
}
private var duplicateUserAlert: Alert {
Alert(
title: Text("Duplicate display name!"),
message: Text("You already have a chat profile with the same display name. Please choose another name.")
)
}
private func creatUserErrorAlert(_ err: LocalizedStringKey) -> Alert {
Alert(
title: Text("Error creating profile!"),
message: Text(err)
)
}
}
func validDisplayName(_ name: String) -> Bool {
@@ -87,6 +87,46 @@ private let versionDescriptions: [VersionDescription] = [
icon: "camera",
title: "GIFs and stickers",
description: "Send them from gallery or custom keyboards."
),
FeatureDescription(
icon: "character",
title: "French interface",
description: "Thanks to the users contribute via Weblate!"
)
]
),
VersionDescription(
version: "v4.5",
features: [
FeatureDescription(
icon: "person.crop.rectangle.stack",
title: "Multiple chat profiles",
description: "Different names, avatars and transport isolation."
),
FeatureDescription(
icon: "rectangle.and.pencil.and.ellipsis",
title: "Message draft",
description: "Preserve the last message draft, with attachments."
),
FeatureDescription(
icon: "network.badge.shield.half.filled",
title: "Transport isolation",
description: "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."
),
FeatureDescription(
icon: "lock.doc",
title: "Private filenames",
description: "To protect timezone, image/voice files use UTC."
),
FeatureDescription(
icon: "battery.25",
title: "Reduced battery usage",
description: "More improvements are coming soon!"
),
FeatureDescription(
icon: "character",
title: "Italian interface",
description: "Thanks to the users [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps)!"
)
]
)
@@ -113,8 +153,7 @@ struct WhatsNewView: View {
var body: some View {
VStack {
TabView(selection: $currentVersion) {
ForEach(0..<3) { i in
let v = versionDescriptions[i]
ForEach(Array(versionDescriptions.enumerated()), id: \.0) { (i, v) in
VStack(alignment: .leading, spacing: 16) {
Text("New in \(v.version)")
.font(.title)
@@ -147,8 +186,9 @@ struct WhatsNewView: View {
private func featureDescription(_ icon: String, _ title: LocalizedStringKey, _ description: LocalizedStringKey) -> some View {
VStack(alignment: .leading) {
HStack(alignment: .center) {
HStack(alignment: .center, spacing: 4) {
Image(systemName: icon).foregroundColor(.secondary)
.frame(minWidth: 30, alignment: .center)
Text(title).font(.title3).bold()
}
Text(description)
+3 -3
View File
@@ -108,10 +108,10 @@ struct TerminalView: View {
func sendMessage() {
let cmd = ChatCommand.string(composeState.message)
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
let resp = ChatResponse.chatCmdError(chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
let resp = ChatResponse.chatCmdError(user: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
DispatchQueue.main.async {
ChatModel.shared.terminalItems.append(.cmd(.now, cmd))
ChatModel.shared.terminalItems.append(.resp(.now, resp))
ChatModel.shared.addTerminalItem(.cmd(.now, cmd))
ChatModel.shared.addTerminalItem(.resp(.now, resp))
}
} else {
DispatchQueue.global().async {
@@ -53,7 +53,8 @@ struct AdvancedNetworkSettings: View {
timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [2_500000, 5_000000, 7_500000, 10_000000, 15_000000, 20_000000], label: secondsLabel)
timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [1_500000, 3_000000, 5_000000, 7_000000, 10_000000, 15_000000], label: secondsLabel)
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000], label: secondsLabel)
timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel)
intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "")
Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive)
if enableKeepAlive {
@@ -124,8 +125,8 @@ struct AdvancedNetworkSettings: View {
private func updateNetCfgView(_ cfg: NetCfg) {
netCfg = cfg
enableKeepAlive = currentNetCfg.enableKeepAlive
keepAliveOpts = currentNetCfg.tcpKeepAlive ?? KeepAliveOpts.defaults
enableKeepAlive = netCfg.enableKeepAlive
keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults
}
private func saveNetCfg() {
@@ -9,13 +9,15 @@
import SwiftUI
import SimpleXChat
enum OnionHostsAlert: Identifiable {
case update(hosts: OnionHosts)
private enum NetworkAlert: Identifiable {
case updateOnionHosts(hosts: OnionHosts)
case updateSessionMode(mode: TransportSessionMode)
case error(err: String)
var id: String {
switch self {
case let .update(hosts): return "update \(hosts)"
case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)"
case let .updateSessionMode(mode): return "updateSessionMode \(mode)"
case let .error(err): return "error \(err)"
}
}
@@ -27,7 +29,8 @@ struct NetworkAndServers: View {
@State private var currentNetCfg = NetCfg.defaults
@State private var netCfg = NetCfg.defaults
@State private var onionHosts: OnionHosts = .no
@State private var showOnionHostsAlert: OnionHostsAlert?
@State private var sessionMode: TransportSessionMode = .user
@State private var alert: NetworkAlert?
var body: some View {
VStack {
@@ -46,13 +49,17 @@ struct NetworkAndServers: View {
.frame(height: 36)
if developerTools {
NavigationLink {
AdvancedNetworkSettings()
.navigationTitle("Network settings")
} label: {
Text("Advanced network settings")
Picker("Transport isolation", selection: $sessionMode) {
ForEach(TransportSessionMode.values, id: \.self) { Text($0.text) }
}
}
NavigationLink {
AdvancedNetworkSettings()
.navigationTitle("Network settings")
} label: {
Text("Advanced network settings")
}
} header: {
Text("Messages")
} footer: {
@@ -77,17 +84,37 @@ struct NetworkAndServers: View {
}
.onChange(of: onionHosts) { _ in
if onionHosts != OnionHosts(netCfg: currentNetCfg) {
showOnionHostsAlert = .update(hosts: onionHosts)
alert = .updateOnionHosts(hosts: onionHosts)
}
}
.alert(item: $showOnionHostsAlert) { a in
.onChange(of: sessionMode) { _ in
if sessionMode != netCfg.sessionMode {
alert = .updateSessionMode(mode: sessionMode)
}
}
.alert(item: $alert) { a in
switch a {
case let .update(hosts):
case let .updateOnionHosts(hosts):
return Alert(
title: Text("Update .onion hosts setting?"),
message: Text(onionHostsInfo()) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
message: Text(onionHostsInfo(hosts)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
primaryButton: .default(Text("Ok")) {
saveNetCfg(hosts)
let (hostMode, requiredHostMode) = hosts.hostMode
netCfg.hostMode = hostMode
netCfg.requiredHostMode = requiredHostMode
saveNetCfg()
},
secondaryButton: .cancel() {
resetNetCfgView()
}
)
case let .updateSessionMode(mode):
return Alert(
title: Text("Update transport isolation mode?"),
message: Text(sessionModeInfo(mode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."),
primaryButton: .default(Text("Ok")) {
netCfg.sessionMode = mode
saveNetCfg()
},
secondaryButton: .cancel() {
resetNetCfgView()
@@ -102,11 +129,8 @@ struct NetworkAndServers: View {
}
}
private func saveNetCfg(_ hosts: OnionHosts) {
private func saveNetCfg() {
do {
let (hostMode, requiredHostMode) = hosts.hostMode
netCfg.hostMode = hostMode
netCfg.requiredHostMode = requiredHostMode
let def = netCfg.hostMode == .onionHost ? NetCfg.proxyDefaults : NetCfg.defaults
netCfg.tcpConnectTimeout = def.tcpConnectTimeout
netCfg.tcpTimeout = def.tcpTimeout
@@ -116,7 +140,7 @@ struct NetworkAndServers: View {
} catch let error {
let err = responseError(error)
resetNetCfgView()
showOnionHostsAlert = .error(err: err)
alert = .error(err: err)
logger.error("\(err)")
}
}
@@ -124,15 +148,23 @@ struct NetworkAndServers: View {
private func resetNetCfgView() {
netCfg = currentNetCfg
onionHosts = OnionHosts(netCfg: netCfg)
sessionMode = netCfg.sessionMode
}
private func onionHostsInfo() -> LocalizedStringKey {
switch onionHosts {
private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey {
switch hosts {
case .no: return "Onion hosts will not be used."
case .prefer: return "Onion hosts will be used when available. Requires enabling VPN."
case .require: return "Onion hosts will be required for connection. Requires enabling VPN."
}
}
private func sessionModeInfo(_ mode: TransportSessionMode) -> LocalizedStringKey {
switch mode {
case .user: return "A separate TCP connection will be used **for each chat profile you have in the app**."
case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail."
}
}
}
struct NetworkServersView_Previews: PreviewProvider {
@@ -71,10 +71,7 @@ struct PreferencesView: View {
p.preferences = fullPreferencesToPreferences(preferences)
if let newProfile = try await apiUpdateProfile(profile: p) {
await MainActor.run {
if let profileId = chatModel.currentUser?.profile.profileId {
chatModel.currentUser?.profile = toLocalProfile(profileId, newProfile, "")
chatModel.currentUser?.fullPreferences = preferences
}
chatModel.updateCurrentUser(newProfile, preferences)
currentPreferences = preferences
}
}
@@ -44,7 +44,7 @@ struct SMPServersView: View {
private func smpServersView() -> some View {
List {
Section("SMP servers") {
Section {
ForEach($servers) { srv in
smpServerView(srv)
}
@@ -57,6 +57,11 @@ struct SMPServersView: View {
Button("Add server…") {
showAddServer = true
}
} header: {
Text("SMP servers")
} footer: {
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
.lineLimit(10)
}
Section {

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