Compare commits

...

350 Commits

Author SHA1 Message Date
JRoberts 6f68840b3a 4.5.2: ios 125, Android 100 2023-02-10 11:43:14 +04:00
JRoberts 2eef858db1 ios: update core library 2023-02-10 11:10:35 +04:00
Evgeny Poberezkin 434315fb08 Merge branch 'stable' 2023-02-09 20:53:05 +00:00
Evgeny Poberezkin 9b495e576c readme: update groups 2023-02-09 20:52:44 +00:00
JRoberts 5405f44f54 core: 4.5.2.0 2023-02-09 16:34:44 +04:00
Stanislav Dmitrenko 53b05974c9 android: limit width + height in chat item view (#1920) 2023-02-09 12:00:33 +00:00
JRoberts 3530022152 ios, android: moderated item content types, CIDeleted type (#1919) 2023-02-09 15:10:35 +04:00
Stanislav Dmitrenko dc6bab7ae6 android: fixed broken autoscroll when a new message is coming (#1917)
* android: fixed broken autoscroll when a new message is coming

* spelling
2023-02-08 23:10:56 +00:00
Stanislav Dmitrenko c9b4ce457e android: prevent race when opening chat (#1914)
* android: prevent race when opening chat

* different way of doing things

* change

* change
2023-02-08 19:25:51 +00:00
JRoberts bd3325a889 core: show/keep message as moderated for moderator (#1916) 2023-02-08 22:29:36 +04:00
JRoberts 9e347484eb core: avoid using wickAckMessage handler on messages leading to connection deletion (#1915) 2023-02-08 21:23:53 +04:00
ishi_sama 894af0602d docs: french (#1905)
* Lang subfolder ; SERVER_fr.md ; fix minor typos

* fix the fix

* fix img src

* CONTRIBUTING_fr

* SQL_fr ; fix rev date

* WEBRTC_fr.md ; rev date

* CLI_fr ; rev date

* fix table content link and sum img src

* fix

* fixing the fix

am i dumb?

* polishing...

* README_fr.md (save)

* Update README_fr.md

* README_fr ; starting SIMPLEX_FR ; link fix

* update README (en+fr)

* Blog README_fr ; translators link

* typo

* SIMPLEX_fr ; fixes

* last fixes

* rename folder

* rename files/links

* update line

* remove ...

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-08 14:03:52 +00:00
Stanislav Dmitrenko aa6011a196 android: prevent race when changing active user (#1913) 2023-02-08 13:21:55 +00:00
Stanislav Dmitrenko f24035a99d android: prevent showing system alert after start (Android 13+) (#1909)
* android: prevent showing system alert after start (Android 13+)

* refactor

* added comments and renamed function

* rename

* rename

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-08 10:48:19 +00:00
Stanislav Dmitrenko c006b8150f ios: ask permission to open profiles (#1910) 2023-02-08 10:39:41 +00:00
Evgeny Poberezkin 9e4499de6d core: allow admins/owners delete member messages (#1869)
* core: allow admins/owners delete member messages

* allow message deletion to admins/owners

* deleted by types, schema

* check role

* fix test, view

* view, tests

* comment

* test timed deletion events

* refactor

* refactor

* refactor

---------

Co-authored-by: JRoberts <8711996+jr-simplex@users.noreply.github.com>
2023-02-08 11:08:53 +04:00
Evgeny Poberezkin a018e4a581 docs: translation guide (#1911)
* docs: translations

* add doc

* add images, corrections

* spellcheck

* link to translation guide from readme

* change image

* images
2023-02-07 21:57:51 +00:00
Stanislav Dmitrenko d29fd93ea7 docs: view files from private data directory of Android app (#1907)
* docs: view files from private data directory of Android app

* naming

* spelling

* spelling

* change
2023-02-07 15:17:10 +00:00
Stanislav Dmitrenko b30c7af3a3 mobile: including fully localized languages only (#1908)
* mobile: including fully localized languages only

* better place for code

* ios: including fully localized languages only

* Revert "ios: including fully localized languages only"

This reverts commit 42a0334d83.
2023-02-07 15:16:34 +00:00
Stanislav Dmitrenko 2798671d22 android: show alert when decode exception happens (#1906) 2023-02-07 12:08:54 +00:00
Stanislav Dmitrenko 0339b399f7 ios: don't go back when system alert appears (#1903) 2023-02-06 16:33:45 +00:00
Stanislav Dmitrenko d048962959 ios: Fixed screenshot size (#1902) 2023-02-06 16:00:29 +00:00
Stanislav Dmitrenko 4af91c4cae android: fixed size for exported QR code (#1901)
* android: fixed size for exported QR code

* border

* automatic version

* modifier

* make image instead of screenshot

* code folding

* don't use deprecated method

* function refactor

* dropped unneeded variable
2023-02-06 15:34:49 +00:00
Stanislav Dmitrenko 8a445ece90 ios: colored and clickable qr code with logo (#1885)
* ios: colored and clickable qr code with logo

* size of circle

* same padding as in android

* add padding to logo

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-06 08:45:56 +00:00
Stanislav Dmitrenko 5082f5b4a4 android: colored and clickable qr code with logo (#1884)
* android: colored and clickable qr code with logo

* save qr code as jpg for better quality

* bigger logo

* bigger logo (0.16f), low error correction (QR scans ok with up to 0.26f circle size)

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-02-06 08:45:40 +00:00
Evgeny Poberezkin c8fae0ec43 blog: typo 2023-02-05 23:46:42 +00:00
Evgeny Poberezkin d9a8d333f7 4.5.1: Android 99, iOS 124 2023-02-05 23:37:58 +00:00
Evgeny Poberezkin 73f8c543e3 translations (#1900)
* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 26.7% (255 of 954 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (895 of 895 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 8.5% (82 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 24.4% (219 of 895 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.8% (18 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 56.4% (538 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 1.6% (15 of 891 strings)

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

* Added translation using Weblate (Croatian)

* Added translation using Weblate (Croatian)

* Translated using Weblate (Dutch)

Currently translated at 63.6% (607 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.6% (950 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 66.1% (589 of 891 strings)

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

* Update translation files

Updated by "Remove blank strings" hook in Weblate.

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

* ios: import localizations

* ios: export localizations

---------

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

* update JS

* show protocol, support TURNS

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

* remove enums for protocol strings in ICE candidates

* 0.2.3

---------

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

* update post, images

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

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

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

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (German)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (895 of 895 strings)

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

* Translated using Weblate (French)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Italian)

Currently translated at 98.5% (882 of 895 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 42.1% (402 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 3.8% (37 of 954 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (895 of 895 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (895 of 895 strings)

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

* Translated using Weblate (Czech)

Currently translated at 3.9% (38 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

---------

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

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

* refactore

* terminate background taks on chat item update

* refactor

* refactor2

* refactor3

* refactor 4

* refactor5

* fix chat item update in Android

---------

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

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

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Dutch)

* Translated using Weblate (Russian)

Currently translated at 100.0% (936 of 936 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 11.0% (103 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (936 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 13.8% (130 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 14.9% (131 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 13.7% (129 of 936 strings)

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

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Spanish)

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Dutch)

* Translated using Weblate (Russian)

Currently translated at 100.0% (936 of 936 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 11.0% (103 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (936 of 936 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 13.8% (130 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 14.9% (131 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 13.7% (129 of 936 strings)

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

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Japanese)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Hindi)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Czech)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Added translation using Weblate (Portuguese (Brazil))

* Added translation using Weblate (Portuguese (Brazil))

* Translated using Weblate (French)

Currently translated at 100.0% (936 of 936 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 17.3% (162 of 936 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 16.2% (143 of 878 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 15.7% (147 of 936 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (878 of 878 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 21.9% (206 of 939 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 20.7% (182 of 878 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 24.3% (229 of 939 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 7.8% (74 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 31.2% (294 of 940 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 12.6% (119 of 940 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 13.5% (127 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 22.7% (214 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 21.7% (191 of 878 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 2.5% (24 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 23.6% (222 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 24.3% (214 of 878 strings)

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

---------

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

* better way of continue connection and end with timeout

* making data classes instead of classes for making logs informative

* refactor

* update webrtc package version

* refactor

* fix

* clear conneciton timeout on disconnection

* refactor

* v0.2.1

---------

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

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

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

* updated texts

---------

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

* updated texts

* refactor

---------

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

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

* enable only failing tests

* start/stop server for every test

* log message that failed to parse

* stop chat synchronously

* print call stack

* add HasCallStack

* increase test timeout

* add call stacks

* more call stacks

* fix test

* disable failing test

* add delay between the tests

* make delay more visible

* remove change in error message

* reduce test delay, increase timeout

* increase delay between the tests

* run each test with a database in a different folder

* folder name

* refactor

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

* show meaningful alert

* update alert messages

---------

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

* different logic in asking to auth

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

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

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

* Added translation using Weblate (Chinese (Simplified))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.6% (23 of 854 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 7.0% (60 of 854 strings)

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

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

* Added translation using Weblate (Chinese (Simplified))

* Added translation using Weblate (Chinese (Simplified))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.6% (23 of 854 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 2.7% (25 of 911 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 7.0% (60 of 854 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (917 of 917 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (859 of 859 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (917 of 917 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (859 of 859 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (917 of 917 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (859 of 859 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 10.7% (92 of 859 strings)

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

* Added translation using Weblate (Chinese (Traditional))

* Added translation using Weblate (Chinese (Traditional))

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

* Added translation using Weblate (Spanish)

* Translated using Weblate (Chinese (Simplified))

Currently translated at 10.1% (95 of 936 strings)

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

* remove UK English file

---------

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

* empty line

* delete unused voice files

* finish recording properly

* refactor

* fix

---------

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

* save and restore images/file/voice for draft

* refactor image

* it was a wrong value

* use param label

* proper stop of voice recording

* safe draft logic

* different way of finishing recording

* keep condition

* refactor

* fix live

* fix

* refactor

* fix

* simplify

* add space after filename in draft

---------

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

* padding

* bigger padding

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

* updated the design of Comparison section

* updated the design of Simplex explained

* updated the design of Simplex network

* updated the design of private section

* updated the design of features section

* updated the design of unique section

* updated the design of privacy matters

* added improvements in design

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

* tint color of icon

* userId from function

* better line

* missing question

* bigger avatar

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

* update paddings

* progressIndicator

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

* sizes of buttons

* update paddings

* change names

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

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

* fix layout

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

* show attachment icon and formatting in draft

* button to remove message text

* show icon for active draft, refactor

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

* another try

* complete

* added file

* enable swipe to go back from ChatView

* Revert "enable swipe to go back from ChatView"

This reverts commit 22de79505c.

* ios: enable swipe to go back from ChatView

* remove title change

* remove unused

* remove unused variable

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

* update text

* refactor, disable button

* darker profile icon colors

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

* add line in try-catch block

* changed lines position

* when -> if

* take condition outside

* mutable version of objects and usage of a new function

* changed additional places in code

* added toMap() so state will be updated

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

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

* another try

* complete

* added file

* enable swipe to go back from ChatView

* Revert "enable swipe to go back from ChatView"

This reverts commit 22de79505c.

* refactor

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

* add comments

* typos

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

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

* query

* check snd cancel

* Revert "check snd cancel"

This reverts commit f16651345d.

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

* update scroll

* update current user and update users list

* refactor

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

* counter update on badge

* padding before profile info in call view

* underline in name

* change after merge

* do not show Simplex Info button if users already created

* unread counter

* do not increase badge counter when chat has disabled notifications

* update incoming call

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

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

* add simplexmq commit and version to version info

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

* typo

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

* ios: update alerts

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

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

* remove comment

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

* export ios translations

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

* return regardless

* refactor

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

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (853 of 853 strings)

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

* import localizations

* re-export localizations

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

* Logging of user in responses

* UserId

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

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

* Do not set active user if it's unchanged

* Blank line

* if

* Change active user function

* refactor

* refactor

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

* Alert

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

* update build

* parens

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

* doSendCancel

* Apply suggestions from code review

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

* sendCancel

* refactor

* error to view

* refactor

* refactor

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

* Change

* Changes

* update view

* fix layout/refactor

* fix preview

* wider menu, update label

* hide Your profiles button

* Clickable background that hides userChooser

* No click listener

* Better animation

* Disabled scrolling for small number of items

* Separated scrollview and buttons

* No transition

* Re-indent

* Limiting width by the longest label

* UserManagerView

* Adapted API

* Hide user chooser after selection

* Top counter,  users refactor

* Padding

* use VStack to fix layout bug

* eol

* rename: rename to getUserChatData

* update layout

* s/semibold/medium

* remove SettingsButton view

* rename

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

* split

* tests

* refactor

* viewUserInfo

* refactor

* remove omit nothing

* corrections

* fix

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

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

* indent for chats without messages, help

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

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

* Added translation using Weblate (Italian)

* Translated using Weblate (French)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 99.6% (850 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 24.9% (226 of 906 strings)

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

* Translated using Weblate (Italian)

Currently translated at 26.3% (225 of 853 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 28.8% (261 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 58.2% (528 of 906 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (853 of 853 strings)

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

* Added translation using Weblate (Italian)

* Added translation using Weblate (Italian)

* Translated using Weblate (French)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 99.6% (850 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 24.9% (226 of 906 strings)

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

* Translated using Weblate (Italian)

Currently translated at 26.3% (225 of 853 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 28.8% (261 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 58.2% (528 of 906 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (853 of 853 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 38.5% (329 of 853 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (853 of 853 strings)

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

* correction

* correction

* correction

* ios: import localizations

* ios: re-export localizations

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

* Better

* Separate function

* rename

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

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

* Test

* Revert "Test"

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

* update

* more
2023-01-13 16:24:54 +04:00
JRoberts 892b91e958 core: update simplexmq (subscribe users in different sessions) (#1734) 2023-01-13 15:14:46 +04:00
JRoberts fb04108b11 Merge branch 'master' into users 2023-01-13 14:19:21 +04:00
JRoberts 424328b9d1 core: agent users (#1727) 2023-01-13 13:54:07 +04:00
JRoberts e73f5c40cf 4.4.2: ios 114, Android 88 2023-01-12 18:38:58 +04:00
JRoberts 4c960bdc44 4.4.2 2023-01-12 16:46:28 +04:00
JRoberts dcb82951ed core: catch errors when sending messages in loops where they haven't been previously caught (#1729) 2023-01-12 16:31:27 +04:00
Stanislav Dmitrenko 138cce4436 ios: fix opening via notification returning to chat list when screen lock is enabled (#1725)
* ios: Fixed broken chat push/pop logic

* remove binding parameter

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

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

* Test

* Revert "Test"

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

* android: improve live message logic

* fix, refactor

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

* rename view

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

* Allow non-transparent pixels

* optimize

* remove prints

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

* Custom Equatable

* Changes

* Change

* Fix liveMessage not hiding

* Refactoring

* Refactoring

* No animation when removing dummy live message item

* Check

* Anim

* Animation

* whitespace

* refactor

* Fix race

* Better fix of race

* fix race condition

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

* Better quoted messages handling

* Do not add item into preview

* Change

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

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

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

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (850 of 850 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (850 of 850 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (850 of 850 strings)

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

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <ptlfr@pm.me>
2023-01-04 20:25:33 +00:00
JRoberts fa9e0086f6 core: multiple users api (#1679)
* api

* UCR

* Revert "UCR"

This reverts commit 1f98d25192.

* comment

* events User

* events in api User

* CRActiveUser in APISetActiveUser

* process message with/without connection

* refactor

* mute error

* user in api responses

* name

* lost response

* user in CRChatCmdError

* compiles

* user in CRChatError

* -- UserId

* mute unused warning

* catch in withUser

* remove comment

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-01-04 21:06:28 +04:00
Evgeny Poberezkin a1b27e9a99 update simplexmq (better handling of quota errors) 2023-01-04 15:07:33 +00:00
Evgeny Poberezkin f68d8fd97c update readme 2023-01-03 13:13:51 +00:00
Evgeny Poberezkin abff42a264 blog: v4.4 (#1675)
* blog: v4.4

* add images

* update post

* update readme, roadmap

* corrections

* correction
2023-01-03 13:07:30 +00:00
Evgeny Poberezkin c1ced70836 Merge branch 'stable' 2023-01-01 14:06:51 +00:00
Evgeny Poberezkin e14966d36e blog: v4.4 release announcement placeholder 2023-01-01 14:05:05 +00:00
Evgeny Poberezkin 97943fc609 ios: v4.4 release, build 110 2023-01-01 14:01:21 +00:00
shum 0f143b2e77 nix: bump lock 2022-12-31 19:42:31 +00:00
Evgeny Poberezkin be10dcbcfc ios: build 109 2022-12-31 19:40:43 +00:00
Evgeny Poberezkin 97dbec927c ios: build 108 2022-12-31 18:23:26 +00:00
Evgeny Poberezkin b4879ca2a3 ios: fix user chat preferences view 2022-12-31 13:38:55 +00:00
Evgeny Poberezkin 15884c0169 4.4.0: iOS 107, Android 84 2022-12-31 11:47:59 +00:00
Evgeny Poberezkin 9f2d5486b6 mobile: fix race condition with live messages (when message is "revived" when it is being sent as no longer live – possibly core should reject "live" updates to non-live messages too) (#1668)
* mobile: fix race condition with live messages (when message is "revived" when it is being sent as no longer live – possibly core should reject "live" updates to non-live messages too)

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

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (850 of 850 strings)

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

* Translated using Weblate (French)

Currently translated at 99.6% (847 of 850 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (850 of 850 strings)

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

* Translated using Weblate (German)

Currently translated at 90.9% (824 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 92.4% (786 of 850 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (904 of 904 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (845 of 845 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (850 of 850 strings)

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

* Translated using Weblate (French)

Currently translated at 99.6% (847 of 850 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (850 of 850 strings)

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

* Translated using Weblate (German)

Currently translated at 90.9% (824 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 92.4% (786 of 850 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (906 of 906 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (850 of 850 strings)

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

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

* Different icon

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

* mobile: items with feature offers

* ios interactive contact/user preference change items

* android: interactive preference items

* Translated using Weblate (French)

Currently translated at 8.1% (68 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (784 of 784 strings)

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

* Translated using Weblate (French)

Currently translated at 10.4% (87 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 10.5% (88 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 14.2% (122 of 855 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 15.9% (137 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 18.2% (157 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 36.3% (312 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 54.0% (464 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 81.0% (695 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 82.1% (705 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 85.9% (756 of 880 strings)

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

* import/export localizations

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

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

* add OK button to WhatsNew

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

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

Currently translated at 95.0% (783 of 824 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (824 of 824 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (824 of 824 strings)

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

* Moved from String path to UIImage param

* Aspect ratio

* Image frame

* gif image size

* refactor

* refactor

* fix fullscreen scroll animation

* rename UploadContent -> AnyImage

* refactor, allow using gifs in profiles

* rename back

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

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

Currently translated at 93.5% (823 of 880 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (880 of 880 strings)

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

* Translated using Weblate (German)

Currently translated at 96.2% (774 of 804 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (804 of 804 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (880 of 880 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (880 of 880 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (804 of 804 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (804 of 804 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (880 of 880 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (804 of 804 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (880 of 880 strings)

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

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

Currently translated at 97.6% (785 of 804 strings)

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

* Translated using Weblate (French)

Currently translated at 97.6% (785 of 804 strings)

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

* cleanup

* optimize chat loading time

* cleanup

* schema

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

* mobile: items with feature offers

* ios interactive contact/user preference change items

* android: interactive preference items

* Translated using Weblate (French)

Currently translated at 8.1% (68 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (784 of 784 strings)

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

* Translated using Weblate (French)

Currently translated at 10.4% (87 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 10.5% (88 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 14.2% (122 of 855 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 15.9% (137 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 18.2% (157 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 36.3% (312 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 54.0% (464 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 81.0% (695 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

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

* Translated using Weblate (French)

Currently translated at 82.1% (705 of 858 strings)

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

* Translated using Weblate (French)

Currently translated at 85.9% (756 of 880 strings)

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

* revert changes

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

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

* ios interactive contact/user preference change items

* android: interactive preference items

* add missing view

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

* do not set ttl in global timed message prefs

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

* texts

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

* new preference items

* test

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

* remove unused func

* remove paren

* outlined timer in meta

* reserving space for meta takes into account ttl text

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

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

* show ttl in meta if different

* mark messages as disappearing when read

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

* White color

* Spacer

* button sizes

* Do not show voice button in live mode

* Add text to the last image in a row

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

* wip

* confirm_pref_pending

* xInfo

* test api

* send confirmPrefProfile

* refactor

* don't return contact

* refactor profile update

* refactor further

* refactor further

* refactor xInfo

* refactor xInfo further

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

* removed unused function

* refactor

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

* change delays in tests

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

* fix tests

* fix test

* view

* refactor

* use prefChangedValue

* use groupPrefChangedValue

* use cupIntValue

* simplify types

* groupFeatureState

* groupPrefToText

* prefToText, view

* remove prefFeature

* rename intValue -> param

* int -> param

* timedTTLText

* remove pragma

* restore pragma

* simplify

* timedTTLText

* fix tests

* off, after

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

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

* terminal: send live messages (#1599)

* terminal: send live messages

* show edited messages

* send and continue live message with Alt-Enter

* truncate live messages to full words

* remove comments

* refactor

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

* $

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

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

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

* lastItemId

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

* remove comments

* remove conflict

* live message buttons and alert

* only send full words

* fix double sending

* typing indicator in live items

* add live parameter to API

* typing indication, pass live parameter to API

* refactor to support live messages with attachments

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

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

* fix text alignment for correct input field height

* refactor

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

* Update src/Simplex/Chat/Store.hs

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

* refactor

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

* Concurrency test

* Revert "Concurrency test"

This reverts commit 911dd0c2ef.

* Comment

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

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

* change schema

* word

* wip

* wip

* todos

* todos

* remove cancel, refactor

* revert prefs

* CITimed

* schema

* time on send direct

* time on send group

* add ttl to msg container, refactor

* timed on receive

* time on read

* getTimedItems, fix tests

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

* refactor

* comment

* util

* insert atomically

* refactor

* use guards

* refactor startTimedItemThread

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

* Just for test

* update strings

* Revert "Just for test"

This reverts commit f9c9a20ab6.

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

* Dividers

* Changes

* Padding

* Share connection code

* Share connection code

* Unused

* icon sizes

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

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

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

* line break

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

Currently translated at 100.0% (831 of 831 strings)

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

* Translated using Weblate (French)

Currently translated at 98.4% (772 of 784 strings)

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

* Change

* Different texts

* Disable service starting until on-boarding finishes

* refactor, change strings

* update layout

* update layout

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

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

This reverts commit 11a6113b4f.

* Disable image selection when there are images already selected

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

Currently translated at 100.0% (822 of 822 strings)

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

* Added translation using Weblate (French)

* Added translation using Weblate (French)

* Translated using Weblate (French)

Currently translated at 0.0% (0 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 0.3% (3 of 822 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 0.9% (8 of 822 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

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

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

Currently translated at 100.0% (822 of 822 strings)

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

* Added translation using Weblate (French)

* Added translation using Weblate (French)

* Translated using Weblate (French)

Currently translated at 0.0% (0 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 2.0% (16 of 773 strings)

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

* Translated using Weblate (French)

Currently translated at 0.3% (3 of 822 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

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

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

* linebreaks

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

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

* Limited voice record max duration

* Better end of recording when it reaches timeout

* New way of doing things

* Change

* Change

* Stop event refactor

* Stopped state

* Replaced some helpers

* Replaced calls in when()

* Comments

* Change

* Change

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

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

* verification in member sheet (still crashes)

* use navigation view for members list

* ios: show verified status in the lists

* update verification status in the list of members

* verified shield layout

* update icon, make add member navigation to right

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

* add header

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-12-11 18:33:56 +00:00
328 changed files with 82147 additions and 11757 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/
+56 -25
View File
@@ -1,3 +1,5 @@
| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/README.md) |
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
@@ -7,7 +9,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 +45,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)
@@ -76,23 +78,25 @@ You can use SimpleX with your own servers and still communicate with people usin
## Frequently asked questions
1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release annoucement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release announcement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
2. _Why should I not just use Signal?_ Signal is a centralised platform that uses phone numbers to identify its users and their contacts. It means that while the content of your messages on Signal is protected with robust end-to-end encryption, there is a large amount of meta-data visible to Signal - who you talk with and when.
2. _Why should I not just use Signal?_ Signal is a centralized platform that uses phone numbers to identify its users and their contacts. It means that while the content of your messages on Signal is protected with robust end-to-end encryption, there is a large amount of meta-data visible to Signal - who you talk with and when.
3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identites?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identities?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
## News and updates
Recent updates:
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration.](./blog/20221206-simplex-chat-v4.3-voice-messages.md)
[Feb 04, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
[Jan 03, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md)
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md)
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
[All updates](./blog)
@@ -136,7 +140,7 @@ SimpleX Chat is a work in progress we are releasing improvements as they are
What is already implemented:
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notificaitons on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
3. [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with forward secrecy (each message is encrypted by its own ephemeral key), break-in recovery (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial key agreement, initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
@@ -146,10 +150,11 @@ What is already implemented:
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
We plan to add soon:
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers termporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
1. Automatic message queue rotation. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`.
3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
@@ -159,7 +164,7 @@ You can:
- use SimpleX Chat library to integrate chat functionality into your mobile apps.
- create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/).
- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScipt chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution.
If you are considering developing with SimpleX platform please get in touch for any advice and support.
@@ -191,17 +196,24 @@ If you are considering developing with SimpleX platform please get in touch for
- ✅ View deleted messages, full message deletion by sender (with recipient opt-in per contact).
- ✅ Block screenshots and view in recent apps.
- ✅ Advanced server configuration.
- ✅ Disappearing messages (with recipient opt-in per-contact).
- ✅ "Live" messages.
- ✅ Contact verification via a separate out-of-band channel.
- ✅ Multiple user profiles in the same chat database.
- ✅ Optionally avoid re-using the same TCP session for multiple connections.
- ✅ Preserve message drafts.
- 🏗 File server to optimize for efficient and private sending of large files.
- 🏗 Improved audio & video calls.
- 🏗 SMP queue redundancy and rotation (manual is supported).
- 🏗 Contact verification via a separate out-of-band channel.
- 🏗 Ephemeral/disappearing/OTR conversations with the existing contacts.
- Optionally avoid re-using the same TCP session for multiple connections.
- 🏗 Reduced battery and traffic usage in large groups.
- 🏗 Support older Android OS and 32-bit CPUs.
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Access password/pin (with optional alternative access password).
- Media server to optimize sending large files to groups.
- Local app files encryption.
- Video messages.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- Multiple user profiles in the same chat database.
- Feeds/broadcasts.
- Unconfirmed: disappearing messages (with recipient opt-in per-contact).
- Web widgets for custom interactivity in the chats.
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
- Supporting the same profile on multiple devices.
@@ -209,28 +221,46 @@ If you are considering developing with SimpleX platform please get in touch for
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups.
- Channels server for large groups and broadcast channels.
- Hosting server for large groups, communities and public channels.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- High capacity multi-node SMP relays.
## Join a user group
You can join a general group with more than 100 members: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D).
You can join an English-speaking group if you want to ask any questions: [#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)
You can also join smaller groups by countries/languages: [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FmIorjTDPG24jdLKXwutS6o9hdQQRZwfQ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA9N0BZaECrAw3we3S1Wq4QO7NERBuPt9447immrB50wo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22S8aISlOgkTMytSox9gAM2Q%3D%3D%22%7D) (German), [\#SimpleX-US](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FlTWmQplLEaoJyHnEL1-B3f2PtDsikcTs%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-hMBlsQjNxK2vaVhqW_UyAVtuoYqgYTigK4B9dJ9CGc%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22G0UtRHIn0TmPoo08h_cbTA%3D%3D%22%7D) (US/English), [\#SimpleX-France](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F11r6XyjwVMj0WDIUMbmNDXO996M_EN_1%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAXDmc2Lrj9WQOjEcWa0DeQHF3HcYOp9b68s8M_BJ7gEk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22EZCeSYpeIBkaQwCcpcF00w%3D%3D%22%7D), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FZSYM278L5WoZiApx3925EAjSXcsAVNVu%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA7RJ2wfT8zdfOLyE5OtWLEAPowj-q6F2HB0ExbATw8Gk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22fsVoklNGptt7n-droqJYUQ%3D%3D%22%7D) (Russian), [#SimpleX-NL](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmP0LbswSbfxoVkkxiWE2NYnBCgZ9Snvj%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAVwZuSsw4Mf52EaBNdNI3RebsLm0jg65ZIkcmH9E5uy8%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22M9xIULUNZx51Wsa5Kdb0Sg%3D%3D%22%7D) (Netherlands/Dutch), [#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaZ_wjh6QAYHB-LjyGtp8bllkzoq880u-%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-_Wulzc3j16i7t77XJ5wgwxeW8_Ea8GxetMo7K4MgjI%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22QWmXdrFzIeMd2OoEPMFkBQ%3D%3D%22%7D) (Italian).
There are also several groups in languages other than English, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users. We do not always answer questions there, so please ask them in one of the English-speaking groups.
- [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking).
- [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking).
- [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking).
- [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
Let us know if you'd like to add some other countries to the list.
Join via the app to share what's going on and ask any questions!
## Translate the apps
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps are translated to many other languages. Join our translators to help SimpleX grow faster!
Current interface languages:
- English (development language)
- German: [@mlanp](https://github.com/mlanp)
- French: [@ishi_sama](https://github.com/ishi-sama)
- Italian: [@unbranched](https://github.com/unbranched)
- Russian: project team
Languages in progress: Chinese, Hindi, Czech, 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, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
## Contribute
We would love to have you join the development! You can contribute to SimpleX Chat with:
- developing features - please connect to us via chat so we can help you get started.
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- translate UI to some language - we are currently setting up the UI to simplify it, please get in touch and let us know if you would be able to support and update the translations.
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- developing features - please connect to us via chat so we can help you get started.
## Help us with donations
@@ -250,6 +280,7 @@ It is possible to donate via:
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
+7 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 29
targetSdk 32
versionCode 78
versionName "4.3.2"
versionCode 100
versionName "4.5.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
@@ -76,6 +76,11 @@ android {
}
jniLibs.useLegacyPackaging = compression_level != "0"
}
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
if (isRelease) {
// Comma separated list of languages that will be included in the apk
android.defaultConfig.resConfigs("en", "ru", "de", "fr", "it")
}
}
dependencies {
+18 -1
View File
@@ -22,10 +22,12 @@ var TransformOperation;
TransformOperation["Decrypt"] = "decrypt";
})(TransformOperation || (TransformOperation = {}));
let activeCall;
let answerTimeout = 30000;
const processCommand = (function () {
const defaultIceServers = [
{ urls: ["stun:stun.simplex.im:443"] },
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
{ urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
{ urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
];
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
return {
@@ -100,9 +102,16 @@ const processCommand = (function () {
const iceCandidates = getIceCandidates(pc, config);
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
await setupMediaStreams(call);
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
pc.addEventListener("connectionstatechange", connectionStateChange);
return call;
async function connectionStateChange() {
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
if (pc.connectionState !== "failed")
connectionHandler();
}
async function connectionHandler() {
sendMessageToNative({
resp: {
type: "connection",
@@ -115,6 +124,7 @@ const processCommand = (function () {
},
});
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
clearConnectionTimeout();
pc.removeEventListener("connectionstatechange", connectionStateChange);
if (activeCall) {
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
@@ -122,6 +132,7 @@ const processCommand = (function () {
endCall();
}
else if (pc.connectionState == "connected") {
clearConnectionTimeout();
const stats = (await pc.getStats());
for (const stat of stats.values()) {
const { type, state } = stat;
@@ -141,6 +152,12 @@ const processCommand = (function () {
}
}
}
function clearConnectionTimeout() {
if (connectionTimeout) {
clearTimeout(connectionTimeout);
connectionTimeout = undefined;
}
}
}
function serialize(x) {
return LZString.compressToBase64(JSON.stringify(x));
@@ -29,6 +29,7 @@ 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
@@ -42,7 +43,6 @@ import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
class MainActivity: FragmentActivity() {
@@ -374,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()
}
}
}
}
@@ -394,7 +387,8 @@ fun MainPage(
}
}
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel)
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
}
ModalManager.shared.showInView()
val invitation = chatModel.activeCallInvitation.value
@@ -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")
@@ -478,7 +483,6 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) {
// TODO open from chat list view
chatModel.appOpenUrl.value = uri
} else {
withUriAction(uri) { linkType ->
@@ -39,7 +39,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
var isAppOnForeground: Boolean = false
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() ?: ""
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
val res: DBMigrationResult = kotlin.runCatching {
@@ -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) {
@@ -100,8 +101,19 @@ class SimplexApp: Application(), LifecycleEventObserver {
isAppOnForeground = true
if (chatModel.chatRunning.value == true) {
kotlin.runCatching {
val chats = chatController.apiGetChats()
chatModel.updateChats(chats)
val currentUserId = chatModel.currentUser.value?.userId
val chats = ArrayList(chatController.apiGetChats())
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
if (chatModel.currentUser.value?.userId == currentUserId) {
val currentChatId = chatModel.chatId.value
val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
if (oldStats != null) {
val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
}
chatModel.updateChats(chats)
}
}.onFailure { Log.e(TAG, it.stackTraceToString()) }
}
}
@@ -115,8 +127,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
* after calling [ChatController.showBackgroundServiceNoticeIfNeeded] notification mode in prefs can be changed.
* It can happen when app was started and a user enables battery optimization while app in background
* */
if (chatModel.chatRunning.value != false && appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
if (chatModel.chatRunning.value != false &&
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
) {
SimplexService.start(applicationContext)
}
}
else -> isAppOnForeground = false
}
@@ -2,18 +2,23 @@ package chat.simplex.app.model
import android.net.Uri
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
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
import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.*
import kotlinx.datetime.*
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
@@ -21,6 +26,8 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import java.io.File
import kotlin.random.Random
import kotlin.time.*
/*
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
@@ -29,6 +36,7 @@ import java.io.File
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)
@@ -36,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)
@@ -75,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 }
@@ -101,7 +107,7 @@ class ChatModel(val controller: ChatController) {
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directContact)
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
@@ -114,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
@@ -133,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) {
@@ -151,7 +140,7 @@ class ChatModel(val controller: ChatController) {
}
}
fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
// update previews
val i = getChatIndex(cInfo.id)
val chat: Chat
@@ -162,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
@@ -175,11 +165,17 @@ class ChatModel(val controller: ChatController) {
}
// add to current chat
if (chatId.value == cInfo.id) {
chatItems.add(cItem)
withContext(Dispatchers.Main) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
} else {
chatItems.add(cItem)
}
}
}
}
fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
// update previews
val i = getChatIndex(cInfo.id)
val chat: Chat
@@ -206,7 +202,9 @@ class ChatModel(val controller: ChatController) {
chatItems[itemIndex] = cItem
return false
} else {
chatItems.add(cItem)
withContext(Dispatchers.Main) {
chatItems.add(cItem)
}
return true
}
} else {
@@ -242,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
@@ -250,6 +249,33 @@ 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) {
chatItems.add(cItem)
}
return cItem
}
fun removeLiveDummy() {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.removeLast()
}
}
fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
val markedRead = markItemsReadInCurrentChat(cInfo, range)
// update preview
@@ -258,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
)
@@ -276,7 +304,13 @@ class ChatModel(val controller: ChatController) {
while (i < chatItems.count()) {
val item = chatItems[i]
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
chatItems[i] = item.withStatus(CIStatus.RcvRead())
val newItem = item.withStatus(CIStatus.RcvRead())
chatItems[i] = newItem
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
)
}
markedRead++
}
i += 1
@@ -290,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)
@@ -341,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) {
@@ -376,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 {
@@ -396,8 +474,8 @@ interface SomeChat {
val sendMsgEnabled: Boolean
val ntfsEnabled: Boolean
val incognito: Boolean
val voiceMessageAllowed: Boolean
val fullDeletionAllowed: Boolean
fun featureEnabled(feature: ChatFeature): Boolean
val timedMessagesTTL: Int?
val createdAt: Instant
val updatedAt: Instant
}
@@ -407,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,
@@ -459,8 +512,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val sendMsgEnabled get() = contact.sendMsgEnabled
override val ntfsEnabled get() = contact.ntfsEnabled
override val incognito get() = contact.incognito
override val voiceMessageAllowed get() = contact.voiceMessageAllowed
override val fullDeletionAllowed get() = contact.fullDeletionAllowed
override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL
override val createdAt get() = contact.createdAt
override val updatedAt get() = contact.updatedAt
override val displayName get() = contact.displayName
@@ -483,8 +536,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
override val ntfsEnabled get() = groupInfo.ntfsEnabled
override val incognito get() = groupInfo.incognito
override val voiceMessageAllowed get() = groupInfo.voiceMessageAllowed
override val fullDeletionAllowed get() = groupInfo.fullDeletionAllowed
override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL
override val createdAt get() = groupInfo.createdAt
override val updatedAt get() = groupInfo.updatedAt
override val displayName get() = groupInfo.displayName
@@ -507,8 +560,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
override val ntfsEnabled get() = contactRequest.ntfsEnabled
override val incognito get() = contactRequest.incognito
override val voiceMessageAllowed get() = contactRequest.voiceMessageAllowed
override val fullDeletionAllowed get() = contactRequest.fullDeletionAllowed
override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL
override val createdAt get() = contactRequest.createdAt
override val updatedAt get() = contactRequest.updatedAt
override val displayName get() = contactRequest.displayName
@@ -531,8 +584,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
override val ntfsEnabled get() = contactConnection.incognito
override val incognito get() = contactConnection.incognito
override val voiceMessageAllowed get() = contactConnection.voiceMessageAllowed
override val fullDeletionAllowed get() = contactConnection.fullDeletionAllowed
override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL
override val createdAt get() = contactConnection.createdAt
override val updatedAt get() = contactConnection.updatedAt
override val displayName get() = contactConnection.displayName
@@ -545,6 +598,51 @@ sealed class ChatInfo: SomeChat, NamedChat {
ContactConnection(PendingContactConnection.getSampleData(status, viaContactUri))
}
}
@Serializable @SerialName("invalidJSON")
class InvalidJSON(val json: String): ChatInfo() {
override val chatType get() = ChatType.Direct
override val localDisplayName get() = invalidChatName
override val id get() = ""
override val apiId get() = 0L
override val ready get() = false
override val sendMsgEnabled get() = false
override val ntfsEnabled get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
override val createdAt get() = Clock.System.now()
override val updatedAt get() = Clock.System.now()
override val displayName get() = invalidChatName
override val fullName get() = invalidChatName
override val image get() = null
override val localAlias get() = ""
companion object {
private val invalidChatName = generalGetString(R.string.invalid_chat)
}
}
}
@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
@@ -568,19 +666,36 @@ data class Contact(
override val sendMsgEnabled get() = true
override val ntfsEnabled get() = chatSettings.enableNtfs
override val incognito get() = contactConnIncognito
override val voiceMessageAllowed get() = mergedPreferences.voice.enabled.forUser
override val fullDeletionAllowed get() = mergedPreferences.fullDelete.enabled.forUser
override fun featureEnabled(feature: ChatFeature) = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
}
override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null }
override val displayName get() = localAlias.ifEmpty { profile.displayName }
override val fullName get() = profile.fullName
override val image get() = profile.image
override val localAlias get() = profile.localAlias
val verified get() = activeConn.connectionCode != null
val directContact: Boolean get() =
val directOrUsed: Boolean get() =
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
val contactConnIncognito =
activeConn.customUserProfileId != null
fun allowsFeature(feature: ChatFeature): Boolean = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
}
fun userAllowsFeature(feature: ChatFeature): Boolean = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO
}
companion object {
val sampleData = Contact(
contactId = 1,
@@ -600,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"
@@ -612,13 +729,24 @@ class ContactSubStatus(
)
@Serializable
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val viaGroupLink: Boolean, val customUserProfileId: Long? = null) {
data class Connection(
val connId: Long,
val agentConnId: String,
val connStatus: ConnStatus,
val connLevel: Int,
val viaGroupLink: Boolean,
val customUserProfileId: Long? = null,
val connectionCode: SecurityCode? = null
) {
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)
}
}
@Serializable
data class SecurityCode(val securityCode: String, val verifiedAt: Instant)
@Serializable
data class Profile(
override val displayName: String,
@@ -691,8 +819,12 @@ data class GroupInfo (
override val sendMsgEnabled get() = membership.memberActive
override val ntfsEnabled get() = chatSettings.enableNtfs
override val incognito get() = membership.memberIncognito
override val voiceMessageAllowed get() = fullGroupPreferences.voice.on
override val fullDeletionAllowed get() = fullGroupPreferences.fullDelete.on
override fun featureEnabled(feature: ChatFeature) = when (feature) {
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
ChatFeature.Voice -> fullGroupPreferences.voice.on
}
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
override val displayName get() = groupProfile.displayName
override val fullName get() = groupProfile.fullName
override val image get() = groupProfile.image
@@ -726,6 +858,7 @@ data class GroupInfo (
data class GroupProfile (
override val displayName: String,
override val fullName: String,
val description: String? = null,
override val image: String? = null,
override val localAlias: String = "",
val groupPreferences: GroupPreferences? = null
@@ -757,6 +890,7 @@ data class GroupMember (
val displayName: String get() = memberProfile.localAlias.ifEmpty { memberProfile.displayName }
val fullName: String get() = memberProfile.fullName
val image: String? get() = memberProfile.image
val verified get() = activeConn?.connectionCode != null
val chatViewName: String
get() = memberProfile.localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
@@ -937,8 +1071,8 @@ class UserContactRequest (
override val sendMsgEnabled get() = false
override val ntfsEnabled get() = false
override val incognito get() = false
override val voiceMessageAllowed get() = false
override val fullDeletionAllowed get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
override val displayName get() = profile.displayName
override val fullName get() = profile.fullName
override val image get() = profile.image
@@ -975,8 +1109,8 @@ class PendingContactConnection(
override val sendMsgEnabled get() = false
override val ntfsEnabled get() = false
override val incognito get() = customUserProfileId != null
override val voiceMessageAllowed get() = false
override val fullDeletionAllowed get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId)
override val displayName: String get() {
if (localAlias.isNotEmpty()) return localAlias
@@ -1074,7 +1208,7 @@ data class ChatItem (
}
}
val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew
val isRcvNew: Boolean get() = meta.isRcvNew
val memberDisplayName: String? get() =
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
@@ -1117,10 +1251,15 @@ data class ChatItem (
is CIContent.SndConnEventContent -> showNtfDir
is CIContent.RcvChatFeature -> false
is CIContent.SndChatFeature -> showNtfDir
is CIContent.RcvChatPreference -> false
is CIContent.SndChatPreference -> showNtfDir
is CIContent.RcvGroupFeature -> false
is CIContent.SndGroupFeature -> showNtfDir
is CIContent.RcvChatFeatureRejected -> showNtfDir
is CIContent.RcvGroupFeatureRejected -> showNtfDir
is CIContent.SndModerated -> true
is CIContent.RcvModerated -> true
is CIContent.InvalidJSON -> false
}
fun withStatus(status: CIStatus): ChatItem = this.copy(meta = meta.copy(itemStatus = status))
@@ -1134,13 +1273,14 @@ data class ChatItem (
status: CIStatus = CIStatus.SndNew(),
quotedItem: CIQuote? = null,
file: CIFile? = null,
itemDeleted: Boolean = false,
itemDeleted: CIDeleted? = null,
itemEdited: Boolean = false,
itemTimed: CITimed? = null,
editable: Boolean = true
) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, itemTimed, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem,
file = file
@@ -1155,7 +1295,7 @@ data class ChatItem (
) =
ChatItem(
chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead()),
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile(text)),
quotedItem = null,
file = CIFile.getSample(fileName = fileName, fileSize = fileSize, fileStatus = fileStatus)
@@ -1170,7 +1310,7 @@ data class ChatItem (
) =
ChatItem(
chatDir = dir,
meta = CIMeta.getSample(id, ts, text, status, itemDeleted = false, itemEdited = false, editable = false),
meta = CIMeta.getSample(id, ts, text, status),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null,
file = null
@@ -1179,7 +1319,7 @@ data class ChatItem (
fun getGroupInvitationSample(status: CIGroupInvitationStatus = CIGroupInvitationStatus.Pending) =
ChatItem(
chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead()),
content = CIContent.RcvGroupInvitation(groupInvitation = CIGroupInvitation.getSample(status = status), memberRole = GroupMemberRole.Admin),
quotedItem = null,
file = null
@@ -1188,17 +1328,17 @@ data class ChatItem (
fun getGroupEventSample() =
ChatItem(
chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead()),
content = CIContent.RcvGroupEventContent(rcvGroupEvent = RcvGroupEvent.MemberAdded(groupMemberId = 1, profile = Profile.sampleData)),
quotedItem = null,
file = null
)
fun getChatFeatureSample(feature: ChatFeature, enabled: FeatureEnabled): ChatItem {
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled)
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled, param = null)
return ChatItem(
chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead()),
content = content,
quotedItem = null,
file = null
@@ -1206,7 +1346,8 @@ data class ChatItem (
}
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
const val TEMP_LIVE_CHAT_ITEM_ID = -2L
val deletedItemDummy: ChatItem
get() = ChatItem(
chatDir = CIDirection.DirectRcv(),
@@ -1216,14 +1357,46 @@ data class ChatItem (
itemText = generalGetString(R.string.deleted_description),
itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(),
itemDeleted = false,
updatedAt = Clock.System.now(),
itemDeleted = null,
itemEdited = false,
itemTimed = null,
itemLive = false,
editable = false
),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null,
file = null
)
fun liveDummy(direct: Boolean): ChatItem = ChatItem(
chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(),
meta = CIMeta(
itemId = TEMP_LIVE_CHAT_ITEM_ID,
itemTs = Clock.System.now(),
itemText = "",
itemStatus = CIStatus.RcvRead(),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
itemDeleted = null,
itemEdited = false,
itemTimed = null,
itemLive = true,
editable = false
),
content = CIContent.SndMsgContent(MsgContent.MCText("")),
quotedItem = null,
file = null
)
fun invalidJSON(json: String): ChatItem =
ChatItem(
chatDir = CIDirection.DirectSnd(),
meta = CIMeta.invalidJSON(),
content = CIContent.InvalidJSON(json),
quotedItem = null,
file = null
)
}
}
@@ -1249,16 +1422,33 @@ data class CIMeta (
val itemText: String,
val itemStatus: CIStatus,
val createdAt: Instant,
val itemDeleted: Boolean,
val updatedAt: Instant,
val itemDeleted: CIDeleted?,
val itemEdited: Boolean,
val itemTimed: CITimed?,
val itemLive: Boolean?,
val editable: Boolean
) {
val timestampText: String get() = getTimestampText(itemTs)
val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now()
val isLive: Boolean get() = itemLive == true
val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null
val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew
fun statusIcon(primaryColor: Color, metaColor: Color = HighOrLowlight): Pair<ImageVector, Color>? =
when (itemStatus) {
is CIStatus.SndSent -> Icons.Filled.Check to metaColor
is CIStatus.SndErrorAuth -> Icons.Filled.Close to Color.Red
is CIStatus.SndError -> Icons.Filled.WarningAmber to WarningYellow
is CIStatus.RcvNew -> Icons.Filled.Circle to primaryColor
else -> null
}
companion object {
fun getSample(
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
itemDeleted: Boolean = false, itemEdited: Boolean = false, editable: Boolean = true
itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true
): CIMeta =
CIMeta(
itemId = id,
@@ -1266,13 +1456,38 @@ data class CIMeta (
itemText = text,
itemStatus = status,
createdAt = ts,
updatedAt = ts,
itemDeleted = itemDeleted,
itemEdited = itemEdited,
itemTimed = itemTimed,
itemLive = itemLive,
editable = editable
)
fun invalidJSON(): CIMeta =
CIMeta(
// itemId can not be the same for different items, otherwise ChatView will crash
itemId = Random.nextLong(-1000000L, -1000L),
itemTs = Clock.System.now(),
itemText = "invalid JSON",
itemStatus = CIStatus.SndNew(),
createdAt = Clock.System.now(),
updatedAt = Clock.System.now(),
itemDeleted = null,
itemEdited = false,
itemTimed = null,
itemLive = false,
editable = false
)
}
}
@Serializable
data class CITimed(
val ttl: Int,
val deleteAt: Instant?
)
fun getTimestampText(t: Instant): String {
val tz = TimeZone.currentSystemDefault()
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
@@ -1293,6 +1508,12 @@ sealed class CIStatus {
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
}
@Serializable
sealed class CIDeleted {
@Serializable @SerialName("deleted") class Deleted: CIDeleted()
@Serializable @SerialName("moderated") class Moderated(val byGroupMember: GroupMember): CIDeleted()
}
@Serializable
enum class CIDeleteMode(val deleteMode: String) {
@SerialName("internal") cidmInternal("internal"),
@@ -1320,12 +1541,17 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatPreference") class RcvChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndChatPreference") class SndChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
override val text: String get() = when (this) {
is SndMsgContent -> msgContent.text
@@ -1341,13 +1567,36 @@ sealed class CIContent: ItemContent {
is SndGroupEventContent -> sndGroupEvent.text
is RcvConnEventContent -> rcvConnEvent.text
is SndConnEventContent -> sndConnEvent.text
is RcvChatFeature -> "${feature.text}: ${enabled.text}"
is SndChatFeature -> "${feature.text}: ${enabled.text}"
is RcvGroupFeature -> "${groupFeature.text}: ${preference.enable.text}"
is SndGroupFeature -> "${groupFeature.text}: ${preference.enable.text}"
is RcvChatFeature -> featureText(feature, enabled.text, param)
is SndChatFeature -> featureText(feature, enabled.text, param)
is RcvChatPreference -> preferenceText(feature, allowed, param)
is SndChatPreference -> preferenceText(feature, allowed, param)
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
is SndModerated -> generalGetString(R.string.moderated_description)
is RcvModerated -> generalGetString(R.string.moderated_description)
is InvalidJSON -> "invalid data"
}
companion object {
fun featureText(feature: Feature, enabled: String, param: Int?): String =
if (feature.hasParam) {
"${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
} else {
"${feature.text}: $enabled"
}
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, TimedMessagesPreference.ttlText(param))
allowed != FeatureAllowed.NO ->
String.format(generalGetString(R.string.feature_offered_item), feature.text, TimedMessagesPreference.ttlText(param))
else ->
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, TimedMessagesPreference.ttlText(param))
}
}
}
@Serializable
@@ -1438,14 +1687,8 @@ sealed class MsgContent {
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
val cmdString: String get() = when (this) {
is MCText -> "text $text"
is MCLink -> "json ${json.encodeToString(this)}"
is MCImage -> "json ${json.encodeToString(this)}"
is MCVoice-> "json ${json.encodeToString(this)}"
is MCFile -> "json ${json.encodeToString(this)}"
is MCUnknown -> "json $json"
}
val cmdString: String get() =
if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}"
}
@Serializable
@@ -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
@@ -38,11 +44,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
private val msgNtfTimeoutMs = 30000L
init {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
}
enum class NotificationAction {
@@ -77,8 +79,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 +90,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 +130,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 +152,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, null))
.build()
with(NotificationManagerCompat.from(context)) {
@@ -182,9 +187,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 +246,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 {
@@ -258,24 +264,46 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
}
/**
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
* already saw such alert or when you want to trigger showing the alert.
* On the first app launch the channels will be created after user profile is created. Subsequent calls will create new channels and delete
* old ones if needed
* */
fun createNtfChannelsMaybeShowAlert() {
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
// Remove old channels since they can't be edited
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
}
/**
* Processes every action specified by [NotificationCompat.Builder.addAction] that comes with [NotificationAction]
* and [ChatInfo.id] as [ChatIdKey] in extra
* */
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 {
@@ -135,7 +75,21 @@ fun TerminalLayout(
topBar = { CloseSheetBar(close) },
bottomBar = {
Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(composeState, false, false, false, sendCommand, ::onMessageChange, { _, _, _ -> }, {}, {}, textStyle)
SendMsgView(
composeState = composeState,
showVoiceRecordIcon = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
allowVoiceToContact = {},
sendMessage = sendCommand,
sendLiveMessage = null,
updateLiveMessage = null,
onMessageChange = ::onMessageChange,
textStyle = textStyle
)
}
},
modifier = Modifier.navigationBarsWithImePadding()
@@ -160,7 +114,8 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
DisposableEffect(Unit) {
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
}
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed() } }
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
val context = LocalContext.current
LazyColumn(state = listState, reverseLayout = true) {
items(reversedTerminalItems) { item ->
Text(
@@ -171,7 +126,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
modifier = Modifier
.fillMaxWidth()
.clickable {
ModalManager.shared.showModal {
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
}
@@ -21,8 +21,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.SimplexService
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile
import chat.simplex.app.ui.theme.*
@@ -38,7 +38,7 @@ fun isValidDisplayName(name: String) : Boolean {
}
@Composable
fun CreateProfilePanel(chatModel: ChatModel) {
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
@@ -72,10 +72,12 @@ fun CreateProfilePanel(chatModel: ChatModel) {
ProfileNameField(fullName)
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
SimpleButton(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
if (chatModel.users.isEmpty()) {
SimpleButton(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
}
Spacer(Modifier.fillMaxWidth().weight(1f))
@@ -83,7 +85,7 @@ fun CreateProfilePanel(chatModel: ChatModel) {
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value) }.padding(8.dp)
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
@@ -105,15 +107,23 @@ fun CreateProfilePanel(chatModel: ChatModel) {
}
}
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) {
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
withApi {
val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
)
chatModel.controller.startChat(user)
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
SimplexService.start(chatModel.controller.appContext)
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
) ?: return@withApi
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
close()
}
}
}
@@ -18,7 +18,7 @@ class CallManager(val chatModel: ChatModel) {
controller.ntfManager.notifyCallInvitation(invitation)
} else {
val contact = invitation.contact
controller.ntfManager.notifyMessageReceived(chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
controller.ntfManager.notifyMessageReceived(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
}
}
}
@@ -297,10 +297,10 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
InfoText(call.callState.text)
val connInfo =
if (call.connectionInfo == null) ""
else " (${call.connectionInfo.text})"
InfoText(call.encryptionStatus + connInfo)
val connInfo = call.connectionInfo
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
InfoText(call.encryptionStatus + connInfoText)
}
}
@@ -480,7 +480,10 @@ fun PreviewActiveCallOverlayVideo() {
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
peerMedia = CallMediaType.Video,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null),
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
)
),
dismiss = {},
toggleAudio = {},
@@ -501,7 +504,10 @@ fun PreviewActiveCallOverlayAudio() {
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
peerMedia = CallMediaType.Audio,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null),
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
)
),
dismiss = {},
toggleAudio = {},
@@ -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 = {}
@@ -1,15 +1,19 @@
package chat.simplex.app.views.call
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import androidx.compose.ui.text.toUpperCase
import chat.simplex.app.*
import chat.simplex.app.model.Contact
import chat.simplex.app.model.User
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URI
import java.util.*
import kotlin.collections.ArrayList
data class Call(
val contact: Contact,
@@ -61,39 +65,39 @@ enum class CallState {
}
}
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
@Serializable
sealed class WCallCommand {
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
@Serializable @SerialName("start") class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("end") object End: WCallCommand()
}
@Serializable
sealed class WCallResponse {
@Serializable @SerialName("capabilities") class Capabilities(val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("answer") class Answer(val answer: String, val iceCandidates: String): WCallResponse()
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
@Serializable @SerialName("capabilities") data class Capabilities(val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
@Serializable @SerialName("answer") data class Answer(val answer: String, val iceCandidates: String): WCallResponse()
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
@Serializable @SerialName("ended") object Ended: WCallResponse()
@Serializable @SerialName("ok") object Ok: WCallResponse()
@Serializable @SerialName("error") class Error(val message: String): WCallResponse()
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
}
@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
@Serializable data class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
@Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
@Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
val callTypeText: String get() = generalGetString(when(callType.media) {
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
@@ -103,19 +107,32 @@ sealed class WCallResponse {
CallMediaType.Audio -> R.string.incoming_audio_call
})
}
@Serializable class CallCapabilities(val encryption: Boolean)
@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
val text: String @Composable get() = when {
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
stringResource(R.string.call_connection_peer_to_peer)
localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay ->
stringResource(R.string.call_connection_via_relay)
else ->
"${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}"
@Serializable data class CallCapabilities(val encryption: Boolean)
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
val text: String @Composable get() {
val local = localCandidate?.candidateType
val remote = remoteCandidate?.candidateType
return when {
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
stringResource(R.string.call_connection_peer_to_peer)
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
stringResource(R.string.call_connection_via_relay)
else ->
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
}
}
val protocolText: String get() {
val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown"
val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)"
return if (local == remote) localText else "$localText / $remote"
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
@@ -150,7 +167,7 @@ enum class VideoCamera {
}
@Serializable
class ConnectionState(
data class ConnectionState(
val connectionState: String,
val iceConnectionState: String,
val iceGatheringState: String,
@@ -158,20 +175,22 @@ class ConnectionState(
)
// the servers are expected in this format:
// stun:stun.simplex.im:443
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443
// stun:stun.simplex.im:443?transport=tcp
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
fun parseRTCIceServer(str: String): RTCIceServer? {
var s = replaceScheme(str, "stun:")
s = replaceScheme(s, "turn:")
s = replaceScheme(s, "turns:")
val u = runCatching { URI(s) }.getOrNull()
if (u != null) {
val scheme = u.scheme
val host = u.host
val port = u.port
if (u.path == "" && (scheme == "stun" || scheme == "turn")) {
if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) {
val userInfo = u.userInfo?.split(":")
val query = if (u.query == null || u.query == "") "" else "?${u.query}"
return RTCIceServer(
urls = listOf("$scheme:$host:$port"),
urls = listOf("$scheme:$host:$port$query"),
username = userInfo?.getOrNull(0),
credential = userInfo?.getOrNull(1)
)
@@ -38,6 +38,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
@Composable
fun ChatInfoView(
@@ -46,18 +47,24 @@ fun ChatInfoView(
connStats: ConnectionStats?,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
close: () -> Unit,
) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null) {
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
mutableStateOf(chatModel.contactNetworkStatus(contact))
}
ChatInfoLayout(
chat,
contact,
connStats,
contactNetworkStatus.value,
customUserProfile,
localAlias,
connectionCode,
developerTools,
onLocalAliasChanged = {
setContactAlias(chat.chatInfo.apiId, it, chatModel)
@@ -74,6 +81,31 @@ fun ChatInfoView(
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
switchContactAddress = {
showSwitchContactAddressAlert(chatModel, contact.contactId)
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
VerifyCodeView(
ct.displayName,
connectionCode,
ct.verified,
verify = { code ->
chatModel.controller.apiVerifyContact(ct.contactId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.updateContact(
ct.copy(
activeConn = ct.activeConn.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)
)
r
}
},
close,
)
}
}
}
)
}
@@ -121,14 +153,17 @@ fun ChatInfoLayout(
chat: Chat,
contact: Contact,
connStats: ConnectionStats?,
contactNetworkStatus: NetworkStatus,
customUserProfile: Profile?,
localAlias: String,
connectionCode: String?,
developerTools: Boolean,
onLocalAliasChanged: (String) -> Unit,
openPreferences: () -> Unit,
deleteContact: () -> Unit,
clearChat: () -> Unit,
switchContactAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
Column(
Modifier
@@ -154,6 +189,10 @@ fun ChatInfoLayout(
SectionSpacer()
SectionView {
if (connectionCode != null) {
VerifyCodeButton(contact.verified, verifyClicked)
SectionDivider()
}
ContactPreferencesButton(openPreferences)
}
@@ -166,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()) {
@@ -208,13 +247,17 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
horizontalAlignment = Alignment.CenterHorizontally
) {
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Text(
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(bottom = 8.dp)
)
Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
if (contact.verified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
}
Text(
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
Text(
cInfo.fullName, style = MaterialTheme.typography.h2,
@@ -276,7 +319,7 @@ fun LocalAliasEditor(
}
@Composable
fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
Row(
Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -308,14 +351,14 @@ fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
}
@Composable
fun ServerImage(networkStatus: Chat.NetworkStatus) {
private fun ServerImage(networkStatus: NetworkStatus) {
Box(Modifier.size(18.dp)) {
when (networkStatus) {
is Chat.NetworkStatus.Connected ->
is NetworkStatus.Connected ->
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
is Chat.NetworkStatus.Disconnected ->
is NetworkStatus.Disconnected ->
Icon(Icons.Filled.Pending, stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight)
is Chat.NetworkStatus.Error ->
is NetworkStatus.Error ->
Icon(Icons.Filled.Error, stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight)
else -> Icon(Icons.Outlined.Circle, stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight)
}
@@ -339,6 +382,16 @@ fun SwitchAddressButton(onClick: () -> Unit) {
}
}
@Composable
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
SettingsActionItem(
if (contactVerified) Icons.Outlined.VerifiedUser else Icons.Outlined.Shield,
stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code),
click = onClick,
iconColor = HighOrLowlight,
)
}
@Composable
private fun ContactPreferencesButton(onClick: () -> Unit) {
SettingsActionItem(
@@ -360,7 +413,7 @@ fun ClearChatButton(onClick: () -> Unit) {
}
@Composable
fun DeleteContactButton(onClick: () -> Unit) {
private fun DeleteContactButton(onClick: () -> Unit) {
SettingsActionItem(
Icons.Outlined.Delete,
stringResource(R.string.button_delete_contact),
@@ -398,19 +451,21 @@ fun PreviewChatInfoLayout() {
ChatInfoLayout(
chat = Chat(
chatInfo = ChatInfo.Direct.sampleData,
chatItems = arrayListOf(),
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
chatItems = arrayListOf()
),
Contact.sampleData,
localAlias = "",
connectionCode = "123",
developerTools = false,
connStats = null,
contactNetworkStatus = NetworkStatus.Connected(),
onLocalAliasChanged = {},
customUserProfile = null,
openPreferences = {},
deleteContact = {},
clearChat = {},
switchContactAddress = {},
verifyClicked = {},
)
}
}
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.*
@@ -12,8 +13,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.mapSaver
@@ -56,7 +56,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
val user = chatModel.currentUser.value
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val composeState = rememberSaveable(saver = ComposeState.saver()) {
mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews))
mutableStateOf(
if (chatModel.draftChatId.value == chatId && chatModel.draft.value != null) {
chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
} else {
ComposeState(useLinkPreviews = useLinkPreviews)
}
)
}
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
@@ -77,16 +83,23 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
}
}
launch {
// .toList() is important for making observation working
snapshotFlow { chatModel.chats.toList() }
.distinctUntilChanged()
.collect { chats ->
chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }.let {
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
if (it?.chatInfo != activeChat.value?.chatInfo) {
activeChat.value = it
}}
snapshotFlow {
/**
* It's possible that in some cases concurrent modification can happen on [ChatModel.chats] list.
* In this case only error log will be printed here (no crash).
* TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that
* */
try {
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
} catch (e: ConcurrentModificationException) {
Log.e(TAG, e.stackTraceToString())
null
}
}
.distinctUntilChanged()
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
.filter { it?.chatInfo != activeChat.value?.chatInfo && it != null }
.collect { activeChat.value = it }
}
}
val view = LocalView.current
@@ -131,16 +144,17 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
withApi {
if (chat.chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)
ModalManager.shared.showModalCloseable(true) { close ->
val contact = remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
contact.value?.let { ct ->
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close)
remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
}
}
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
var groupLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
ModalManager.shared.showModalCloseable(true) { close ->
GroupChatInfoView(chatModel, close)
GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close)
}
}
}
@@ -149,8 +163,21 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
hideKeyboard(view)
withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
} else {
member to null
}
setGroupMembers(groupInfo, chatModel)
ModalManager.shared.showModalCloseable(true) { close ->
GroupMemberInfoView(groupInfo, member, stats, chatModel, close, close)
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
}
}
}
},
@@ -183,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) }
@@ -205,6 +235,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.callManager.acceptIncomingCall(invitation = invitation)
}
},
acceptFeature = { contact, feature, param ->
withApi {
chatModel.controller.allowFeatureToContact(contact, feature, param)
}
},
addMembers = { groupInfo ->
hideKeyboard(view)
withApi {
@@ -261,6 +296,7 @@ fun ChatLayout(
joinGroup: (Long) -> Unit,
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
addMembers: (GroupInfo) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
@@ -301,7 +337,7 @@ fun ChatLayout(
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, markRead, setFloatingButton, onComposed,
receiveFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
)
}
}
@@ -424,10 +460,15 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
Modifier.padding(start = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
cInfo.displayName, fontWeight = FontWeight.SemiBold,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
Row(verticalAlignment = Alignment.CenterVertically) {
if ((cInfo as? ChatInfo.Direct)?.contact?.verified == true) {
ContactVerifiedShield()
}
Text(
cInfo.displayName, fontWeight = FontWeight.SemiBold,
maxLines = 1, overflow = TextOverflow.Ellipsis
)
}
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.localAlias.isEmpty()) {
Text(
cInfo.fullName,
@@ -438,6 +479,11 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
}
}
@Composable
private fun ContactVerifiedShield() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight)
}
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
val CIListStateSaver = run {
@@ -466,6 +512,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
receiveFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: () -> Unit,
@@ -491,7 +538,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
Spacer(Modifier.size(8.dp))
val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } }
val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } }
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
val scrollToItem: (Long) -> Unit = { itemId: Long ->
val index = reversedChatItems.indexOfFirst { it.id == itemId }
@@ -530,7 +577,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
scope.launch {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
}
@@ -571,11 +618,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
} else {
Spacer(Modifier.size(42.dp))
}
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
} else {
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
} else { // direct message
@@ -586,7 +633,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent) 12.dp else 76.dp,
).then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
@@ -617,17 +664,29 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
// Don't autoscroll next time until it will be needed
shouldAutoScroll = false to chatId
}
val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() }
/*
* Since we use key with each item in LazyColumn, LazyColumn will not autoscroll to bottom item. We need to do it ourselves.
* When the first visible item (from bottom) is fully visible we can autoscroll to 0 item
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
* */
LaunchedEffect(Unit) {
snapshotFlow { chatItems.lastOrNull()?.id }
.distinctUntilChanged()
.filter { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 }
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
.collect {
listState.animateScrollToItem(0)
try {
if (listState.firstVisibleItemIndex == 0) {
listState.animateScrollToItem(0)
} else {
listState.animateScrollBy(scrollDistance)
}
} catch (e: CancellationException) {
/**
* When you tap and hold a finger on a lazy column with chatItems, and then you receive a message,
* this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll.
* Which breaks auto-scrolling to bottom. So just ignoring the exception
* */
}
}
}
}
@@ -984,6 +1043,7 @@ fun PreviewChatLayout() {
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
addMembers = { _ -> },
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },
@@ -1042,6 +1102,7 @@ fun PreviewGroupChatLayout() {
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
addMembers = { _ -> },
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },
@@ -1,8 +1,10 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.app.views.chat
import ComposeVoiceView
import ComposeFileView
import ComposeVoiceView
import android.Manifest
import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
@@ -32,27 +34,25 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.chat.item.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.*
import java.io.File
import java.nio.file.Files
@Serializable
sealed class ComposePreview {
@Serializable object NoPreview: ComposePreview()
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
@Serializable class ImagePreview(val images: List<String>): ComposePreview()
@Serializable class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
@Serializable class FilePreview(val fileName: String): ComposePreview()
@Serializable class ImagePreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
@Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview()
}
@Serializable
@@ -62,16 +62,26 @@ sealed class ComposeContextItem {
@Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
}
@Serializable
data class LiveMessage(
val chatItem: ChatItem,
val typedMsg: String,
val sentMsg: String,
val sent: Boolean
)
@Serializable
data class ComposeState(
val message: String = "",
val liveMessage: LiveMessage? = null,
val preview: ComposePreview = ComposePreview.NoPreview,
val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem,
val inProgress: Boolean = false,
val useLinkPreviews: Boolean
) {
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this(
constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this(
editingItem.content.text,
liveMessage,
chatItemPreview(editingItem),
ComposeContextItem.EditingItem(editingItem),
useLinkPreviews = useLinkPreviews
@@ -89,10 +99,13 @@ data class ComposeState(
is ComposePreview.ImagePreview -> true
is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true
else -> message.isNotEmpty()
else -> message.isNotEmpty() || liveMessage != null
}
hasContent && !inProgress
}
val endLiveDisabled: Boolean
get() = liveMessage != null && message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
val linkPreviewAllowed: Boolean
get() =
when (preview) {
@@ -108,6 +121,19 @@ data class ComposeState(
else -> null
}
val attachmentDisabled: Boolean
get() {
if (editing || liveMessage != null) return true
return when (preview) {
ComposePreview.NoPreview -> false
is ComposePreview.CLinkPreview -> false
else -> true
}
}
val empty: Boolean
get() = message.isEmpty() && preview is ComposePreview.NoPreview
companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
save = { json.encodeToString(serializer(), it.value) },
@@ -118,16 +144,24 @@ data class ComposeState(
}
}
sealed class RecordingState {
object NotStarted: RecordingState()
class Started(val filePath: String, val progressMs: Int = 0): RecordingState()
class Finished(val filePath: String, val durationMs: Int): RecordingState()
val filePathNullable: String?
get() = (this as? Started)?.filePath
}
fun chatItemPreview(chatItem: ChatItem): ComposePreview {
val fileName = chatItem.file?.fileName ?: ""
return when (val mc = chatItem.content.msgContent) {
is MsgContent.MCText -> ComposePreview.NoPreview
is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview)
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image))
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = chatItem.file?.fileName ?: "", mc.duration / 1000, true)
is MsgContent.MCFile -> {
val fileName = chatItem.file?.fileName ?: ""
ComposePreview.FilePreview(fileName)
}
// TODO: include correct type
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true)
is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName))
is MsgContent.MCUnknown, null -> ComposePreview.NoPreview
}
}
@@ -148,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 ->
@@ -209,8 +234,7 @@ fun ComposeView(
}
if (imagesPreview.isNotEmpty()) {
chosenContent.value = content
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview, content))
}
}
val processPickedFile = { uri: Uri?, text: String? ->
@@ -219,8 +243,7 @@ fun ComposeView(
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) {
chosenFile.value = uri
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
}
} else {
AlertManager.shared.showAlertMsg(
@@ -233,6 +256,7 @@ fun ComposeView(
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) {
@@ -310,128 +334,160 @@ fun ComposeView(
cancelledLinks.clear()
}
fun checkLinkPreview(): MsgContent {
fun clearState(live: Boolean = false) {
if (live) {
composeState.value = composeState.value.copy(inProgress = false)
} else {
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
resetLinkPreview()
}
recState.value = RecordingState.NotStarted
textStyle.value = smallFont
chatModel.removeLiveDummy()
}
fun deleteUnusedFiles() {
chatModel.filesToDelete.forEach { it.delete() }
chatModel.filesToDelete.clear()
}
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
quotedItemId = quoted,
mc = mc,
live = live
)
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem?.chatItem
}
suspend fun sendMessageAsync(text: String?, live: Boolean): ChatItem? {
val cInfo = chat.chatInfo
val cs = composeState.value
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
val url = parseMessage(cs.message)
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(cs.message, preview = lp)
} else {
MsgContent.MCText(cs.message)
var sent: ChatItem?
val msgText = text ?: cs.message
fun sending() {
composeState.value = composeState.value.copy(inProgress = true)
}
fun checkLinkPreview(): MsgContent {
return when (val composePreview = cs.preview) {
is ComposePreview.CLinkPreview -> {
val url = parseMessage(msgText)
val lp = composePreview.linkPreview
if (lp != null && url == lp.uri) {
MsgContent.MCLink(msgText, preview = lp)
} else {
MsgContent.MCText(msgText)
}
}
else -> MsgContent.MCText(msgText)
}
}
fun updateMsgContent(msgContent: MsgContent): MsgContent {
return when (msgContent) {
is MsgContent.MCText -> checkLinkPreview()
is MsgContent.MCLink -> checkLinkPreview()
is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image)
is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration)
is MsgContent.MCFile -> MsgContent.MCFile(msgText)
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json)
}
}
suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
val oldMsgContent = ei.content.msgContent
if (oldMsgContent != null) {
val updatedItem = chatModel.controller.apiUpdateChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = ei.meta.itemId,
mc = updateMsgContent(oldMsgContent),
live = live
)
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
return updatedItem?.chatItem
}
return null
}
val liveMessage = cs.liveMessage
if (!live) {
if (liveMessage != null) composeState.value = cs.copy(liveMessage = null)
sending()
}
if (cs.contextItem is ComposeContextItem.EditingItem) {
val ei = cs.contextItem.chatItem
sent = updateMessage(ei, cInfo, live)
} else if (liveMessage != null && liveMessage.sent) {
sent = updateMessage(liveMessage.chatItem, cInfo, live)
} else {
val msgs: ArrayList<MsgContent> = ArrayList()
val files: ArrayList<String> = ArrayList()
when (val preview = cs.preview) {
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.ImagePreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
}
if (file != null) {
files.add(file)
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
}
}
}
is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath)
val actualFile = File(getAppFilePath(SimplexApp.context, tmpFile.name.replaceAfter(RecorderNative.extension, "")))
withContext(Dispatchers.IO) {
Files.move(tmpFile.toPath(), actualFile.toPath())
}
files.add(actualFile.name)
deleteUnusedFiles()
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
}
is ComposePreview.FilePreview -> {
val file = saveFileFromUri(context, preview.uri)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
}
}
}
else -> MsgContent.MCText(cs.message)
val quotedItemId: Long? = when (cs.contextItem) {
is ComposeContextItem.QuotedItem -> cs.contextItem.chatItem.id
else -> null
}
sent = null
msgs.forEachIndexed { index, content ->
if (index > 0) delay(100)
sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index),
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
)
}
if (sent == null && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview || cs.preview is ComposePreview.VoicePreview)) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
}
}
}
fun updateMsgContent(msgContent: MsgContent): MsgContent {
val cs = composeState.value
return when (msgContent) {
is MsgContent.MCText -> checkLinkPreview()
is MsgContent.MCLink -> checkLinkPreview()
is MsgContent.MCImage -> MsgContent.MCImage(cs.message, image = msgContent.image)
is MsgContent.MCVoice -> MsgContent.MCVoice(cs.message, duration = msgContent.duration)
is MsgContent.MCFile -> MsgContent.MCFile(cs.message)
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = cs.message, json = msgContent.json)
}
}
fun clearState() {
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
textStyle.value = smallFont
chosenContent.value = emptyList()
chosenAudio.value = null
chosenFile.value = null
linkUrl.value = null
prevLinkUrl.value = null
pendingLinkUrl.value = null
cancelledLinks.clear()
clearState(live)
return sent
}
fun sendMessage() {
composeState.value = composeState.value.copy(inProgress = true)
val cInfo = chat.chatInfo
val cs = composeState.value
when (val contextItem = cs.contextItem) {
is ComposeContextItem.EditingItem -> {
val ei = contextItem.chatItem
val oldMsgContent = ei.content.msgContent
if (oldMsgContent != null) {
withApi {
val updatedItem = chatModel.controller.apiUpdateChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = ei.meta.itemId,
mc = updateMsgContent(oldMsgContent)
)
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
clearState()
}
}
}
else -> {
val msgs: ArrayList<MsgContent> = ArrayList()
val files: ArrayList<String> = ArrayList()
when (val preview = cs.preview) {
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(cs.message))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.ImagePreview -> {
chosenContent.value.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
}
if (file != null) {
files.add(file)
msgs.add(MsgContent.MCImage(if (msgs.isEmpty()) cs.message else "", preview.images[index]))
}
}
}
is ComposePreview.VoicePreview -> {
val chosenAudioVal = chosenAudio.value
if (chosenAudioVal != null) {
val file = chosenAudioVal.first.toFile().name
files.add((file))
chatModel.filesToDelete.remove(chosenAudioVal.first.toFile())
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) cs.message else "", chosenAudioVal.second / 1000))
}
}
is ComposePreview.FilePreview -> {
val chosenFileVal = chosenFile.value
if (chosenFileVal != null) {
val file = saveFileFromUri(context, chosenFileVal)
if (file != null) {
files.add((file))
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) cs.message else ""))
}
}
}
}
val quotedItemId: Long? = when (contextItem) {
is ComposeContextItem.QuotedItem -> contextItem.chatItem.id
else -> null
}
if (msgs.isNotEmpty()) {
withApi {
msgs.forEachIndexed { index, content ->
if (index > 0) delay(100)
val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = files.getOrNull(index),
quotedItemId = if (index == 0) quotedItemId else null,
mc = content
)
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
}
clearState()
}
} else {
clearState()
}
}
withBGApi {
sendMessageAsync(null, false)
}
}
@@ -450,34 +506,17 @@ fun ComposeView(
fun onAudioAdded(filePath: String, durationMs: Int, finished: Boolean) {
val file = File(filePath)
chosenAudio.value = file.toUri() to durationMs
chatModel.filesToDelete.add(file)
composeState.value = composeState.value.copy(preview = ComposePreview.VoicePreview(filePath, durationMs, finished))
}
fun allowVoiceToContact() {
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
val prefs = contact.mergedPreferences.toPreferences().copy(voice = ChatPreference(allow = FeatureAllowed.YES))
withApi {
val toContact = chatModel.controller.apiSetContactPrefs(contact.contactId, prefs)
if (toContact != null) {
chatModel.updateContact(toContact)
}
chatModel.controller.allowFeatureToContact(contact, ChatFeature.Voice)
}
}
fun showDisabledVoiceAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.voice_messages_prohibited),
text = generalGetString(
if (chat.chatInfo is ChatInfo.Direct)
R.string.ask_your_contact_to_enable_voice
else
R.string.only_group_owners_can_enable_voice
)
)
}
fun cancelLinkPreview() {
val uri = composeState.value.linkPreview?.uri
if (uri != null) {
@@ -489,17 +528,70 @@ fun ComposeView(
fun cancelImages() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenContent.value = emptyList()
}
fun cancelVoice() {
val filePath = recState.value.filePathNullable
recState.value = RecordingState.NotStarted
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenAudio.value = null
withBGApi {
RecorderNative.stopRecording?.invoke()
AudioPlayer.stop(filePath)
filePath?.let { File(it).delete() }
}
}
fun cancelFile() {
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
chosenFile.value = null
}
fun truncateToWords(s: String): String {
var acc = ""
val word = StringBuilder()
for (c in s) {
if (c.isLetter() || c.isDigit()) {
word.append(c)
} else {
acc = acc + word.toString() + c
word.clear()
}
}
return acc
}
suspend fun sendLiveMessage() {
val cs = composeState.value
val typedMsg = cs.message
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
val ci = sendMessageAsync(typedMsg, live = true)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
}
} else if (cs.liveMessage == null) {
val cItem = chatModel.addLiveDummy(chat.chatInfo)
composeState.value = composeState.value.copy(liveMessage = LiveMessage(cItem, typedMsg = typedMsg, sentMsg = typedMsg, sent = false))
}
}
fun liveMessageToSend(lm: LiveMessage, t: String): String? {
val s = if (t != lm.typedMsg) truncateToWords(t) else t
return if (s != lm.sentMsg) s else null
}
suspend fun updateLiveMessage() {
val typedMsg = composeState.value.message
val liveMessage = composeState.value.liveMessage
if (liveMessage != null) {
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
if (sentMsg != null) {
val ci = sendMessageAsync(sentMsg, live = true)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
}
} else if (liveMessage.typedMsg != typedMsg) {
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
}
}
}
@Composable
@@ -564,47 +656,95 @@ fun ComposeView(
modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
val attachEnabled = !composeState.value.editing && composeState.value.preview !is ComposePreview.VoicePreview
IconButton(showChooseAttachment, enabled = attachEnabled) {
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) {
Icon(
Icons.Filled.AttachFile,
contentDescription = stringResource(R.string.attach),
tint = if (attachEnabled) MaterialTheme.colors.primary else HighOrLowlight,
tint = if (!composeState.value.attachmentDisabled) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
)
}
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.voiceMessageAllowed }
val needToAllowVoiceToContact = remember(chat.chatInfo) {
when (chat.chatInfo) {
is ChatInfo.Direct -> with(chat.chatInfo.contact.mergedPreferences.voice) {
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
contactPreference.allow == FeatureAllowed.YES
}
else -> false
}
}
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()
}
}
val needToAllowVoiceToContact = remember(chat.chatInfo) {
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) {
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
contactPreference.allow == FeatureAllowed.YES
}
}
LaunchedEffect(Unit) {
snapshotFlow { recState.value }
.distinctUntilChanged()
.collect {
when(it) {
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
is RecordingState.Finished -> onAudioAdded(it.filePath, it.durationMs, true)
is RecordingState.NotStarted -> {}
}
}
}
fun clearCurrentDraft() {
if (chatModel.draftChatId.value == chat.id) {
chatModel.draft.value = null
chatModel.draftChatId.value = null
}
}
val activity = LocalContext.current as Activity
DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation
onDispose {
if (orientation == activity.resources.configuration.orientation) {
val cs = composeState.value
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage()
resetLinkPreview()
clearCurrentDraft()
deleteUnusedFiles()
} else if (!composeState.value.empty) {
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
}
chatModel.draft.value = composeState.value
chatModel.draftChatId.value = chat.id
} else {
clearCurrentDraft()
deleteUnusedFiles()
}
chatModel.removeLiveDummy()
}
}
}
SendMsgView(
composeState,
showVoiceRecordIcon = true,
allowedVoiceByPrefs = allowedVoiceByPrefs,
needToAllowVoiceToContact = needToAllowVoiceToContact,
recState,
chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
needToAllowVoiceToContact,
allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
sendMessage = {
sendMessage()
resetLinkPreview()
},
::onMessageChange,
::onAudioAdded,
::allowVoiceToContact,
::showDisabledVoiceAlert,
textStyle
sendLiveMessage = ::sendLiveMessage,
updateLiveMessage = ::updateLiveMessage,
cancelLiveMessage = {
composeState.value = composeState.value.copy(liveMessage = null)
chatModel.removeLiveDummy()
},
onMessageChange = ::onMessageChange,
textStyle = textStyle
)
}
}
@@ -46,7 +46,7 @@ fun ComposeVoiceView(
val endTime = when {
finishedRecording -> duration.value
audioPlaying.value -> recordedDurationMs
else -> MAX_VOICE_MILLIS_FOR_SENDING.toInt()
else -> MAX_VOICE_MILLIS_FOR_SENDING
}
val to = ((startTime.toDouble() / endTime) * maxWidth.value).dp
progressBarWidth.animateTo(to.value, audioProgressBarAnimationSpec())
@@ -20,6 +20,7 @@ import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggle
@Composable
fun ContactPreferencesView(
@@ -85,6 +86,14 @@ private fun ContactPreferencesLayout(
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.contact_preferences))
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl ?: 86400))
}
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
}
SectionSpacer()
val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) }
FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) {
applyPrefs(featuresAllowed.copy(fullDelete = it))
@@ -112,7 +121,8 @@ private fun FeatureSection(
onSelected: (ContactFeatureAllowed) -> Unit
) {
val enabled = FeatureEnabled.enabled(
user = ChatPreference(allow = allowFeature.value.allowed),
feature.asymmetric,
user = SimpleChatPreference(allow = allowFeature.value.allowed),
contact = pref.contactPreference
)
@@ -140,6 +150,50 @@ private fun FeatureSection(
SectionTextFooter(feature.enabledDescription(enabled))
}
@Composable
private fun TimedMessagesFeatureSection(
featuresAllowed: ContactFeaturesAllowed,
pref: ContactUserPreferenceTimed,
allowFeature: State<Boolean>,
onTTLUpdated: (Int?) -> Unit,
onSelected: (Boolean, Int?) -> Unit
) {
val enabled = FeatureEnabled.enabled(
ChatFeature.TimedMessages.asymmetric,
user = TimedMessagesPreference(allow = if (allowFeature.value) FeatureAllowed.YES else FeatureAllowed.NO),
contact = pref.contactPreference
)
SectionView(
ChatFeature.TimedMessages.text.uppercase(),
icon = ChatFeature.TimedMessages.iconFilled,
iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red,
leadingIcon = true,
) {
SectionItemView {
PreferenceToggle(
generalGetString(R.string.chat_preferences_you_allow),
checked = allowFeature.value,
) { allow ->
onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
}
}
SectionDivider()
InfoRow(
generalGetString(R.string.chat_preferences_contact_allows),
pref.contactPreference.allow.text
)
SectionDivider()
if (featuresAllowed.timedMessagesAllowed) {
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
TimedMessagesTTLPicker(ttl, onTTLUpdated)
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
}
}
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
}
@Composable
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
@@ -153,6 +207,20 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
}
}
@Composable
fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) {
val ttlValues = TimedMessagesPreference.ttlValues
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
SectionItemView {
ExposedDropDownSettingRow(
generalGetString(R.string.delete_after),
values.map { it to TimedMessagesPreference.ttlText(it) },
selection,
onSelected = onSelected
)
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
@@ -0,0 +1,53 @@
package chat.simplex.app.views.chat
import android.Manifest
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ScanCodeLayout(verifyCode, close)
}
@Composable
private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
Column(
Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.scan_code), false)
Box(
Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1F)
.padding(bottom = DEFAULT_PADDING)
) {
QRCodeScanner { text ->
verifyCode(text) {
if (it) {
close()
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.incorrect_code)
)
}
}
}
}
Text(stringResource(R.string.scan_code_from_contacts_app))
}
}
@@ -6,28 +6,36 @@ import android.app.Activity
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.text.InputType
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.*
import android.widget.EditText
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.*
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.inputmethod.EditorInfoCompat
@@ -35,184 +43,124 @@ import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.core.widget.*
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.ChatItem
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.*
import java.io.*
@Composable
fun SendMsgView(
composeState: MutableState<ComposeState>,
showVoiceRecordIcon: Boolean,
allowedVoiceByPrefs: Boolean,
recState: MutableState<RecordingState>,
isDirectChat: Boolean,
liveMessageAlertShown: SharedPreference<Boolean>,
needToAllowVoiceToContact: Boolean,
sendMessage: () -> Unit,
onMessageChange: (String) -> Unit,
onAudioAdded: (String, Int, Boolean) -> Unit,
allowedVoiceByPrefs: Boolean,
allowVoiceToContact: () -> Unit,
showDisabledVoiceAlert: () -> Unit,
sendMessage: () -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null,
updateLiveMessage: (suspend () -> Unit)? = null,
cancelLiveMessage: (() -> Unit)? = null,
onMessageChange: (String) -> Unit,
textStyle: MutableState<TextStyle>
) {
Column(Modifier.padding(vertical = 8.dp)) {
Box {
val cs = composeState.value
val attachEnabled = !composeState.value.editing
val filePath = rememberSaveable { mutableStateOf(null as String?) }
var recordingTimeRange by rememberSaveable(saver = LongRange.saver) { mutableStateOf(0L..0L) } // since..to
val showVoiceButton = ((cs.message.isEmpty() || recordingTimeRange.first > 0L) && showVoiceRecordIcon && attachEnabled && cs.preview is ComposePreview.NoPreview) || filePath.value != null
Box(if (recordingTimeRange.first == 0L)
Modifier
else
Modifier.clickable(false, onClick = {})
) {
NativeKeyboard(composeState, textStyle, onMessageChange)
Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, onMessageChange)
// Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview) {
Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { }))
}
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
}
Box(Modifier.align(Alignment.BottomEnd)) {
val sendButtonSize = remember { Animatable(36f) }
val sendButtonAlpha = remember { Animatable(1f) }
val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO))
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
// Making LiveMessage alive when screen orientation was changed
if (cs.liveMessage != null && sendLiveMessage != null && updateLiveMessage != null) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
}
}
Box(Modifier.align(Alignment.BottomEnd)) {
val icon = if (cs.editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
if (cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.VoicePreview || cs.preview is ComposePreview.FilePreview)) {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp)
} else if (!showVoiceButton) {
IconButton(sendMessage, Modifier.size(36.dp), enabled = cs.sendEnabled()) {
Icon(
icon,
stringResource(R.string.icon_descr_send_message),
tint = Color.White,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
.clip(CircleShape)
.background(color)
)
}
} else {
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.RECORD_AUDIO,
)
)
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
val recordingInProgress: State<Boolean> = remember { rec.recordingInProgress }
var now by remember { mutableStateOf(System.currentTimeMillis()) }
LaunchedEffect(Unit) {
while (isActive) {
now = System.currentTimeMillis()
if (recordingTimeRange.first != 0L && recordingInProgress.value && composeState.value.preview is ComposePreview.VoicePreview) {
filePath.value?.let { onAudioAdded(it, (now - recordingTimeRange.first).toInt(), false) }
}
delay(100)
}
}
val stopRecordingAndAddAudio: () -> Unit = {
rec.stop()
recordingTimeRange = recordingTimeRange.first..System.currentTimeMillis()
filePath.value?.let { onAudioAdded(it, (recordingTimeRange.last - recordingTimeRange.first).toInt(), true) }
}
val startStopRecording: () -> Unit = {
when {
showProgress -> ProgressIndicator()
showVoiceButton -> {
Row(verticalAlignment = Alignment.CenterVertically) {
val stopRecOnNextClick = remember { mutableStateOf(false) }
when {
needToAllowVoiceToContact -> {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.allow_voice_messages_question),
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
confirmText = generalGetString(R.string.allow_verb),
dismissText = generalGetString(R.string.cancel_verb),
onConfirm = allowVoiceToContact,
)
needToAllowVoiceToContact || !allowedVoiceByPrefs -> {
DisallowedVoiceButton {
if (needToAllowVoiceToContact) {
showNeedToAllowVoiceAlert(allowVoiceToContact)
} else {
showDisabledVoiceAlert(isDirectChat)
}
}
}
!allowedVoiceByPrefs -> showDisabledVoiceAlert()
!permissionsState.allPermissionsGranted -> permissionsState.launchMultiplePermissionRequest()
recordingInProgress.value -> stopRecordingAndAddAudio()
filePath.value == null -> {
recordingTimeRange = System.currentTimeMillis()..0L
filePath.value = rec.start(stopRecordingAndAddAudio)
filePath.value?.let { onAudioAdded(it, (now - recordingTimeRange.first).toInt(), false) }
!permissionsState.allPermissionsGranted ->
VoiceButtonWithoutPermission { permissionsState.launchMultiplePermissionRequest() }
else ->
RecordVoiceView(recState, stopRecOnNextClick)
}
if (sendLiveMessage != null
&& updateLiveMessage != null
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem) {
Spacer(Modifier.width(10.dp))
StartLiveMessageButton {
if (composeState.value.preview is ComposePreview.NoPreview) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
}
}
}
}
var stopRecOnNextClick by remember { mutableStateOf(false) }
val context = LocalContext.current
DisposableEffect(stopRecOnNextClick) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
if (stopRecOnNextClick) {
// Lock orientation to current orientation because screen rotation will break the recording
activity.requestedOrientation = if (activity.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
else
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
// Unlock orientation
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
}
val cleanUp = { remove: Boolean ->
rec.stop()
AudioPlayer.stop(filePath.value)
if (remove) filePath.value?.let { File(it).delete() }
filePath.value = null
stopRecOnNextClick = false
recordingTimeRange = 0L..0L
}
LaunchedEffect(cs.preview) {
if (cs.preview !is ComposePreview.VoicePreview && filePath.value != null) {
// Pressed on X icon in preview
cleanUp(true)
}
}
val interactionSource = interactionSourceWithTapDetection(
// It's just a key for triggering dropping a state in the compose function. Without it
// nothing will react on changed params like needToAllowVoiceToContact or allowedVoiceByPrefs
needToAllowVoiceToContact.toString() + allowedVoiceByPrefs.toString(),
onPress = {
if (filePath.value == null) startStopRecording()
},
onClick = {
// Voice not allowed or not granted voice record permission for the app
if (!allowedVoiceByPrefs || !permissionsState.allPermissionsGranted) return@interactionSourceWithTapDetection
if (!recordingInProgress.value && filePath.value != null) {
sendMessage()
cleanUp(false)
} else if (stopRecOnNextClick) {
stopRecordingAndAddAudio()
stopRecOnNextClick = false
} else {
// tapped and didn't hold a finger
stopRecOnNextClick = true
}
},
onCancel = startStopRecording,
onRelease = startStopRecording
)
val sendButtonModifier = if (recordingTimeRange.last != 0L)
Modifier.clip(CircleShape).background(color)
else
Modifier
IconButton({}, Modifier.size(36.dp), enabled = !cs.inProgress, interactionSource = interactionSource) {
Icon(
when {
recordingTimeRange.last != 0L -> Icons.Outlined.ArrowUpward
stopRecOnNextClick -> Icons.Filled.Stop
allowedVoiceByPrefs -> Icons.Filled.KeyboardVoice
else -> Icons.Outlined.KeyboardVoice
},
stringResource(R.string.icon_descr_record_voice_message),
tint = when {
recordingTimeRange.last != 0L -> Color.White
stopRecOnNextClick -> MaterialTheme.colors.primary
allowedVoiceByPrefs -> MaterialTheme.colors.primary
else -> HighOrLowlight
},
modifier = Modifier
.size(36.dp)
.padding(4.dp)
.then(sendButtonModifier)
)
}
DisposableEffect(Unit) {
onDispose {
rec.stop()
}
cs.liveMessage?.sent == false && cs.message.isEmpty() -> {
CancelLiveMessageButton {
cancelLiveMessage?.invoke()
}
}
else -> {
val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) Icons.Filled.Check else Icons.Outlined.ArrowUpward
val disabled = !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled
if (cs.liveMessage == null &&
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null
) {
var showDropdown by rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false },
Modifier.width(220.dp),
) {
ItemAction(
generalGetString(R.string.send_live_message),
Icons.Filled.Bolt,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown = false
}
)
}
} else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
}
}
}
@@ -224,6 +172,7 @@ fun SendMsgView(
private fun NativeKeyboard(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
onMessageChange: (String) -> Unit
) {
val cs = composeState.value
@@ -234,7 +183,6 @@ private fun NativeKeyboard(
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
var showKeyboard by remember { mutableStateOf(false) }
LaunchedEffect(cs.contextItem) {
if (cs.contextItem is ComposeContextItem.QuotedItem) {
@@ -255,6 +203,7 @@ private fun NativeKeyboard(
) {
super.setOnReceiveContentListener(mimeTypes, listener)
}
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
val connection = super.onCreateInputConnection(editorInfo)
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
@@ -301,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(
@@ -312,6 +262,296 @@ private fun NativeKeyboard(
}
}
@Composable
private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) {
IconButton(
{ composeState.value = composeState.value.copy(message = "") },
Modifier.align(Alignment.TopEnd).size(36.dp)
) {
Icon(Icons.Filled.Close, null, Modifier.padding(7.dp).size(36.dp), tint = HighOrLowlight)
}
}
@Composable
private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) {
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
DisposableEffect(Unit) { onDispose { rec.stop() } }
val stopRecordingAndAddAudio: () -> Unit = {
recState.value.filePathNullable?.let {
recState.value = RecordingState.Finished(it, rec.stop())
}
}
if (stopRecOnNextClick.value) {
LaunchedEffect(recState.value) {
if (recState.value is RecordingState.NotStarted) {
stopRecOnNextClick.value = false
}
}
// Lock orientation to current orientation because screen rotation will break the recording
LockToCurrentOrientationUntilDispose()
StopRecordButton(stopRecordingAndAddAudio)
} else {
val startRecording: () -> Unit = {
recState.value = RecordingState.Started(
filePath = rec.start { progress: Int?, finished: Boolean ->
val state = recState.value
if (state is RecordingState.Started && progress != null) {
recState.value = if (!finished)
RecordingState.Started(state.filePath, progress)
else
RecordingState.Finished(state.filePath, progress)
}
},
)
}
val interactionSource = interactionSourceWithTapDetection(
onPress = { if (recState.value is RecordingState.NotStarted) startRecording() },
onClick = {
if (stopRecOnNextClick.value) {
stopRecordingAndAddAudio()
} else {
// tapped and didn't hold a finger
stopRecOnNextClick.value = true
}
},
onCancel = stopRecordingAndAddAudio,
onRelease = stopRecordingAndAddAudio
)
RecordVoiceButton(interactionSource)
}
}
@Composable
private fun DisallowedVoiceButton(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
Icons.Outlined.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message),
tint = HighOrLowlight,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
@Composable
private fun VoiceButtonWithoutPermission(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
Icons.Filled.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(34.dp)
.padding(4.dp)
)
}
}
@Composable
private fun LockToCurrentOrientationUntilDispose() {
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context as Activity
val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager
val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation
activity.requestedOrientation = when (rotation) {
android.view.Surface.ROTATION_90 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
android.view.Surface.ROTATION_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
android.view.Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
// Unlock orientation
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
}
}
@Composable
private fun StopRecordButton(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
Icons.Filled.Stop,
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
@Composable
private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
IconButton({}, Modifier.size(36.dp), interactionSource = interactionSource) {
Icon(
Icons.Filled.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(34.dp)
.padding(4.dp)
)
}
}
@Composable
private fun ProgressIndicator() {
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp)
}
@Composable
private fun CancelLiveMessageButton(
onClick: () -> Unit
) {
IconButton(onClick, Modifier.size(36.dp)) {
Icon(
Icons.Filled.Close,
stringResource(R.string.icon_descr_cancel_live_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
@Composable
private fun SendMsgButton(
icon: ImageVector,
sizeDp: Animatable<Float, AnimationVector1D>,
alpha: Animatable<Float, AnimationVector1D>,
enabled: Boolean,
sendMessage: () -> Unit,
onLongClick: (() -> Unit)? = null
) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier.requiredSize(36.dp)
.combinedClickable(
onClick = sendMessage,
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
icon,
stringResource(R.string.icon_descr_send_message),
tint = Color.White,
modifier = Modifier
.size(sizeDp.value.dp)
.padding(4.dp)
.alpha(alpha.value)
.clip(CircleShape)
.background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight)
.padding(3.dp)
)
}
}
@Composable
private fun StartLiveMessageButton(onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier.requiredSize(36.dp)
.clickable(
onClick = onClick,
enabled = true,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Filled.Bolt,
stringResource(R.string.icon_descr_send_message),
tint = MaterialTheme.colors.primary,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
)
}
}
private fun startLiveMessage(
scope: CoroutineScope,
send: suspend () -> Unit,
update: suspend () -> Unit,
sendButtonSize: Animatable<Float, AnimationVector1D>,
sendButtonAlpha: Animatable<Float, AnimationVector1D>,
composeState: MutableState<ComposeState>,
liveMessageAlertShown: SharedPreference<Boolean>
) {
fun run() {
scope.launch {
while (composeState.value.liveMessage != null) {
sendButtonSize.animateTo(if (sendButtonSize.value == 36f) 32f else 36f, tween(700, 50))
}
sendButtonSize.snapTo(36f)
}
scope.launch {
while (composeState.value.liveMessage != null) {
sendButtonAlpha.animateTo(if (sendButtonAlpha.value == 1f) 0.75f else 1f, tween(700, 50))
}
sendButtonAlpha.snapTo(1f)
}
scope.launch {
delay(3000)
while (composeState.value.liveMessage != null) {
update()
delay(3000)
}
}
}
fun start() = withBGApi {
if (composeState.value.liveMessage == null) {
send()
}
run()
}
if (liveMessageAlertShown.state.value) {
start()
} else {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.live_message),
text = generalGetString(R.string.send_live_message_desc),
confirmText = generalGetString(R.string.send_verb),
onConfirm = {
liveMessageAlertShown.set(true)
start()
})
}
}
private fun showNeedToAllowVoiceAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.allow_voice_messages_question),
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
confirmText = generalGetString(R.string.allow_verb),
dismissText = generalGetString(R.string.cancel_verb),
onConfirm = onConfirm,
)
}
private fun showDisabledVoiceAlert(isDirectChat: Boolean) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.voice_messages_prohibited),
text = generalGetString(
if (isDirectChat)
R.string.ask_your_contact_to_enable_voice
else
R.string.only_group_owners_can_enable_voice
)
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -326,13 +566,14 @@ fun PreviewSendMsgView() {
SendMsgView(
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
showVoiceRecordIcon = false,
allowedVoiceByPrefs = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> },
allowVoiceToContact = {},
showDisabledVoiceAlert = {},
textStyle = textStyle
)
}
@@ -353,13 +594,14 @@ fun PreviewSendMsgViewEditing() {
SendMsgView(
composeState = remember { mutableStateOf(composeStateEditing) },
showVoiceRecordIcon = false,
allowedVoiceByPrefs = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> },
allowVoiceToContact = {},
showDisabledVoiceAlert = {},
textStyle = textStyle
)
}
@@ -375,18 +617,19 @@ fun PreviewSendMsgViewEditing() {
fun PreviewSendMsgViewInProgress() {
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt"), inProgress = true, useLinkPreviews = true)
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt", getAppFileUri("test.txt")), inProgress = true, useLinkPreviews = true)
SimpleXTheme {
SendMsgView(
composeState = remember { mutableStateOf(composeStateInProgress) },
showVoiceRecordIcon = false,
allowedVoiceByPrefs = false,
recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> },
allowVoiceToContact = {},
showDisabledVoiceAlert = {},
textStyle = textStyle
)
}
@@ -0,0 +1,137 @@
package chat.simplex.app.views.chat
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun VerifyCodeView(
displayName: String,
connectionCode: String?,
connectionVerified: Boolean,
verify: suspend (String?) -> Pair<Boolean, String>?,
close: () -> Unit,
) {
if (connectionCode != null) {
VerifyCodeLayout(
displayName,
connectionCode,
connectionVerified,
verifyCode = { newCode, cb ->
withBGApi {
val res = verify(newCode)
if (res != null) {
val (verified) = res
cb(verified)
if (verified) close()
}
}
}
)
}
}
@Composable
private fun VerifyCodeLayout(
displayName: String,
connectionCode: String,
connectionVerified: Boolean,
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.security_code), false)
val splitCode = splitToParts(connectionCode, 24)
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
if (connectionVerified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 4.dp).size(22.dp), tint = HighOrLowlight)
Text(String.format(stringResource(R.string.is_verified), displayName))
} else {
Text(String.format(stringResource(R.string.is_not_verified), displayName))
}
}
SectionView {
QRCode(connectionCode, Modifier.aspectRatio(1f))
}
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.weight(2f))
SelectionContainer(Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING_HALF)) {
Text(
splitCode,
fontFamily = FontFamily.Monospace,
fontSize = 18.sp,
maxLines = 20
)
}
val context = LocalContext.current
Box(Modifier.weight(1f)) {
IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
Icon(Icons.Filled.Share, null, tint = MaterialTheme.colors.primary)
}
}
Spacer(Modifier.weight(1f))
}
Text(
generalGetString(R.string.to_verify_compare),
Modifier.padding(bottom = DEFAULT_PADDING)
)
Row(
Modifier.padding(bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally),
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
if (connectionVerified) {
SimpleButton(generalGetString(R.string.clear_verification), Icons.Outlined.Shield) {
verifyCode(null) {}
}
} else {
SimpleButton(generalGetString(R.string.scan_code), Icons.Outlined.QrCode) {
ModalManager.shared.showModal {
ScanCodeView(verifyCode) { }
}
}
SimpleButton(generalGetString(R.string.mark_code_verified), Icons.Outlined.VerifiedUser) {
verifyCode(connectionCode) { verified ->
if (!verified) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.incorrect_code)
)
}
}
}
}
}
}
}
private fun splitToParts(s: String, length: Int): String {
if (length >= s.length) return s
return (0..(s.length - 1) / length)
.map { s.drop(it * length).take(length) }
.joinToString(separator = "\n")
}
@@ -6,6 +6,7 @@ import SectionItemView
import SectionSpacer
import SectionTextFooter
import SectionView
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -23,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
@@ -32,7 +34,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.*
@Composable
fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
@@ -45,6 +47,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
.sortedBy { it.displayName.lowercase() },
developerTools,
groupLink,
addMembers = {
withApi {
setGroupMembers(groupInfo, chatModel)
@@ -56,8 +59,23 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
showMemberInfo = { member ->
withApi {
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val (_, code) = if (member.memberActive) {
try {
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
} else {
member to null
}
ModalManager.shared.showModalCloseable(true) { closeCurrent ->
GroupMemberInfoView(groupInfo, member, stats, chatModel, closeCurrent) { closeCurrent(); close() }
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
closeCurrent()
close()
}
}
}
}
},
@@ -78,8 +96,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
manageGroupLink = {
withApi {
val groupLink = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink) }
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) }
}
}
)
@@ -128,6 +145,7 @@ fun GroupChatInfoLayout(
groupInfo: GroupInfo,
members: List<GroupMember>,
developerTools: Boolean,
groupLink: String?,
addMembers: () -> Unit,
showMemberInfo: (GroupMember) -> Unit,
editGroupProfile: () -> Unit,
@@ -163,7 +181,13 @@ fun GroupChatInfoLayout(
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
if (groupInfo.canAddMembers) {
SectionItemView(manageGroupLink) { GroupLinkButton() }
SectionItemView(manageGroupLink) {
if (groupLink == null) {
CreateGroupLinkButton()
} else {
GroupLinkButton()
}
}
SectionDivider()
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
SectionItemView(onAddMembersClick) {
@@ -206,7 +230,7 @@ fun GroupChatInfoLayout(
}
@Composable
fun GroupChatInfoHeader(cInfo: ChatInfo) {
private fun GroupChatInfoHeader(cInfo: ChatInfo) {
Column(
Modifier.padding(horizontal = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -239,7 +263,7 @@ private fun GroupPreferencesButton(onClick: () -> Unit) {
}
@Composable
fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
Row(
Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
@@ -255,7 +279,7 @@ fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
}
@Composable
fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
Column {
members.forEachIndexed { index, member ->
SectionItemView({ showMemberInfo(member) }, minHeight = 50.dp) {
@@ -269,7 +293,7 @@ fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Uni
}
@Composable
fun MemberRow(member: GroupMember, user: Boolean = false) {
private fun MemberRow(member: GroupMember, user: Boolean = false) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -281,10 +305,15 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
) {
ProfileImage(size = 46.dp, member.image)
Column {
Text(
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (member.memberIncognito) Indigo else Color.Unspecified
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
MemberVerifiedShield()
}
Text(
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
color = if (member.memberIncognito) Indigo else Color.Unspecified
)
}
val s = member.memberStatus.shortText
val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s
Text(
@@ -304,7 +333,12 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
}
@Composable
fun GroupLinkButton() {
private fun MemberVerifiedShield() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 3.dp).size(16.dp), tint = HighOrLowlight)
}
@Composable
private fun GroupLinkButton() {
Row(
Modifier
.fillMaxSize(),
@@ -320,6 +354,23 @@ fun GroupLinkButton() {
}
}
@Composable
private fun CreateGroupLinkButton() {
Row(
Modifier
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Outlined.AddLink,
stringResource(R.string.create_group_link),
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(stringResource(R.string.create_group_link))
}
}
@Composable
fun EditGroupProfileButton() {
Row(
@@ -338,7 +389,7 @@ fun EditGroupProfileButton() {
}
@Composable
fun LeaveGroupButton() {
private fun LeaveGroupButton() {
Row(
Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
@@ -354,7 +405,7 @@ fun LeaveGroupButton() {
}
@Composable
fun DeleteGroupButton() {
private fun DeleteGroupButton() {
Row(
Modifier.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
@@ -376,12 +427,12 @@ fun PreviewGroupChatInfoLayout() {
GroupChatInfoLayout(
chat = Chat(
chatInfo = ChatInfo.Direct.sampleData,
chatItems = arrayListOf(),
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
chatItems = arrayListOf()
),
groupInfo = GroupInfo.sampleData,
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
developerTools = false,
groupLink = null,
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
)
}
@@ -1,10 +1,12 @@
package chat.simplex.app.views.chat.group
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -15,22 +17,32 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.GroupInfo
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?) {
var groupLink by remember { mutableStateOf(connReqContact) }
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current
fun createLink() {
creatingLink = true
withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
onGroupLinkUpdated(groupLink)
creatingLink = false
}
}
LaunchedEffect(Unit) {
if (groupLink == null && !creatingLink) {
createLink()
}
}
GroupLinkLayout(
groupLink = groupLink,
createLink = {
withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
}
},
creatingLink,
createLink = ::createLink,
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
deleteLink = {
AlertManager.shared.showAlertMsg(
@@ -42,17 +54,22 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) {
groupLink = null
onGroupLinkUpdated(null)
}
}
}
)
}
)
if (creatingLink) {
ProgressIndicator()
}
}
@Composable
fun GroupLinkLayout(
groupLink: String?,
creatingLink: Boolean,
createLink: () -> Unit,
share: () -> Unit,
deleteLink: () -> Unit
@@ -74,7 +91,7 @@ fun GroupLinkLayout(
verticalArrangement = Arrangement.SpaceEvenly
) {
if (groupLink == null) {
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, click = createLink)
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
} else {
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
Row(
@@ -99,3 +116,18 @@ fun GroupLinkLayout(
}
}
@Composable
fun ProgressIndicator() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
}
}
@@ -5,6 +5,7 @@ import SectionDivider
import SectionItemView
import SectionSpacer
import SectionView
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -12,6 +13,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -21,19 +23,20 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.SimplexServers
import chat.simplex.app.views.chat.SwitchAddressButton
import chat.simplex.app.views.chatlist.openChat
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import kotlinx.datetime.Clock
@Composable
fun GroupMemberInfoView(
groupInfo: GroupInfo,
member: GroupMember,
connStats: ConnectionStats?,
connectionCode: String?,
chatModel: ChatModel,
close: () -> Unit,
closeAll: () -> Unit, // Close all open windows up to ChatView
@@ -49,6 +52,7 @@ fun GroupMemberInfoView(
connStats,
newRole,
developerTools,
connectionCode,
getContactChat = { chatModel.getContactChat(it) },
knownDirectChat = {
withApi {
@@ -62,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()
}
}
@@ -91,6 +93,32 @@ fun GroupMemberInfoView(
},
switchMemberAddress = {
switchMemberAddress(chatModel, groupInfo, member)
},
verifyClicked = {
ModalManager.shared.showModalCloseable { close ->
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
VerifyCodeView(
mem.displayName,
connectionCode,
mem.verified,
verify = { code ->
chatModel.controller.apiVerifyGroupMember(mem.groupId, mem.groupMemberId, code)?.let { r ->
val (verified, existingCode) = r
chatModel.upsertGroupMember(
groupInfo,
mem.copy(
activeConn = mem.activeConn?.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)
)
r
}
},
close,
)
}
}
}
)
}
@@ -120,12 +148,14 @@ fun GroupMemberInfoLayout(
connStats: ConnectionStats?,
newRole: MutableState<GroupMemberRole>,
developerTools: Boolean,
connectionCode: String?,
getContactChat: (Long) -> Chat?,
knownDirectChat: (Chat) -> Unit,
newDirectChat: (Long) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
Column(
Modifier
@@ -141,17 +171,25 @@ fun GroupMemberInfoLayout(
}
SectionSpacer()
val contactId = member.memberContactId
if (contactId != null) {
val chat = getContactChat(contactId)
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directContact) {
if (member.memberActive) {
val contactId = member.memberContactId
if (contactId != null) {
SectionView {
OpenChatButton(onClick = { knownDirectChat(chat) })
}
SectionSpacer()
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
SectionView {
OpenChatButton(onClick = { newDirectChat(contactId) })
val chat = getContactChat(contactId)
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
OpenChatButton(onClick = { knownDirectChat(chat) })
if (connectionCode != null) {
SectionDivider()
}
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { newDirectChat(contactId) })
if (connectionCode != null) {
SectionDivider()
}
}
if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked)
}
}
SectionSpacer()
}
@@ -178,10 +216,10 @@ fun GroupMemberInfoLayout(
}
}
SectionSpacer()
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
if (connStats != null) {
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchMemberAddress)
SectionDivider()
if (connStats != null) {
val rcvServers = connStats.rcvServers
val sndServers = connStats.sndServers
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
@@ -196,8 +234,8 @@ fun GroupMemberInfoLayout(
}
}
}
SectionSpacer()
}
SectionSpacer()
if (member.canBeRemoved(groupInfo)) {
SectionView {
@@ -224,12 +262,17 @@ fun GroupMemberInfoHeader(member: GroupMember) {
horizontalAlignment = Alignment.CenterHorizontally
) {
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
Text(
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (member.verified) {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
}
Text(
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
color = MaterialTheme.colors.onBackground,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
if (member.fullName != "" && member.fullName != member.displayName) {
Text(
member.fullName, style = MaterialTheme.typography.h2,
@@ -319,12 +362,14 @@ fun PreviewGroupMemberInfoLayout() {
connStats = null,
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
developerTools = false,
connectionCode = "123",
getContactChat = { Chat.sampleData },
knownDirectChat = {},
newDirectChat = {},
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},
verifyClicked = {},
)
}
}
@@ -18,7 +18,9 @@ import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.TimedMessagesTTLPicker
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
@Composable
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
@@ -74,18 +76,30 @@ private fun GroupPreferencesLayout(
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.group_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl ?: 86400)))
}
FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable ->
if (enable == GroupFeatureEnabled.ON) {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400)))
} else {
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl)))
}
}
SectionSpacer()
val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) }
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo) {
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it)))
}
SectionSpacer()
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) }
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo) {
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
}
SectionSpacer()
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo) {
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
}
if (groupInfo.canEdit) {
@@ -100,21 +114,34 @@ private fun GroupPreferencesLayout(
}
@Composable
private fun FeatureSection(feature: GroupFeature, enableFeature: State<GroupFeatureEnabled>, groupInfo: GroupInfo, onSelected: (GroupFeatureEnabled) -> Unit) {
private fun FeatureSection(
feature: GroupFeature,
enableFeature: State<GroupFeatureEnabled>,
groupInfo: GroupInfo,
preferences: FullGroupPreferences,
onTTLUpdated: (Int?) -> Unit,
onSelected: (GroupFeatureEnabled) -> Unit
) {
SectionView {
val on = enableFeature.value == GroupFeatureEnabled.ON
val icon = if (on) feature.iconFilled else feature.icon
val iconTint = if (on) SimplexGreen else HighOrLowlight
val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON
if (groupInfo.canEdit) {
SectionItemView {
ExposedDropDownSettingRow(
PreferenceToggleWithIcon(
feature.text,
GroupFeatureEnabled.values().map { it to it.text },
enableFeature,
icon = icon,
iconTint = iconTint,
onSelected = onSelected
)
icon,
iconTint,
enableFeature.value == GroupFeatureEnabled.ON,
) { checked ->
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
}
}
if (timedOn) {
SectionDivider()
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
TimedMessagesTTLPicker(ttl, onTTLUpdated)
}
} else {
InfoRow(
@@ -123,6 +150,10 @@ private fun FeatureSection(feature: GroupFeature, enableFeature: State<GroupFeat
icon = icon,
iconTint = iconTint,
)
if (timedOn) {
SectionDivider()
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
}
}
}
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit))
@@ -136,7 +136,13 @@ fun GroupProfileLayout(
if (enabled) {
Text(
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable { saveProfile(GroupProfile(displayName.value, fullName.value, profileImage.value)) },
modifier = Modifier.clickable {
saveProfile(groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
))
},
color = MaterialTheme.colors.primary
)
} else {
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.model.*
@@ -14,14 +15,15 @@ import chat.simplex.app.model.*
fun CIChatFeatureView(
chatItem: ChatItem,
feature: Feature,
iconColor: Color
iconColor: Color,
icon: ImageVector? = null
) {
Row(
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(feature.iconFilled, feature.text, Modifier.size(15.dp), tint = iconColor)
Icon(icon ?: feature.iconFilled, feature.text, Modifier.size(18.dp), tint = iconColor)
Text(
chatEventText(chatItem),
Modifier,
@@ -32,7 +32,7 @@ fun CIEventView(ci: ChatItem) {
if (memberDisplayName != null) {
chatEventTextView(
buildAnnotatedString {
withChatEventStyle(this, memberDisplayName)
withStyle(chatEventStyle) { append(memberDisplayName) }
append(" ")
}.plus(chatEventText(ci))
)
@@ -43,15 +43,11 @@ fun CIEventView(ci: ChatItem) {
}
}
private fun withChatEventStyle(builder: AnnotatedString.Builder, text: String) {
return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) }
}
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)
fun chatEventText(ci: ChatItem): AnnotatedString =
buildAnnotatedString {
withChatEventStyle(this, ci.content.text)
append(" ")
withChatEventStyle(this, ci.timestampText)
withStyle(chatEventStyle) { append(ci.content.text + " " + ci.timestampText) }
}
@Preview(showBackground = true)
@@ -0,0 +1,54 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun CIFeaturePreferenceView(
chatItem: ChatItem,
contact: Contact?,
feature: ChatFeature,
allowed: FeatureAllowed,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
) {
Row(
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon(feature.icon, feature.text, Modifier.size(18.dp), tint = HighOrLowlight)
if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) {
val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
val setParam = feature == ChatFeature.TimedMessages && contact.mergedPreferences.timedMessages.userPreference.pref.ttl == null
val acceptTextId = if (setParam) R.string.accept_feature_set_1_day else R.string.accept_feature
val param = if (setParam) 86400 else null
val annotatedText = buildAnnotatedString {
withStyle(chatEventStyle) { append(chatItem.content.text + " ") }
withAnnotation(tag = "Accept", annotation = "Accept") {
withStyle(acceptStyle) { append(generalGetString(acceptTextId) + " ") }
}
withStyle(chatEventStyle) { append(chatItem.timestampText) }
}
fun accept(offset: Int): Boolean = annotatedText.getStringAnnotations(tag = "Accept", start = offset, end = offset).isNotEmpty()
ClickableText(
annotatedText,
onClick = { if (accept(it)) { acceptFeature(contact, feature, param) } },
shouldConsumeEvent = ::accept
)
} else {
Text(chatItem.content.text + " " + chatItem.timestampText,
fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)
}
}
}
@@ -174,14 +174,14 @@ fun CIFileView(
class ChatItemProvider: PreviewParameterProvider<ChatItem> {
private val sentFile = ChatItem(
chatDir = CIDirection.DirectSnd(),
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemDeleted = false, itemEdited = true, editable = false),
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemEdited = true),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCFile("")),
quotedItem = null,
file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete)
)
private val fileChatItemWtFile = ChatItem(
chatDir = CIDirection.DirectRcv(),
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), ),
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile("")),
quotedItem = null,
file = null
@@ -0,0 +1,46 @@
package chat.simplex.app.views.chat.item
import SectionSpacer
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable
fun CIInvalidJSONView(json: String) {
Row(Modifier
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
.padding(horizontal = 10.dp, vertical = 6.dp)
) {
Text(stringResource(R.string.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)
}
}
@Composable
fun InvalidJSONView(json: String) {
Column {
Spacer(Modifier.height(DEFAULT_PADDING))
SectionView {
val context = LocalContext.current
SettingsActionItem(Icons.Outlined.Share, generalGetString(R.string.share_verb), click = {
shareText(context, json)
})
}
Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) {
Text(json)
}
}
}
@@ -3,62 +3,85 @@ package chat.simplex.app.views.chat.item
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.Timer
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.WarningYellow
import kotlinx.datetime.Clock
@Composable
fun CIMetaView(chatItem: ChatItem, metaColor: Color = HighOrLowlight) {
Row(verticalAlignment = Alignment.CenterVertically) {
if (!chatItem.isDeletedContent) {
if (chatItem.meta.itemEdited) {
Icon(
Icons.Filled.Edit,
modifier = Modifier.height(12.dp).padding(end = 1.dp),
contentDescription = stringResource(R.string.icon_descr_edited),
tint = metaColor,
)
}
CIStatusView(chatItem.meta.itemStatus, metaColor)
fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = HighOrLowlight) {
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
if (chatItem.isDeletedContent) {
Text(
chatItem.timestampText,
color = metaColor,
fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp)
)
} else {
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor)
}
Text(
chatItem.timestampText,
color = metaColor,
fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp)
)
}
}
@Composable
// changing this function requires updating reserveSpaceForMeta
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
if (meta.itemEdited) {
StatusIconText(Icons.Outlined.Edit, color)
Spacer(Modifier.width(3.dp))
}
if (meta.disappearing) {
StatusIconText(Icons.Outlined.Timer, color)
val ttl = meta.itemTimed?.ttl
if (ttl != chatTTL) {
Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
}
Spacer(Modifier.width(4.dp))
}
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
if (statusIcon != null) {
val (icon, statusColor) = statusIcon
StatusIconText(icon, statusColor)
Spacer(Modifier.width(4.dp))
} else if (!meta.disappearing) {
StatusIconText(Icons.Filled.Circle, Color.Transparent)
Spacer(Modifier.width(4.dp))
}
Text(meta.timestampText, color = color, fontSize = 13.sp)
}
// the conditions in this function should match CIMetaText
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
val iconSpace = " "
var res = ""
if (meta.itemEdited) res += iconSpace
if (meta.itemTimed != null) {
res += iconSpace
val ttl = meta.itemTimed.ttl
if (ttl != chatTTL) {
res += TimedMessagesPreference.shortTtlText(ttl)
}
}
if (meta.statusIcon(HighOrLowlight) != null || !meta.disappearing) {
res += iconSpace
}
return res + meta.timestampText
}
@Composable
fun CIStatusView(status: CIStatus, metaColor: Color = HighOrLowlight) {
when (status) {
is CIStatus.SndSent -> {
Icon(Icons.Filled.Check, stringResource(R.string.icon_descr_sent_msg_status_sent), Modifier.height(12.dp), tint = metaColor)
}
is CIStatus.SndErrorAuth -> {
Icon(Icons.Filled.Close, stringResource(R.string.icon_descr_sent_msg_status_unauthorized_send), Modifier.height(12.dp), tint = Color.Red)
}
is CIStatus.SndError -> {
Icon(Icons.Filled.WarningAmber, stringResource(R.string.icon_descr_sent_msg_status_send_failed), Modifier.height(12.dp), tint = WarningYellow)
}
is CIStatus.RcvNew -> {
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = MaterialTheme.colors.primary)
}
else -> {}
}
private fun StatusIconText(icon: ImageVector, color: Color) {
Icon(icon, null, Modifier.height(12.dp), tint = color)
}
@Preview
@@ -67,7 +90,8 @@ fun PreviewCIMetaView() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
)
),
null
)
}
@@ -78,7 +102,8 @@ fun PreviewCIMetaViewUnread() {
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
status = CIStatus.RcvNew()
)
),
null
)
}
@@ -89,7 +114,8 @@ fun PreviewCIMetaViewSendFailed() {
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
status = CIStatus.SndError("CMD SYNTAX")
)
),
null
)
}
@@ -99,7 +125,8 @@ fun PreviewCIMetaViewSendNoAuth() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
)
),
null
)
}
@@ -109,7 +136,8 @@ fun PreviewCIMetaViewSendSent() {
CIMetaView(
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent()
)
),
null
)
}
@@ -120,7 +148,8 @@ fun PreviewCIMetaViewEdited() {
chatItem = ChatItem.getSampleData(
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
itemEdited = true
)
),
null
)
}
@@ -132,7 +161,8 @@ fun PreviewCIMetaViewEditedUnread() {
1, CIDirection.DirectRcv(), Clock.System.now(), "hello",
itemEdited = true,
status=CIStatus.RcvNew()
)
),
null
)
}
@@ -144,7 +174,8 @@ fun PreviewCIMetaViewEditedSent() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
itemEdited = true,
status=CIStatus.SndSent()
)
),
null
)
}
@@ -152,6 +183,7 @@ fun PreviewCIMetaViewEditedSent() {
@Composable
fun PreviewCIMetaViewDeletedContent() {
CIMetaView(
chatItem = ChatItem.getDeletedContentSampleData()
chatItem = ChatItem.getDeletedContentSampleData(),
null
)
}
@@ -34,6 +34,7 @@ fun CIVoiceView(
sent: Boolean,
hasText: Boolean,
ci: ChatItem,
timedMessagesTTL: Int?,
longClick: () -> Unit,
) {
Row(
@@ -63,7 +64,7 @@ fun CIVoiceView(
durationText(time / 1000)
}
}
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, play, pause, longClick)
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick)
} else {
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
val metaReserve = if (edited)
@@ -86,6 +87,7 @@ private fun VoiceLayout(
brokenAudio: Boolean,
sent: Boolean,
hasText: Boolean,
timedMessagesTTL: Int?,
play: () -> Unit,
pause: () -> Unit,
longClick: () -> Unit
@@ -105,7 +107,7 @@ private fun VoiceLayout(
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci)
CIMetaView(ci, timedMessagesTTL)
}
}
}
@@ -115,7 +117,7 @@ private fun VoiceLayout(
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci)
CIMetaView(ci, timedMessagesTTL)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState
@@ -42,6 +43,7 @@ fun ChatItemView(
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
@@ -49,9 +51,10 @@ fun ChatItemView(
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
val showMenu = remember { mutableStateOf(false) }
val revealed = remember { mutableStateOf(false) }
val fullDeleteAllowed = remember(cInfo) { cInfo.fullDeletionAllowed }
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
val onLinkLongClick = { _: String -> showMenu.value = true }
val live = composeState.value.liveMessage != null
Box(
modifier = Modifier
@@ -95,7 +98,7 @@ fun ChatItemView(
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
if (!cItem.meta.itemDeleted) {
if (cItem.meta.itemDeleted == null && !live) {
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
@@ -131,13 +134,13 @@ fun ChatItemView(
})
}
}
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice) {
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = {
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false
})
}
if (cItem.meta.itemDeleted && revealed.value) {
if (cItem.meta.itemDeleted != null && revealed.value) {
ItemAction(
stringResource(R.string.hide_verb),
Icons.Outlined.VisibilityOff,
@@ -147,7 +150,9 @@ fun ChatItemView(
}
)
}
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
}
@@ -173,15 +178,15 @@ fun ChatItemView(
@Composable
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted && !revealed.value) {
MarkedDeletedItemView(cItem, showMember = showMember)
if (cItem.meta.itemDeleted != null && !revealed.value) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
MarkedDeletedItemDropdownMenu()
} else if (cItem.quotedItem == null && !cItem.meta.itemDeleted) {
} else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem)
EmojiItemView(cItem, cInfo.timedMessagesTTL)
MsgContentItemDropdownMenu()
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, longClick = { onLinkLongClick("") })
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
MsgContentItemDropdownMenu()
} else {
framedItemView()
@@ -194,7 +199,7 @@ fun ChatItemView(
}
@Composable fun DeletedItem() {
DeletedItemView(cItem, showMember = showMember)
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
@@ -215,7 +220,7 @@ fun ChatItemView(
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
@@ -224,10 +229,18 @@ fun ChatItemView(
is CIContent.SndConnEventContent -> CIEventView(cItem)
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.RcvChatPreference -> {
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
}
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, HighOrLowlight, icon = c.feature.icon,)
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
is CIContent.SndModerated -> DeletedItem()
is CIContent.RcvModerated -> DeletedItem()
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
}
}
}
@@ -319,6 +332,7 @@ fun PreviewChatItemView() {
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> }
)
}
}
@@ -338,6 +352,7 @@ fun PreviewChatItemViewDeletedContent() {
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> }
)
}
}
@@ -1,8 +1,7 @@
package chat.simplex.app.views.chat.item
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
@@ -18,10 +17,11 @@ import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
val sent = ci.chatDir.sent
Surface(
shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight,
color = if (sent) SentColorLight else ReceivedColorLight,
) {
Row(
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
@@ -35,7 +35,7 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci)
CIMetaView(ci, timedMessagesTTL)
}
}
}
@@ -49,7 +49,8 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
fun PreviewDeletedItemView() {
SimpleXTheme {
DeletedItemView(
ChatItem.getDeletedContentSampleData()
ChatItem.getDeletedContentSampleData(),
null
)
}
}
@@ -15,13 +15,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp)
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp)
@Composable
fun EmojiItemView(chatItem: ChatItem) {
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) {
Column(
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
EmojiText(chatItem.content.text)
CIMetaView(chatItem)
CIMetaView(chatItem, timedMessagesTTL)
}
}
@@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
@@ -27,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)
@@ -47,6 +49,7 @@ fun FramedItemView(
scrollToItem: (Long) -> Unit = {},
) {
val sent = ci.chatDir.sent
val chatTTL = chatInfo.timedMessagesTTL
fun membership(): GroupMember? {
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
@@ -67,7 +70,7 @@ fun FramedItemView(
}
@Composable
fun ciDeletedView() {
fun FramedItemHeader(caption: String, italic: Boolean, icon: ImageVector? = null) {
Row(
Modifier
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight)
@@ -78,15 +81,19 @@ fun FramedItemView(
.padding(bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
Icons.Outlined.Delete,
stringResource(R.string.marked_deleted_description),
Modifier.size(18.dp),
tint = if (isInDarkTheme()) FileDark else FileLight
)
if (icon != null) {
Icon(
icon,
caption,
Modifier.size(18.dp),
tint = if (isInDarkTheme()) FileDark else FileLight
)
}
Text(
buildAnnotatedString {
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) }
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = HighOrLowlight)) {
append(caption)
}
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
)
@@ -138,12 +145,12 @@ fun FramedItemView(
@Composable
fun ciFileView(ci: ChatItem, text: String) {
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
if (text != "") {
CIMarkdownText(ci, showMember, linkMode = linkMode, uriHandler)
if (text != "" || ci.meta.isLive) {
CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler)
}
}
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && ci.content.text.isEmpty() && ci.quotedItem == null
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
Box(Modifier
.clip(RoundedCornerShape(18.dp))
@@ -158,9 +165,13 @@ fun FramedItemView(
Box(contentAlignment = Alignment.BottomEnd) {
Column(Modifier.width(IntrinsicSize.Max)) {
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
if (ci.meta.itemDeleted) { ciDeletedView() }
if (ci.meta.itemDeleted != null) {
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
} else if (ci.meta.isLive) {
FramedItemHeader(stringResource(R.string.live), false)
}
ci.quotedItem?.let { ciQuoteView(it) }
if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) {
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
Column(
Modifier
@@ -176,36 +187,36 @@ fun FramedItemView(
when (val mc = ci.content.msgContent) {
is MsgContent.MCImage -> {
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "") {
if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White
} else {
CIMarkdownText(ci, showMember, linkMode, uriHandler)
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}
}
is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, longClick = { onLinkLongClick("") })
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
if (mc.text != "") {
CIMarkdownText(ci, showMember, linkMode, uriHandler)
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}
}
is MsgContent.MCFile -> ciFileView(ci, mc.text)
is MsgContent.MCUnknown ->
if (ci.file == null) {
CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
} else {
ciFileView(ci, mc.text)
}
is MsgContent.MCLink -> {
ChatItemLinkView(mc.preview)
CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
}
else -> CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
}
}
}
}
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
CIMetaView(ci, metaColor)
CIMetaView(ci, chatTTL, metaColor)
}
}
}
@@ -214,21 +225,29 @@ fun FramedItemView(
@Composable
fun CIMarkdownText(
ci: ChatItem,
chatTTL: Int?,
showMember: Boolean,
linkMode: SimplexLinkMode,
uriHandler: UriHandler?,
onLinkLongClick: (link: String) -> Unit = {}
) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
MarkdownText(
ci.content.text, ci.formattedText, if (showMember) ci.memberDisplayName else null,
metaText = ci.timestampText, edited = ci.meta.itemEdited, linkMode = linkMode,
text, if (text.isEmpty()) emptyList() else ci.formattedText, if (showMember) ci.memberDisplayName else null,
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick
)
}
}
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
/**
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
* See [androidx.compose.ui.unit.Constraints.createConstraints]
* */
const val MAX_SAFE_WIDTH = 0x3FFFF - 1
@Composable
fun PriorityLayout(
@@ -236,6 +255,17 @@ fun PriorityLayout(
priorityLayoutId: String,
content: @Composable () -> Unit
) {
/**
* Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints]
* */
fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31
width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height
width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height
width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height
width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height
else -> 0x1FFF // shouldn't happen since width is limited already
}
Layout(
content = content,
modifier = modifier
@@ -247,9 +277,11 @@ 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, constraints.maxWidth))) }
// Limit width for every other element to width of important element and height for a sum of all elements.
val width = imagePlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width })
val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height })
layout(width, height) {
var y = 0
placeables.forEach {
it.place(0, y)
@@ -22,7 +22,7 @@ import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
Surface(
Modifier.clickable(onClick = {
AlertManager.shared.showAlertMsg(
@@ -45,7 +45,7 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci)
CIMetaView(ci, timedMessagesTTL)
}
}
}
@@ -59,7 +59,8 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
fun IntegrityErrorItemViewPreview() {
SimpleXTheme {
IntegrityErrorItemView(
ChatItem.getDeletedContentSampleData()
ChatItem.getDeletedContentSampleData(),
null
)
}
}
@@ -14,13 +14,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.CIDeleted
import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
Surface(
shape = RoundedCornerShape(18.dp),
color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight,
@@ -37,7 +38,7 @@ fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci)
CIMetaView(ci, timedMessagesTTL)
}
}
}
@@ -51,7 +52,8 @@ fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
fun PreviewMarkedDeletedItemView() {
SimpleXTheme {
DeletedItemView(
ChatItem.getSampleData(itemDeleted = true)
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()),
null
)
}
}
@@ -1,16 +1,20 @@
package chat.simplex.app.views.chat.item
import android.app.Activity
import android.content.ActivityNotFoundException
import android.util.Log
import androidx.annotation.IntRange
import androidx.compose.foundation.text.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
@@ -18,7 +22,9 @@ import androidx.compose.ui.unit.*
import androidx.core.text.BidiFormatter
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.helpers.detectGesture
import kotlinx.coroutines.*
val reserveTimestampStyle = SpanStyle(color = Color.Transparent)
val boldFont = SpanStyle(fontWeight = FontWeight.Medium)
@@ -40,13 +46,31 @@ fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolea
}
}
private val noTyping: AnnotatedString = AnnotatedString(" ")
private val typingIndicators: List<AnnotatedString> = listOf(
typing(FontWeight.Black) + typing() + typing(),
typing(FontWeight.Bold) + typing(FontWeight.Black) + typing(),
typing() + typing(FontWeight.Bold) + typing(FontWeight.Black),
typing() + typing() + typing(FontWeight.Bold)
)
private fun typingIndicator(recent: Boolean, @IntRange (from = 0, to = 4) typingIdx: Int): AnnotatedString = buildAnnotatedString {
pushStyle(SpanStyle(color = HighOrLowlight, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp))
append(if (recent) typingIndicators[typingIdx] else noTyping)
}
private fun typing(w: FontWeight = FontWeight.Light): AnnotatedString =
AnnotatedString(".", SpanStyle(fontWeight = w))
@Composable
fun MarkdownText (
text: String,
text: CharSequence,
formattedText: List<FormattedText>? = null,
sender: String? = null,
metaText: String? = null,
edited: Boolean = false,
meta: CIMeta? = null,
chatTTL: Int? = null,
style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp),
maxLines: Int = Int.MAX_VALUE,
overflow: TextOverflow = TextOverflow.Clip,
@@ -54,29 +78,69 @@ fun MarkdownText (
senderBold: Boolean = false,
modifier: Modifier = Modifier,
linkMode: SimplexLinkMode,
inlineContent: Map<String, InlineTextContent>? = null,
onLinkLongClick: (link: String) -> Unit = {}
) {
val textLayoutDirection = remember (text) {
if (BidiFormatter.getInstance().isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
}
val reserve = when {
textLayoutDirection != LocalLayoutDirection.current && metaText != null -> "\n"
edited -> " "
else -> " "
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
"\n"
} else if (meta != null) {
reserveSpaceForMeta(meta, chatTTL)
} else {
" "
}
val scope = rememberCoroutineScope()
CompositionLocalProvider(
LocalLayoutDirection provides if (textLayoutDirection != LocalLayoutDirection.current)
if (LocalLayoutDirection.current == LayoutDirection.Ltr) LayoutDirection.Rtl else LayoutDirection.Ltr
else
LocalLayoutDirection.current
) {
var timer: Job? by remember { mutableStateOf(null) }
var typingIdx by rememberSaveable { mutableStateOf(0) }
fun stopTyping() {
timer?.cancel()
timer = null
}
fun switchTyping() {
if (meta != null && meta.isLive && meta.recent) {
timer = timer ?: scope.launch {
while (isActive) {
typingIdx = (typingIdx + 1) % typingIndicators.size
delay(250)
}
}
} else {
stopTyping()
}
}
if (meta?.isLive == true) {
val activity = LocalContext.current as Activity
LaunchedEffect(meta.recent, meta.isLive) {
switchTyping()
}
DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation
onDispose {
if (orientation == activity.resources.configuration.orientation) {
stopTyping()
}
}
}
}
if (formattedText == null) {
val annotatedText = buildAnnotatedString {
appendSender(this, sender, senderBold)
append(text)
if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
if (text is String) append(text)
else if (text is AnnotatedString) append(text)
if (meta?.isLive == true) {
append(typingIndicator(meta.recent, typingIdx))
}
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
}
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent ?: mapOf())
} else {
var hasLinks = false
val annotatedText = buildAnnotatedString {
@@ -100,10 +164,13 @@ fun MarkdownText (
}
}
}
if (meta?.isLive == true) {
append(typingIndicator(meta.recent, typingIdx))
}
// With RTL language set globally links looks bad sometimes, better to add a new line to bo sure everything looks good
/*if (metaText != null && hasLinks && LocalLayoutDirection.current == LayoutDirection.Rtl)
withStyle(reserveTimestampStyle) { append("\n" + metaText) }
else */if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
}
if (hasLinks && uriHandler != null) {
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,
@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.annotatedStringResource
import chat.simplex.app.views.usersettings.MarkdownHelpView
import chat.simplex.app.views.usersettings.simplexTeamUri
val bold = SpanStyle(fontWeight = FontWeight.Bold)
@@ -76,6 +77,15 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Text(annotatedStringResource(R.string.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
Text(annotatedStringResource(R.string.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
}
Column(
Modifier.padding(vertical = 24.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2)
MarkdownHelpView()
}
}
}
@@ -11,15 +11,20 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.chat.group.deleteGroupDialog
import chat.simplex.app.views.chat.group.leaveGroupDialog
import chat.simplex.app.views.chat.item.InvalidJSONView
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.ContactConnectionInfoView
@@ -39,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,
@@ -75,6 +82,18 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
showMenu,
stopped
)
is ChatInfo.InvalidJSON ->
ChatListNavLinkLayout(
chatLinkPreview = {
InvalidDataView()
},
click = {
ModalManager.shared.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
},
dropdownMenuItems = null,
showMenu,
stopped
)
}
}
@@ -282,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
}
)
@@ -320,6 +339,29 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
)
}
@Composable
private fun InvalidDataView() {
Row {
ProfileImage(72.dp, null, Icons.Filled.AccountCircle, HighOrLowlight)
Column(
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1F)
) {
Text(
stringResource(R.string.invalid_data),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3,
fontWeight = FontWeight.Bold,
color = Color.Red
)
val height = with(LocalDensity.current) { 46.sp.toDp() }
Spacer(Modifier.height(height))
}
}
}
fun markChatRead(c: Chat, chatModel: ChatModel) {
var chat = c
withApi {
@@ -367,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)
}
@@ -585,8 +627,11 @@ fun PreviewChatListNavLinkDirect() {
),
chatStats = Chat.ChatStats()
),
null,
null,
false,
null,
null,
stopped = false,
linkMode = SimplexLinkMode.DESCRIPTION
)
@@ -623,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,41 +14,60 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.*
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.NewChatSheet
import chat.simplex.app.views.onboarding.WhatsNewView
import chat.simplex.app.views.onboarding.shouldShowWhatsNew
import chat.simplex.app.views.usersettings.SettingsView
import chat.simplex.app.views.usersettings.simplexTeamUri
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@Composable
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
val newChatSheetState by rememberSaveable(stateSaver = NewChatSheetState.saver()) { mutableStateOf(MutableStateFlow(NewChatSheetState.GONE)) }
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val showNewChatSheet = {
newChatSheetState.value = NewChatSheetState.VISIBLE
newChatSheetState.value = AnimatedViewState.VISIBLE
}
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
if (animated) newChatSheetState.value = NewChatSheetState.HIDING
else newChatSheetState.value = NewChatSheetState.GONE
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
else newChatSheetState.value = AnimatedViewState.GONE
}
LaunchedEffect(Unit) {
if (shouldShowWhatsNew(chatModel)) {
delay(1000L)
ModalManager.shared.showCustomModal { close -> WhatsNewView(close = close) }
}
}
LaunchedEffect(chatModel.clearOverlays.value) {
if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false)
}
LaunchedEffect(chatModel.appOpenUrl.value) {
val url = chatModel.appOpenUrl.value
if (url != null) {
chatModel.appOpenUrl.value = null
connectIfOpenedViaUri(url, chatModel)
}
}
var searchInList by rememberSaveable { mutableStateOf("") }
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val switchingUsers = rememberSaveable { mutableStateOf(false) }
Scaffold(
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, stopped) { searchInList = it.trim() } },
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
scaffoldState = scaffoldState,
drawerContent = { SettingsView(chatModel, setPerformLA) },
floatingActionButton = {
@@ -80,7 +100,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
) {
if (chatModel.chats.isNotEmpty()) {
ChatList(chatModel, search = searchInList)
} else {
} else if (!switchingUsers.value) {
Box(Modifier.fillMaxSize()) {
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
OnboardingButtons(showNewChatSheet)
@@ -94,6 +114,17 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
if (searchInList.isEmpty()) {
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
}
UserPicker(chatModel, userPickerState, switchingUsers) {
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
}
if (switchingUsers.value) {
Box(
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
contentAlignment = Alignment.Center
) {
ProgressIndicator()
}
}
}
@Composable
@@ -139,7 +170,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
}
@Composable
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
var showSearch by rememberSaveable { mutableStateOf(false) }
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
if (showSearch) {
@@ -172,10 +203,23 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
val scope = rememberCoroutineScope()
DefaultTopAppBar(
navigationButton = {
if (showSearch)
if (showSearch) {
NavigationButtonBack(hideSearchOnBack)
else
} else if (chatModel.users.isEmpty()) {
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
} else {
val users by remember { derivedStateOf { chatModel.users.toList() } }
val allRead = users
.filter { !it.user.activeUser }
.all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
if (users.size == 1) {
scope.launch { drawerState.open() }
} else {
userPickerState.value = AnimatedViewState.VISIBLE
}
}
}
},
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -202,6 +246,47 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
Divider(Modifier.padding(top = AppBarHeight))
}
@Composable
private fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
IconButton(onClick = onButtonClicked) {
Box {
ProfileImage(
image = image,
size = 37.dp
)
if (!allRead) {
unreadBadge()
}
}
}
}
@Composable
private fun BoxScope.unreadBadge(text: String? = "") {
Text(
text ?: "",
color = MaterialTheme.colors.onPrimary,
fontSize = 6.sp,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.badgeLayout()
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp)
.align(Alignment.TopEnd)
)
}
@Composable
private fun ProgressIndicator() {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
}
private var lazyListState = 0 to 0
@Composable
@@ -4,17 +4,20 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -22,11 +25,22 @@ import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.ComposePreview
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.views.helpers.*
@Composable
fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean, 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
@@ -63,11 +77,58 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
)
}
@Composable
fun VerifiedIcon() {
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight)
}
fun messageDraft(draft: ComposeState): Pair<AnnotatedString, Map<String, InlineTextContent>> {
fun attachment(): Pair<ImageVector, String?>? =
when (draft.preview) {
is ComposePreview.FilePreview -> Icons.Filled.InsertDriveFile to draft.preview.fileName
is ComposePreview.ImagePreview -> Icons.Outlined.Image to null
is ComposePreview.VoicePreview -> Icons.Filled.PlayArrow to durationText(draft.preview.durationMs / 1000)
else -> null
}
val attachment = attachment()
val text = buildAnnotatedString {
appendInlineContent(id = "editIcon")
append(" ")
if (attachment != null) {
appendInlineContent(id = "attachmentIcon")
if (attachment.second != null) {
append(attachment.second as String)
}
append(" ")
}
append(draft.message)
}
val inlineContent: Map<String, InlineTextContent> = mapOf(
"editIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(Icons.Outlined.EditNote, null, tint = MaterialTheme.colors.primary)
},
"attachmentIcon" to InlineTextContent(
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
) {
Icon(attachment?.first ?: Icons.Outlined.EditNote, null, tint = HighOrLowlight)
}
)
return text to inlineContent
}
@Composable
fun chatPreviewTitle() {
when (cInfo) {
is ChatInfo.Direct ->
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight)
Row(verticalAlignment = Alignment.CenterVertically) {
if (cInfo.contact.verified) {
VerifiedIcon()
}
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight)
}
is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary)
@@ -82,16 +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 == null -> 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 == null -> 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,
metaText = null,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
inlineContent = inlineTextContent,
modifier = Modifier.fillMaxWidth(),
)
} else {
@@ -179,7 +254,7 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
Modifier.padding(top = 52.dp),
contentAlignment = Alignment.Center
) {
ChatStatusImage(chat)
ChatStatusImage(contactNetworkStatus)
}
}
}
@@ -202,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,
@@ -213,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)
@@ -233,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)
}
}
@@ -29,7 +29,7 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
stopped
)
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection -> {}
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {}
}
}
@@ -0,0 +1,198 @@
package chat.simplex.app.views.chatlist
import SectionItemViewSpaceBetween
import android.util.Log
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.TAG
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, openSettings: () -> Unit) {
val scope = rememberCoroutineScope()
var newChat by remember { mutableStateOf(userPickerState.value) }
val users by remember { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } }
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
LaunchedEffect(Unit) {
launch {
userPickerState.collect {
newChat = it
launch {
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE
}
}
}
}
LaunchedEffect(Unit) {
snapshotFlow { newChat.isVisible() }
.distinctUntilChanged()
.filter { it }
.collect {
try {
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
var same = users.size == updatedUsers.size
if (same) {
for (i in 0 until minOf(users.size, updatedUsers.size)) {
val prev = updatedUsers[i].user
val next = users[i].user
if (prev.userId != next.userId || prev.activeUser != next.activeUser || prev.chatViewName != next.chatViewName || prev.image != next.image) {
same = false
break
}
}
}
if (!same) {
chatModel.users.clear()
chatModel.users.addAll(updatedUsers)
}
} catch (e: Exception) {
Log.e(TAG, "Error updating users ${e.stackTraceToString()}")
}
}
}
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density }
Box(Modifier
.fillMaxSize()
.offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else xOffset, 0) }
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
.padding(bottom = 10.dp, top = 10.dp)
.graphicsLayer {
alpha = animatedFloat.value
translationY = (animatedFloat.value - 1) * xOffset
}
) {
Column(
Modifier
.widthIn(min = 220.dp)
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
) {
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
openSettings()
userPickerState.value = AnimatedViewState.GONE
}) {
userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) {
chatModel.chats.clear()
scope.launch {
val job = launch {
delay(500)
switchingUsers.value = true
}
chatModel.controller.changeActiveUser(u.user.userId)
job.cancel()
switchingUsers.value = false
}
}
}
Divider(Modifier.requiredHeight(1.dp))
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
}
}
SettingsPickerItem {
openSettings()
userPickerState.value = AnimatedViewState.GONE
}
}
}
}
@Composable
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = 46.dp)
.combinedClickable(
onClick = if (u.activeUser) openSettings else onClick,
onLongClick = onLongClick,
interactionSource = remember { MutableInteractionSource() },
indication = if (!u.activeUser) LocalIndication.current else null
)
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
ProfileImage(
image = u.image,
size = 54.dp
)
Text(
u.displayName,
modifier = Modifier
.padding(start = 8.dp, end = 8.dp),
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
if (u.activeUser) {
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
} else if (unreadCount > 0) {
Row {
Text(
unreadCountStr(unreadCount),
color = MaterialTheme.colors.onPrimary,
fontSize = 11.sp,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp),
textAlign = TextAlign.Center,
maxLines = 1
)
Spacer(Modifier.width(2.dp))
}
} else {
Box(Modifier.size(20.dp))
}
}
}
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
Text(
text,
color = MaterialTheme.colors.onBackground,
)
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
}
}
@@ -27,6 +27,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
@@ -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()
}
}
}
)
}
}
@@ -30,7 +36,7 @@ fun CloseSheetBar(close: () -> Unit) {
@Composable
fun AppBarTitle(title: String, withPadding: Boolean = true) {
val padding = if (withPadding)
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING )
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
else
PaddingValues(bottom = DEFAULT_PADDING)
Text(
@@ -53,6 +53,15 @@ fun NavigationButtonBack(onButtonClicked: () -> Unit) {
}
}
@Composable
fun ShareButton(onButtonClicked: () -> Unit) {
IconButton(onButtonClicked) {
Icon(
Icons.Outlined.Share, stringResource(R.string.share_verb), tint = MaterialTheme.colors.primary
)
}
}
@Composable
fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
IconButton(onClick = onButtonClicked) {
@@ -1,9 +1,13 @@
@file:UseSerializers(UriSerializer::class)
package chat.simplex.app.views.helpers
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.runtime.saveable.Saver
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
sealed class SharedContent {
data class Text(val text: String): SharedContent()
@@ -11,7 +15,7 @@ sealed class SharedContent {
data class File(val text: String, val uri: Uri): SharedContent()
}
enum class NewChatSheetState {
enum class AnimatedViewState {
VISIBLE, HIDING, GONE;
fun isVisible(): Boolean {
return this == VISIBLE
@@ -23,7 +27,7 @@ enum class NewChatSheetState {
return this == GONE
}
companion object {
fun saver(): Saver<MutableStateFlow<NewChatSheetState>, *> = Saver(
fun saver(): Saver<MutableStateFlow<AnimatedViewState>, *> = Saver(
save = { it.value.toString() },
restore = {
MutableStateFlow(valueOf(it))
@@ -32,7 +36,16 @@ enum class NewChatSheetState {
}
}
sealed class UploadContent {
data class SimpleImage(val uri: Uri): UploadContent()
data class AnimatedImage(val uri: Uri): UploadContent()
@Serializer(forClass = Uri::class)
object UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uri) = encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString())
}
@Serializable
sealed class UploadContent {
@Serializable data class SimpleImage(val uri: Uri): UploadContent()
@Serializable data class AnimatedImage(val uri: Uri): UploadContent()
}
@@ -214,8 +214,8 @@ fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit)
}
@Composable
fun interactionSourceWithTapDetection(key: Any, onPress: () -> Unit, onClick: () -> Unit, onCancel: () -> Unit, onRelease: ()-> Unit): MutableInteractionSource {
val interactionSource = remember(key) { MutableInteractionSource() }
fun interactionSourceWithTapDetection(onPress: () -> Unit, onClick: () -> Unit, onCancel: () -> Unit, onRelease: ()-> Unit): MutableInteractionSource {
val interactionSource = remember { MutableInteractionSource() }
LaunchedEffect(interactionSource) {
var firstTapTime = 0L
interactionSource.interactions.collect { interaction ->
@@ -6,6 +6,7 @@ import android.content.*
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.graphics.*
import android.graphics.ImageDecoder.DecodeException
import android.net.Uri
import android.provider.MediaStore
import android.util.Base64
@@ -205,21 +206,22 @@ fun GetImageBottomSheet(
val processPickedImage = { uri: Uri? ->
if (uri != null) {
val source = ImageDecoder.createSource(context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
imageBitmap.value = uri
onImageChange(bitmap)
}
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it) }
val galleryLauncherFallback = rememberGetContentLauncher { processPickedImage(it) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
if (uri != null) {
imageBitmap.value = uri
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
onImageChange(bitmap)
try {
val bitmap = ImageDecoder.decodeBitmap(source)
imageBitmap.value = uri
onImageChange(bitmap)
} catch (e: DecodeException) {
Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.image_decoding_exception_title),
text = generalGetString(R.string.image_decoding_exception_desc)
)
}
}
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage)
val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage)
val cameraLauncher = rememberCameraLauncher(processPickedImage)
val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
if (isGranted) {
cameraLauncher.launchWithFallback()
@@ -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,8 +1,10 @@
package chat.simplex.app.views.helpers
import android.app.Application
import android.content.Context
import android.media.*
import android.media.AudioManager.AudioPlaybackCallback
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
import android.os.Build
import android.util.Log
@@ -10,25 +12,25 @@ import androidx.compose.runtime.*
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatItem
import chat.simplex.app.views.helpers.AudioPlayer.duration
import kotlinx.coroutines.*
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
interface Recorder {
val recordingInProgress: MutableState<Boolean>
fun start(onStop: () -> Unit): String
fun stop()
fun cancel(filePath: String, recordingInProgress: MutableState<Boolean>)
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
fun stop(): Int
}
class RecorderNative(private val recordedBytesLimit: Long): Recorder {
companion object {
// Allows to stop the recorder from outside without having the recorder in a variable
var stopRecording: (() -> Unit)? = null
const val extension = "m4a"
}
override val recordingInProgress = mutableStateOf(false)
private var recorder: MediaRecorder? = null
private var progressJob: Job? = null
private var filePath: String? = null
private var recStartedAt: Long? = null
private fun initRecorder() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(SimplexApp.context)
@@ -36,9 +38,8 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
MediaRecorder()
}
override fun start(onStop: () -> Unit): String {
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
AudioPlayer.stop()
recordingInProgress.value = true
val rec: MediaRecorder
recorder = initRecorder().also { rec = it }
rec.setAudioSource(MediaRecorder.AudioSource.MIC)
@@ -47,28 +48,39 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
rec.setAudioChannels(1)
rec.setAudioSamplingRate(16000)
rec.setAudioEncodingBitRate(16000)
rec.setMaxDuration(-1) // TODO set limit
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
rec.setMaxFileSize(recordedBytesLimit)
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val filePath = getAppFilePath(SimplexApp.context, uniqueCombine(SimplexApp.context, getAppFilePath(SimplexApp.context, "voice_${timestamp}.m4a")))
rec.setOutputFile(filePath)
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
val fileToSave = File.createTempFile(generateNewFileName(SimplexApp.context, "voice", "${extension}_"), ".tmp", tmpDir)
fileToSave.deleteOnExit()
val path = fileToSave.absolutePath
filePath = path
rec.setOutputFile(path)
rec.prepare()
rec.start()
rec.setOnInfoListener { mr, what, extra ->
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
stop()
onStop()
recStartedAt = System.currentTimeMillis()
progressJob = CoroutineScope(Dispatchers.Default).launch {
while(isActive) {
onProgressUpdate(progress(), false)
delay(50)
}
}.apply {
invokeOnCompletion {
onProgressUpdate(realDuration(path), true)
}
}
stopRecording = { stop(); onStop() }
return filePath
rec.setOnInfoListener { _, what, _ ->
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED || what == MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
stop()
}
}
stopRecording = { stop() }
return path
}
override fun stop() {
if (!recordingInProgress.value) return
override fun stop(): Int {
val path = filePath ?: return 0
stopRecording = null
recordingInProgress.value = false
recorder?.metrics?.
runCatching {
recorder?.stop()
}
@@ -76,16 +88,25 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
recorder?.reset()
}
runCatching {
// release all resources
recorder?.release()
}
// Await coroutine finishes in order to send real duration to it's listener
runBlocking {
progressJob?.cancelAndJoin()
}
progressJob = null
filePath = null
recorder = null
return (realDuration(path) ?: 0).also { recStartedAt = null }
}
override fun cancel(filePath: String, recordingInProgress: MutableState<Boolean>) {
stop()
runCatching { File(filePath).delete() }.getOrElse { Log.d(TAG, "Unable to delete a file: ${it.stackTraceToString()}") }
}
private fun progress(): Int? = recStartedAt?.let { (System.currentTimeMillis() - it).toInt() }
/**
* Return real duration from [AudioPlayer] if it's possible (should always be possible).
* As a fallback, return internally counted duration
* */
private fun realDuration(path: String): Int? = duration(path) ?: progress()
}
object AudioPlayer {
@@ -251,8 +272,8 @@ object AudioPlayer {
audioPlaying.value = false
}
fun duration(filePath: String): Int {
var res = 0
fun duration(filePath: String): Int? {
var res: Int? = null
kotlin.runCatching {
helperPlayer.setDataSource(filePath)
helperPlayer.prepare()
@@ -1,4 +1,5 @@
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
@@ -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),
@@ -28,6 +28,22 @@ fun SimpleButton(text: String, icon: ImageVector,
}
}
@Composable
fun SimpleButton(
text: String, icon: ImageVector,
color: Color = MaterialTheme.colors.primary,
disabled: Boolean,
click: () -> Unit
) {
SimpleButtonFrame(click, disabled = disabled) {
Icon(
icon, text, tint = if (disabled) HighOrLowlight else color,
modifier = Modifier.padding(end = 8.dp)
)
Text(text, style = MaterialTheme.typography.caption, color = if (disabled) HighOrLowlight else color)
}
}
@Composable
fun SimpleButtonIconEnded(
text: String,
@@ -1,5 +1,6 @@
package chat.simplex.app.views.helpers
import android.app.Application
import android.content.Context
import android.content.res.Resources
import android.graphics.*
@@ -228,7 +229,7 @@ const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
const val MAX_VOICE_MILLIS_FOR_SENDING: Long = 43_000 // approximately is ok
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
const val MAX_FILE_SIZE: Long = 8000000
@@ -244,6 +245,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 +337,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 +360,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 ->
@@ -371,6 +375,24 @@ fun saveAnimImage(context: Context, uri: Uri): String? {
}
}
fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? {
return try {
val ext = if (asPng) "png" else "jpg"
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
return File(tmpDir.absolutePath + File.separator + generateNewFileName(SimplexApp.context, "IMG", ext)).apply {
outputStream().use { out ->
image.compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out)
out.flush()
}
deleteOnExit()
SimplexApp.context.chatModel.filesToDelete.add(this)
}
} catch (e: Exception) {
Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}")
null
}
}
fun saveFileFromUri(context: Context, uri: Uri): String? {
return try {
val inputStream = context.contentResolver.openInputStream(uri)
@@ -390,15 +412,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 = {},
@@ -1,32 +1,100 @@
package chat.simplex.app.views.newchat
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import boofcv.alg.fiducial.qrcode.QrCodeEncoder
import boofcv.alg.fiducial.qrcode.QrCodeGeneratorImage
import androidx.core.graphics.*
import androidx.core.graphics.drawable.toBitmap
import boofcv.alg.drawing.FiducialImageEngine
import boofcv.alg.fiducial.qrcode.*
import boofcv.android.ConvertBitmap
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.launch
@Composable
fun QRCode(connReq: String, modifier: Modifier = Modifier) {
Image(
bitmap = qrCodeBitmap(connReq, 1024).asImageBitmap(),
contentDescription = stringResource(R.string.image_descr_qr_code),
modifier = modifier
)
fun QRCode(
connReq: String,
modifier: Modifier = Modifier,
tintColor: Color = Color(0xff062d56),
withLogo: Boolean = true
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
BoxWithConstraints {
val maxWidthInPx = with(LocalDensity.current) { maxWidth.roundToPx() }
val qr = remember(maxWidthInPx, connReq, tintColor, withLogo) {
qrCodeBitmap(connReq, maxWidthInPx).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo() else it }
.asImageBitmap()
}
Image(
bitmap = qr,
contentDescription = stringResource(R.string.image_descr_qr_code),
modifier
.clickable {
scope.launch {
val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
.let { if (withLogo) it.addLogo() else it }
val file = saveTempImageUncompressed(image, false)
if (file != null) {
shareFile(context, "", file.absolutePath)
}
}
}
)
}
}
fun qrCodeBitmap(content: String, size: Int): Bitmap {
val qrCode = QrCodeEncoder().addAutomatic(content).fixate()
val renderer = QrCodeGeneratorImage(5)
fun qrCodeBitmap(content: String, size: Int = 1024): Bitmap {
val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate()
/** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */
val numModules = QrCode.totalModules(qrCode.version)
val borderModule = 1
// val calculatedFinalWidth = (pixelsPerModule * numModules) + 2 * (borderModule * pixelsPerModule)
// size = (x * numModules) + 2 * (borderModule * x)
// size / x = numModules + 2 * borderModule
// x = size / (numModules + 2 * borderModule)
val pixelsPerModule = size / (numModules + 2 * borderModule)
// + 1 to make it with better quality
val renderer = QrCodeGeneratorImage(pixelsPerModule + 1)
renderer.borderModule = borderModule
renderer.render(qrCode)
return ConvertBitmap.grayToBitmap(renderer.gray, Bitmap.Config.RGB_565)
return ConvertBitmap.grayToBitmap(renderer.gray, Bitmap.Config.RGB_565).scale(size, size)
}
fun Bitmap.replaceColor(from: Int, to: Int): Bitmap {
val pixels = IntArray(width * height)
getPixels(pixels, 0, width, 0, 0, width, height)
var i = 0
while (i < pixels.size) {
if (pixels[i] == from) {
pixels[i] = to
}
i++
}
setPixels(pixels, 0, width, 0, 0, width, height)
return this
}
fun Bitmap.addLogo(): Bitmap = applyCanvas {
val radius = (width * 0.16f) / 2
val paint = android.graphics.Paint()
paint.color = android.graphics.Color.WHITE
drawCircle(width / 2f, height / 2f, radius, paint)
val logo = SimplexApp.context.resources.getDrawable(R.mipmap.icon_foreground, null).toBitmap()
val logoSize = (width * 0.24).toInt()
translate((width - logoSize) / 2f, (height - logoSize) / 2f)
drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null)
}
@Preview
@@ -16,11 +16,12 @@ import kotlinx.coroutines.launch
enum class OnboardingStage {
Step1_SimpleXInfo,
Step2_CreateProfile,
Step3_SetNotificationsMode,
OnboardingComplete
}
@Composable
fun CreateProfile(chatModel: ChatModel) {
fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
@@ -33,7 +34,10 @@ fun CreateProfile(chatModel: ChatModel) {
.background(color = MaterialTheme.colors.background)
.padding(20.dp)
) {
CreateProfilePanel(chatModel)
CreateProfilePanel(chatModel, close)
LaunchedEffect(Unit) {
setLastVersionDefault(chatModel)
}
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
@@ -0,0 +1,68 @@
package chat.simplex.app.views.onboarding
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.NotificationsMode
import chat.simplex.app.views.usersettings.changeNotificationsMode
@Composable
fun SetNotificationsMode(m: ChatModel) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(20.dp)
) {
AppBarTitle(stringResource(R.string.onboarding_notifications_mode_title), false)
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
Text(stringResource(R.string.onboarding_notifications_mode_subtitle))
Spacer(Modifier.padding(DEFAULT_PADDING_HALF))
NotificationButton(currentMode, NotificationsMode.OFF, R.string.onboarding_notifications_mode_off, R.string.onboarding_notifications_mode_off_desc)
NotificationButton(currentMode, NotificationsMode.PERIODIC, R.string.onboarding_notifications_mode_periodic, R.string.onboarding_notifications_mode_periodic_desc)
NotificationButton(currentMode, NotificationsMode.SERVICE, R.string.onboarding_notifications_mode_service, R.string.onboarding_notifications_mode_service_desc)
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage) {
changeNotificationsMode(currentMode.value, m)
}
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}
}
@Composable
private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mode: NotificationsMode, @StringRes title: Int, @StringRes description: Int) {
TextButton(
onClick = { currentMode.value = mode },
border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight.copy(alpha = 0.5f)),
shape = RoundedCornerShape(15.dp),
) {
Column(Modifier.padding(bottom = 6.dp).padding(horizontal = 8.dp)) {
Text(
stringResource(title),
style = MaterialTheme.typography.h2,
fontWeight = FontWeight.Medium,
color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier.padding(bottom = 4.dp)
)
Text(annotatedStringResource(description), color = MaterialTheme.colors.onBackground, lineHeight = 24.sp)
}
}
Spacer(Modifier.height(DEFAULT_PADDING))
}
@@ -25,7 +25,6 @@ import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ModalManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
@@ -105,14 +104,14 @@ private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: I
@Composable
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
if (user == null) {
ActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
} else {
ActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
}
}
@Composable
private fun ActionButton(
fun OnboardingActionButton(
@StringRes labelId: Int,
onboarding: OnboardingStage?,
onboardingStage: MutableState<OnboardingStage?>,
@@ -0,0 +1,301 @@
package chat.simplex.app.views.onboarding
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) }
@Composable
fun featureDescription(icon: ImageVector, titleId: Int, descrId: Int, link: String?) {
@Composable
fun linkButton(link: String) {
val uriHandler = LocalUriHandler.current
Icon(
Icons.Outlined.OpenInNew, stringResource(titleId), tint = MaterialTheme.colors.primary,
modifier = Modifier
.clickable { uriHandler.openUri(link) }
)
}
Column(
horizontalAlignment = Alignment.Start
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(icon, stringResource(titleId), tint = HighOrLowlight)
Text(
generalGetString(titleId),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h3,
fontWeight = FontWeight.Medium
)
if (link != null) {
linkButton(link)
}
}
Text(generalGetString(descrId))
}
}
@Composable
fun pagination() {
Row(
Modifier
.padding(bottom = 16.dp)
) {
if (currentVersion.value > 0) {
val prev = currentVersion.value - 1
Surface(shape = RoundedCornerShape(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.clickable { currentVersion.value = prev }
.padding(8.dp)
) {
Icon(Icons.Outlined.ArrowBackIosNew, "previous", tint = MaterialTheme.colors.primary)
Text(versionDescriptions[prev].version, color = MaterialTheme.colors.primary)
}
}
}
Spacer(Modifier.fillMaxWidth().weight(1f))
if (currentVersion.value < versionDescriptions.lastIndex) {
val next = currentVersion.value + 1
Surface(shape = RoundedCornerShape(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.clickable { currentVersion.value = next }
.padding(8.dp)
) {
Text(versionDescriptions[next].version, color = MaterialTheme.colors.primary)
Icon(Icons.Outlined.ArrowForwardIos, "next", tint = MaterialTheme.colors.primary)
}
}
}
}
}
val v = versionDescriptions[currentVersion.value]
ModalView(close = close) {
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
String.format(generalGetString(R.string.new_in_version), v.version),
Modifier
.fillMaxWidth()
.padding(DEFAULT_PADDING),
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1,
fontWeight = FontWeight.Normal,
color = HighOrLowlight
)
v.features.forEach { feature ->
featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link)
}
if (!viaSettings) {
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(
Modifier.fillMaxWidth(), contentAlignment = Alignment.Center
) {
Text(
generalGetString(R.string.ok),
modifier = Modifier.clickable(onClick = close),
style = MaterialTheme.typography.h3,
color = MaterialTheme.colors.primary
)
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}
Spacer(Modifier.fillMaxHeight().weight(1f))
pagination()
}
}
}
private data class FeatureDescription(
val icon: ImageVector,
val titleId: Int,
val descrId: Int,
val link: String? = null
)
private data class VersionDescription(
val version: String,
val features: List<FeatureDescription>
)
private val versionDescriptions: List<VersionDescription> = listOf(
VersionDescription(
version = "v4.2",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.VerifiedUser,
titleId = R.string.v4_2_security_assessment,
descrId = R.string.v4_2_security_assessment_desc,
link = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"
),
FeatureDescription(
icon = Icons.Outlined.Group,
titleId = R.string.v4_2_group_links,
descrId = R.string.v4_2_group_links_desc
),
FeatureDescription(
icon = Icons.Outlined.Check,
titleId = R.string.v4_2_auto_accept_contact_requests,
descrId = R.string.v4_2_auto_accept_contact_requests_desc
),
)
),
VersionDescription(
version = "v4.3",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.Mic,
titleId = R.string.v4_3_voice_messages,
descrId = R.string.v4_3_voice_messages_desc
),
FeatureDescription(
icon = Icons.Outlined.DeleteForever,
titleId = R.string.v4_3_irreversible_message_deletion,
descrId = R.string.v4_3_irreversible_message_deletion_desc
),
FeatureDescription(
icon = Icons.Outlined.WifiTethering,
titleId = R.string.v4_3_improved_server_configuration,
descrId = R.string.v4_3_improved_server_configuration_desc
),
FeatureDescription(
icon = Icons.Outlined.VisibilityOff,
titleId = R.string.v4_3_improved_privacy_and_security,
descrId = R.string.v4_3_improved_privacy_and_security_desc
),
)
),
VersionDescription(
version = "v4.4",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.Timer,
titleId = R.string.v4_4_disappearing_messages,
descrId = R.string.v4_4_disappearing_messages_desc
),
FeatureDescription(
icon = Icons.Outlined.Pending,
titleId = R.string.v4_4_live_messages,
descrId = R.string.v4_4_live_messages_desc
),
FeatureDescription(
icon = Icons.Outlined.VerifiedUser,
titleId = R.string.v4_4_verify_connection_security,
descrId = R.string.v4_4_verify_connection_security_desc
),
FeatureDescription(
icon = Icons.Outlined.Translate,
titleId = R.string.v4_4_french_interface,
descrId = R.string.v4_4_french_interface_descr
)
)
),
VersionDescription(
version = "v4.5",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.ManageAccounts,
titleId = R.string.v4_5_multiple_chat_profiles,
descrId = R.string.v4_5_multiple_chat_profiles_descr
),
FeatureDescription(
icon = Icons.Outlined.EditNote,
titleId = R.string.v4_5_message_draft,
descrId = R.string.v4_5_message_draft_descr
),
FeatureDescription(
icon = Icons.Outlined.SafetyDivider,
titleId = R.string.v4_5_transport_isolation,
descrId = R.string.v4_5_transport_isolation_descr,
link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation"
),
FeatureDescription(
icon = Icons.Outlined.Task,
titleId = R.string.v4_5_private_filenames,
descrId = R.string.v4_5_private_filenames_descr
),
FeatureDescription(
icon = Icons.Outlined.Battery2Bar,
titleId = R.string.v4_5_reduced_battery_usage,
descrId = R.string.v4_5_reduced_battery_usage_descr
),
FeatureDescription(
icon = Icons.Outlined.Translate,
titleId = R.string.v4_5_italian_interface,
descrId = R.string.v4_5_italian_interface_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
)
)
private val lastVersion = versionDescriptions.last().version
fun setLastVersionDefault(m: ChatModel) {
m.controller.appPrefs.whatsNewVersion.set(lastVersion)
}
fun shouldShowWhatsNew(m: ChatModel): Boolean {
val v = m.controller.appPrefs.whatsNewVersion.get()
setLastVersionDefault(m)
return v != lastVersion
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewWhatsNewView() {
SimpleXTheme {
WhatsNewView(
viaSettings = true,
close = {}
)
}
}
@@ -33,6 +33,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) }
val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) }
val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) }
val networkTCPKeepIdle: MutableState<Int>
val networkTCPKeepIntvl: MutableState<Int>
@@ -48,10 +49,6 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
}
fun buildCfg(): NetCfg {
val socksProxy = currentCfg.value.socksProxy
val tcpConnectTimeout = networkTCPConnectTimeout.value
val tcpTimeout = networkTCPTimeout.value
val smpPingInterval = networkSMPPingInterval.value
val enableKeepAlive = networkEnableKeepAlive.value
val tcpKeepAlive = if (enableKeepAlive) {
val keepIdle = networkTCPKeepIdle.value
@@ -62,11 +59,15 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
null
}
return NetCfg(
socksProxy = socksProxy,
tcpConnectTimeout = tcpConnectTimeout,
tcpTimeout = tcpTimeout,
socksProxy = currentCfg.value.socksProxy,
hostMode = currentCfg.value.hostMode,
requiredHostMode = currentCfg.value.requiredHostMode,
sessionMode = currentCfg.value.sessionMode,
tcpConnectTimeout = networkTCPConnectTimeout.value,
tcpTimeout = networkTCPTimeout.value,
tcpKeepAlive = tcpKeepAlive,
smpPingInterval = smpPingInterval
smpPingInterval = networkSMPPingInterval.value,
smpPingCount = networkSMPPingCount.value
)
}
@@ -74,6 +75,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPConnectTimeout.value = cfg.tcpConnectTimeout
networkTCPTimeout.value = cfg.tcpTimeout
networkSMPPingInterval.value = cfg.smpPingInterval
networkSMPPingCount.value = cfg.smpPingCount
networkEnableKeepAlive.value = cfg.enableKeepAlive
if (cfg.tcpKeepAlive != null) {
networkTCPKeepIdle.value = cfg.tcpKeepAlive.keepIdle
@@ -113,6 +115,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPConnectTimeout,
networkTCPTimeout,
networkSMPPingInterval,
networkSMPPingCount,
networkEnableKeepAlive,
networkTCPKeepIdle,
networkTCPKeepIntvl,
@@ -129,6 +132,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPConnectTimeout: MutableState<Long>,
networkTCPTimeout: MutableState<Long>,
networkSMPPingInterval: MutableState<Long>,
networkSMPPingCount: MutableState<Int>,
networkEnableKeepAlive: MutableState<Boolean>,
networkTCPKeepIdle: MutableState<Int>,
networkTCPKeepIntvl: MutableState<Int>,
@@ -170,7 +174,14 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
SectionItemView {
TimeoutSettingRow(
stringResource(R.string.network_option_ping_interval), networkSMPPingInterval,
listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000), secondsLabel
listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000), secondsLabel
)
}
SectionDivider()
SectionItemView {
IntSettingRow(
stringResource(R.string.network_option_ping_count), networkSMPPingCount,
listOf(1, 2, 3, 5, 8), ""
)
}
SectionDivider()
@@ -412,6 +423,7 @@ fun PreviewAdvancedNetworkSettingsLayout() {
networkTCPConnectTimeout = remember { mutableStateOf(10_000000) },
networkTCPTimeout = remember { mutableStateOf(10_000000) },
networkSMPPingInterval = remember { mutableStateOf(10_000000) },
networkSMPPingCount = remember { mutableStateOf(3) },
networkEnableKeepAlive = remember { mutableStateOf(true) },
networkTCPKeepIdle = remember { mutableStateOf(10) },
networkTCPKeepIntvl = remember { mutableStateOf(10) },
@@ -24,8 +24,6 @@ import androidx.compose.ui.graphics.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
@@ -65,11 +63,6 @@ fun AppearanceView() {
AppearanceLayout(
appIcon,
changeIcon = ::setAppIcon,
showThemeSelector = {
ModalManager.shared.showModal(true) {
ThemeSelectorView()
}
},
editPrimaryColor = { primary ->
ModalManager.shared.showModalCloseable { close ->
ColorEditor(primary, close)
@@ -81,7 +74,6 @@ fun AppearanceView() {
@Composable fun AppearanceLayout(
icon: MutableState<AppIcon>,
changeIcon: (AppIcon) -> Unit,
showThemeSelector: () -> Unit,
editPrimaryColor: (Color) -> Unit,
) {
Column(
@@ -115,8 +107,12 @@ fun AppearanceView() {
SectionSpacer()
val currentTheme by CurrentColors.collectAsState()
SectionView(stringResource(R.string.settings_section_title_themes)) {
SectionItemViewSpaceBetween(showThemeSelector) {
Text(generalGetString(R.string.theme))
SectionItemViewSpaceBetween {
val darkTheme = isSystemInDarkTheme()
val state = remember { derivedStateOf { currentTheme.second } }
ThemeSelector(state) {
ThemeManager.applyTheme(it.name, darkTheme)
}
}
SectionDivider()
SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }) {
@@ -183,6 +179,21 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
)
}
@Composable
private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme) -> Unit) {
val darkTheme = isSystemInDarkTheme()
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second to it.third }) }
ExposedDropDownSettingRow(
generalGetString(R.string.theme),
values,
state,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
SimplexApp.context.packageManager.getComponentEnabledSetting(
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
@@ -196,7 +207,6 @@ fun PreviewAppearanceSettings() {
AppearanceLayout(
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
changeIcon = {},
showThemeSelector = {},
editPrimaryColor = {},
)
}
@@ -9,6 +9,7 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -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,
@@ -17,18 +17,13 @@ import chat.simplex.app.model.Format
import chat.simplex.app.model.FormatColor
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.AppBarTitle
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun MarkdownHelpView() {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.how_to_use_markdown), false)
Text(stringResource(R.string.you_can_use_markdown_to_format_messages__prompt))
Spacer(Modifier.height(DEFAULT_PADDING))
val bold = stringResource(R.string.bold)
@@ -31,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 = {},
)
}
}
@@ -46,25 +46,6 @@ enum class NotificationPreviewMode {
fun NotificationsSettingsView(
chatModel: ChatModel,
) {
val onNotificationsModeSelected = { mode: NotificationsMode ->
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
}
chatModel.notificationsMode.value = mode
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE)
SimplexService.start(SimplexApp.context)
else
SimplexService.safeStopService(SimplexApp.context)
}
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
}
val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode ->
chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
chatModel.notificationPreviewMode.value = mode
@@ -76,7 +57,7 @@ fun NotificationsSettingsView(
showPage = { page ->
ModalManager.shared.showModalCloseable(true) {
when (page) {
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode, onNotificationsModeSelected)
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode) { changeNotificationsMode(it, chatModel) }
CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)
}
}
@@ -159,7 +140,7 @@ fun NotificationPreviewView(
}
// mode, name, description
fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
val res = ArrayList<ValueTitleDesc<NotificationsMode>>()
res.add(
ValueTitleDesc(
@@ -211,3 +192,23 @@ fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> {
)
return res
}
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
}
chatModel.notificationsMode.value = mode
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
CoroutineScope(Dispatchers.Default).launch {
if (mode == NotificationsMode.SERVICE)
SimplexService.start(SimplexApp.context)
else
SimplexService.safeStopService(SimplexApp.context)
}
if (mode != NotificationsMode.PERIODIC) {
MessagesFetcherWorker.cancelAll()
}
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
}
@@ -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()
}
@@ -69,14 +65,19 @@ private fun PreferencesLayout(
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.your_preferences))
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) }
TimedMessagesFeatureSection(timedMessages) {
applyPrefs(preferences.copy(timedMessages = TimedMessagesPreference(allow = if (it) FeatureAllowed.YES else FeatureAllowed.NO)))
}
SectionSpacer()
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) }
FeatureSection(ChatFeature.FullDelete, allowFullDeletion) {
applyPrefs(preferences.copy(fullDelete = ChatPreference(allow = it)))
applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it)))
}
SectionSpacer()
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) }
FeatureSection(ChatFeature.Voice, allowVoice) {
applyPrefs(preferences.copy(voice = ChatPreference(allow = it)))
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
}
SectionSpacer()
ResetSaveButtons(
@@ -103,6 +104,22 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllo
SectionTextFooter(feature.allowDescription(allowFeature.value))
}
@Composable
private fun TimedMessagesFeatureSection(allowFeature: State<FeatureAllowed>, onSelected: (Boolean) -> Unit) {
SectionView {
SectionItemView {
PreferenceToggleWithIcon(
ChatFeature.TimedMessages.text,
ChatFeature.TimedMessages.icon,
HighOrLowlight,
allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES,
onSelected
)
}
}
SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value))
}
@Composable
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
SectionView {
@@ -51,8 +51,6 @@ fun PrivacySettingsView(
SectionView(stringResource(R.string.settings_section_title_chats)) {
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.ImageAspectRatio, stringResource(R.string.transfer_images_faster), chatModel.controller.appPrefs.privacyTransferImagesInline)
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SectionDivider()
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
@@ -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.*
@@ -34,6 +35,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.CreateLinkTab
import chat.simplex.app.views.newchat.CreateLinkView
import chat.simplex.app.views.onboarding.SimpleXInfo
import chat.simplex.app.views.onboarding.WhatsNewView
@Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
@@ -43,6 +45,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
MaintainIncognitoState(chatModel)
if (user != null) {
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state }
val context = LocalContext.current
SettingsLayout(
profile = user.profile,
stopped,
@@ -55,8 +59,43 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } },
// showVideoChatPrototype = { ModalManager.shared.showCustomModal { close -> CallViewDebug(close) } },
showVersion = {
withApi {
val info = chatModel.controller.apiGetVersion()
if (info != null) {
ModalManager.shared.showModal { VersionInfoView(info) }
}
}
},
withAuth = { block ->
if (!requireAuth.value) {
block()
} else {
ModalManager.shared.showModalCloseable { close ->
val onFinishAuth = { success: Boolean ->
if (success) {
close()
block()
}
}
LaunchedEffect(Unit) {
runAuth(context, onFinishAuth)
}
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(R.string.auth_unlock),
icon = Icons.Outlined.Lock,
click = {
runAuth(context, onFinishAuth)
}
)
}
}
}
},
)
}
}
@@ -64,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,
@@ -87,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())) {
@@ -112,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()
@@ -138,9 +169,9 @@ fun SettingsLayout(
SectionView(stringResource(R.string.settings_section_title_help)) {
SettingsActionItem(Icons.Outlined.HelpOutline, stringResource(R.string.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
SettingsActionItem(Icons.Outlined.Add, stringResource(R.string.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.TextFormat, stringResource(R.string.markdown_in_messages), showModal { MarkdownHelpView() })
SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
SectionDivider()
SettingsActionItem(Icons.Outlined.Tag, stringResource(R.string.chat_with_the_founder), { uriHandler.openUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
SectionDivider()
@@ -162,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)
}
}
}
@@ -239,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
)
}
@@ -344,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})")
}
}
@@ -407,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))
@@ -468,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,
@@ -488,9 +531,9 @@ fun PreviewSettingsLayout() {
setPerformLA = {},
showModal = { {} },
showSettingsModal = { {} },
showCustomModal = { {}},
showTerminal = {},
// showVideoChatPrototype = {}
showCustomModal = { {} },
showVersion = {},
withAuth = {},
)
}
}
@@ -1,44 +0,0 @@
package chat.simplex.app.views.usersettings
import SectionViewSelectable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun ThemeSelectorView() {
val darkTheme = isSystemInDarkTheme()
val allThemes by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { ValueTitleDesc(it.second, it.third, "") }) }
ThemeSelectorLayout(
allThemes,
onSelectTheme = {
ThemeManager.applyTheme(it.name, darkTheme)
},
)
}
@Composable
private fun ThemeSelectorLayout(
allThemes: List<ValueTitleDesc<DefaultTheme>>,
onSelectTheme: (DefaultTheme) -> Unit,
) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.settings_section_title_themes).lowercase().capitalize(Locale.current))
val currentTheme by CurrentColors.collectAsState()
val state = remember { derivedStateOf { currentTheme.second } }
SectionViewSelectable(null, state, allThemes, onSelectTheme)
}
}
@@ -44,12 +44,9 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
close,
saveProfile = { displayName, fullName, image ->
withApi {
val p = Profile(displayName, fullName, image)
val newProfile = chatModel.controller.apiUpdateProfile(p)
val newProfile = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image))
if (newProfile != null) {
chatModel.currentUser.value?.profile?.profileId?.let {
chatModel.updateUserProfile(newProfile.toLocalProfile(it))
}
chatModel.updateCurrentUser(newProfile)
profile = newProfile
}
editProfile.value = false
@@ -97,7 +94,7 @@ fun UserProfileLayout(
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
AppBarTitle(stringResource(R.string.your_chat_profile), false)
AppBarTitle(stringResource(R.string.your_current_profile), false)
Text(
stringResource(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it),
Modifier.padding(bottom = 24.dp),
@@ -0,0 +1,141 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.chatlist.UserProfilePickerItem
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.CreateProfile
@Composable
fun UserProfilesView(m: ChatModel) {
val users by remember { derivedStateOf { m.users.map { it.user } } }
UserProfilesView(
users = users,
addUser = {
ModalManager.shared.showModalCloseable { close ->
CreateProfile(m, close)
}
},
activateUser = { user ->
withBGApi {
m.controller.changeActiveUser(user.userId)
}
},
removeUser = { user ->
val text = buildAnnotatedString {
append(generalGetString(R.string.users_delete_all_chats_deleted) + "\n\n" + generalGetString(R.string.users_delete_profile_for) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(user.displayName)
}
append(":")
}
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.users_delete_question),
text = text,
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, true)
}) {
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, false)
}
) {
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
}
}
}
)
}
)
}
@Composable
private fun UserProfilesView(
users: List<User>,
addUser: () -> Unit,
activateUser: (User) -> Unit,
removeUser: (User) -> Unit,
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(R.string.your_chat_profiles))
SectionView {
for (user in users) {
UserView(user, users, activateUser, removeUser)
SectionDivider()
}
SectionItemView(addUser, minHeight = 68.dp) {
Icon(Icons.Outlined.Add, stringResource(R.string.users_add), tint = MaterialTheme.colors.primary)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.users_add), color = MaterialTheme.colors.primary)
}
}
SectionTextFooter(stringResource(R.string.your_chat_profiles_stored_locally))
}
}
@Composable
private fun UserView(user: User, users: List<User>, activateUser: (User) -> Unit, removeUser: (User) -> Unit) {
var showDropdownMenu by remember { mutableStateOf(false) }
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
activateUser(user)
}
Box(Modifier.padding(horizontal = 16.dp)) {
DropdownMenu(
expanded = showDropdownMenu,
onDismissRequest = { showDropdownMenu = false },
Modifier.width(220.dp)
) {
ItemAction(stringResource(R.string.delete_verb), Icons.Outlined.Delete, color = Color.Red, onClick = {
removeUser(user)
showDropdownMenu = false
}
)
}
}
}
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean) {
if (users.size < 2) return
withBGApi {
try {
if (user.activeUser) {
val newActive = users.first { !it.activeUser }
m.controller.changeActiveUser_(newActive.userId)
}
m.controller.apiDeleteUser(user.userId, delSMPQueues)
m.users.removeAll { it.user.userId == user.userId }
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
}
}
}
@@ -0,0 +1,30 @@
package chat.simplex.app.views.usersettings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.BuildConfig
import chat.simplex.app.R
import chat.simplex.app.model.CoreVersionInfo
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.helpers.AppBarTitle
@Composable
fun VersionInfoView(info: CoreVersionInfo) {
Column(
Modifier.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
AppBarTitle(stringResource(R.string.app_version_title), false)
Text(String.format(stringResource(R.string.app_version_name), BuildConfig.VERSION_NAME))
Text(String.format(stringResource(R.string.app_version_code), BuildConfig.VERSION_CODE))
Text(String.format(stringResource(R.string.core_version), info.version))
Text(String.format(stringResource(R.string.core_build_timestamp), info.buildTimestamp))
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
}
}
@@ -0,0 +1,953 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="allow_voice_messages_only_if">Povolte hlasové zprávy, pouze pokud je váš kontakt povolí.</string>
<string name="allow_to_send_disappearing">Povolit odesílání mizejících zpráv.</string>
<string name="allow_to_send_voice">Povolit odesílání hlasových zpráv.</string>
<string name="v4_2_group_links_desc">Správci mohou vytvářet odkazy pro připojení ke skupinám.</string>
<string name="accept_contact_button">Přijmout</string>
<string name="smp_servers_preset_add">Přidejte přednastavené servery</string>
<string name="network_settings">Pokročilá nastavení sítě</string>
<string name="accept">Přijmout</string>
<string name="smp_servers_add">Přidat server…</string>
<string name="network_enable_socks_info">Přistupovat k serverům přes SOCKS proxy na portu 9050\? Před povolením této možnosti musí být spuštěna proxy.</string>
<string name="accept_feature">Přijmout</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Umožněte svým kontaktům odesílat mizející zprávy.</string>
<string name="about_simplex_chat">O <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="smp_servers_add_to_another_device">Přidat do jiného zařízení</string>
<string name="accept_requests">Přijímat žádosti</string>
<string name="allow_verb">Povolit</string>
<string name="allow_voice_messages_question">Povolit hlasové zprávy\?</string>
<string name="about_simplex">O SimpleX</string>
<string name="a_plus_b">a + b</string>
<string name="accept_call_on_lock_screen">Přijmout</string>
<string name="chat_item_ttl_day">1 den</string>
<string name="group_member_role_admin">správce</string>
<string name="users_add">Přidat profil</string>
<string name="users_delete_all_chats_deleted">Všechny chaty a zprávy budou smazány tuto akci nelze vrátit zpět!</string>
<string name="allow_disappearing_messages_only_if">Povolte mizející zprávy, pouze pokud to váš kontakt povolí.</string>
<string name="v4_3_improved_server_configuration_desc">Přidejte servery skenováním QR kódů.</string>
<string name="chat_item_ttl_month">1 měsíc</string>
<string name="chat_item_ttl_week">1 týden</string>
<string name="callstatus_accepted">přijatý hovor</string>
<string name="accept_contact_incognito_button">Přijmout inkognito</string>
<string name="accept_connection_request__question">Přijmout žádost o připojení\?</string>
<string name="all_group_members_will_remain_connected">Všichni členové skupiny zůstanou připojeni.</string>
<string name="allow_irreversible_message_deletion_only_if">Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí.</string>
<string name="allow_direct_messages">Povolit odesílání přímých zpráv členům.</string>
<string name="allow_to_delete_messages">Povolit nevratné smazání odeslaných zpráv.</string>
<string name="clear_chat_warning">Všechny zprávy budou smazány tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás.</string>
<string name="allow_your_contacts_irreversibly_delete">Umožněte svým kontaktům nevratně odstranit odeslané zprávy.</string>
<string name="allow_your_contacts_to_send_voice_messages">Povolte svým kontaktům odesílání hlasových zpráv.</string>
<string name="button_create_group_link">Vytvořit odkaz</string>
<string name="delete_link_question">Smazat odkaz\?</string>
<string name="button_send_direct_message">Odeslat přímou zprávu</string>
<string name="member_info_section_title_member">ČLEN</string>
<string name="change_member_role_question">Změnit roli ve skupině\?</string>
<string name="info_row_connection">Připojení</string>
<string name="conn_level_desc_indirect">nepřímé (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="conn_stats_section_title_servers">SERVERY</string>
<string name="receiving_via">Příjem prostřednictvím</string>
<string name="create_secret_group_title">Vytvoření tajné skupiny</string>
<string name="group_display_name_field">Zobrazení názvu skupiny:</string>
<string name="group_full_name_field">Úplný název skupiny:</string>
<string name="group_main_profile_sent">Váš profil v chatu bude zaslán členům skupiny</string>
<string name="group_profile_is_stored_on_members_devices">Profil skupiny je uložen v zařízeních členů, nikoli na serverech.</string>
<string name="network_options_save">Uložit</string>
<string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string>
<string name="incognito">Inkognito</string>
<string name="incognito_random_profile">Váš náhodný profil</string>
<string name="incognito_random_profile_description">Vašemu kontaktu bude zaslán náhodný profil</string>
<string name="save_color">Uložit barvu</string>
<string name="reset_color">Obnovení barev</string>
<string name="color_primary">Akcent</string>
<string name="chat_preferences_you_allow">Povolíte</string>
<string name="chat_preferences_default">výchozí (%s)</string>
<string name="chat_preferences_yes">ano</string>
<string name="chat_preferences_no">ne</string>
<string name="chat_preferences_always">vždy</string>
<string name="set_group_preferences">Nastavení skupinových předvoleb</string>
<string name="your_preferences">Vaše preference</string>
<string name="timed_messages">Zmizení zpráv</string>
<string name="feature_enabled_for_contact">povoleno pro kontakt</string>
<string name="feature_received_prohibited">přijaté, zakázané</string>
<string name="both_you_and_your_contact_can_send_disappearing">Vy i váš kontakt můžete posílat mizející zprávy.</string>
<string name="only_your_contact_can_send_disappearing">Zmizelé zprávy může odesílat pouze váš kontakt.</string>
<string name="only_you_can_delete_messages">Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání).</string>
<string name="message_deletion_prohibited">Nevratné mazání zpráv je v tomto chatu zakázáno.</string>
<string name="prohibit_direct_messages">Zakázat odesílání přímých zpráv členům.</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_s">%ds</string>
<string name="ttl_min">%d min</string>
<string name="ttl_hour">\"%d hodina</string>
<string name="feature_offered_item_with_param">offered %s: %2s</string>
<string name="v4_2_group_links">Odkazy na skupiny</string>
<string name="v4_3_voice_messages">Hlasové zprávy</string>
<string name="v4_3_irreversible_message_deletion_desc">Vaše kontakty mohou povolit úplné vymazání zpráv.</string>
<string name="v4_4_disappearing_messages">Zmizení zpráv</string>
<string name="v4_4_verify_connection_security_desc">Porovnejte bezpečnostní kódy se svými kontakty.</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="thousand_abbreviation">k</string>
<string name="connect_via_contact_link">Připojit se přes kontaktní odkaz\?</string>
<string name="connect_via_invitation_link">Připojit se přes pozvánku\?</string>
<string name="connect_via_group_link">Připojit se přes odkaz skupiny\?</string>
<string name="profile_will_be_sent_to_contact_sending_link">Váš profil bude odeslán kontaktu, od kterého jste obdrželi tento odkaz.</string>
<string name="server_connected">připojeno</string>
<string name="server_error">chyba</string>
<string name="server_connecting">připojení</string>
<string name="trying_to_connect_to_server_to_receive_messages">Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.</string>
<string name="deleted_description">Smazáno</string>
<string name="invalid_chat">neplatný chat</string>
<string name="invalid_data">neplatné údaje</string>
<string name="connection_local_display_name">spojení <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="display_name_connection_established">spojení navázáno</string>
<string name="display_name_invited_to_connect">pozvánka k připojení</string>
<string name="display_name_connecting">připojení…</string>
<string name="description_you_shared_one_time_link">jste sdíleli jednorázové spojení</string>
<string name="description_you_shared_one_time_link_incognito">sdíleli jste jednorázový odkaz inkognito</string>
<string name="description_via_group_link">prostřednictvím skupinového odkazu</string>
<string name="description_via_contact_address_link">prostřednictvím odkazu na kontaktní adresu</string>
<string name="description_via_contact_address_link_incognito">inkognito přes odkaz na kontaktní adresu</string>
<string name="description_via_one_time_link">prostřednictvím jednorázového odkazu</string>
<string name="description_via_one_time_link_incognito">inkognito přes jednorázový odkaz</string>
<string name="simplex_link_contact">SimpleX kontaktní adresa</string>
<string name="simplex_link_invitation">Jednorázová pozvánka SimpleX</string>
<string name="simplex_link_group">Skupinový odkaz SimpleX</string>
<string name="simplex_link_connection">prostřednictvím <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode">Odkazy na SimpleX</string>
<string name="simplex_link_mode_description">Popis</string>
<string name="simplex_link_mode_full">Úplný odkaz</string>
<string name="simplex_link_mode_browser">Prostřednictvím prohlížeče</string>
<string name="simplex_link_mode_browser_warning">Otevření odkazu v prohlížeči může snížit soukromí a bezpečnost připojení. Nedůvěryhodné odkazy SimpleX budou červené.</string>
<string name="error_saving_smp_servers">Chyba při ukládání serverů SMP</string>
<string name="error_setting_network_config">Chyba při aktualizaci konfigurace sítě</string>
<string name="failed_to_parse_chat_title">Nepodařilo se načíst chat</string>
<string name="failed_to_parse_chats_title">Nepodařilo se načíst chaty</string>
<string name="contact_developers">Aktualizujte aplikaci a kontaktujte vývojáře.</string>
<string name="connection_timeout">Časový limit připojení</string>
<string name="connection_error">Chyba připojení</string>
<string name="network_error_desc">Zkontrolujte prosím své síťové připojení pomocí <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> a zkuste to znovu.</string>
<string name="error_sending_message">Chyba při odesílání zprávy</string>
<string name="error_adding_members">Chyba při přidávání prutu(ů)</string>
<string name="contact_already_exists">Kontakt již existuje</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jste již připojeni k <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="invalid_connection_link">Neplatný odkaz na spojení</string>
<string name="error_accepting_contact_request">Chyba příjmu požadavku od kontaktu</string>
<string name="error_changing_address">Chuba změny adresy</string>
<string name="settings_notifications_mode_title">Služba oznamování</string>
<string name="notifications_mode_service_desc">Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
<string name="notification_preview_mode_message">Text zprávy</string>
<string name="notification_preview_mode_contact">Jméno kontaktu</string>
<string name="notification_preview_mode_hidden">Skryté</string>
<string name="notification_preview_mode_message_desc">Zobrazit kontakt a zprávu</string>
<string name="notification_contact_connected">Připojeno</string>
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
<string name="auth_log_in_using_credential">Přihlaste se pomocí svého pověření</string>
<string name="auth_enable_simplex_lock">Zapnutí zámku SimpleX</string>
<string name="reply_verb">Odpovězte na</string>
<string name="share_verb">Sdílet</string>
<string name="copy_verb">Kopírovat</string>
<string name="icon_descr_received_msg_status_unread">nepřečteno</string>
<string name="personal_welcome">Vítejte <xliff:g>%1$s</xliff:g>!</string>
<string name="welcome">Vítejte!</string>
<string name="this_text_is_available_in_settings">Tento text je k dispozici v nastavení</string>
<string name="icon_descr_sent_msg_status_send_failed">odeslání se nezdařilo</string>
<string name="share_file">Sdílet soubor…</string>
<string name="attach">Připojit</string>
<string name="icon_descr_context">Kontextová ikona</string>
<string name="image_decoding_exception_desc">Obrázek nelze dekódovat. Zkuste prosím použít jiný obrázek nebo kontaktujte vývojáře.</string>
<string name="image_descr">Obrázek</string>
<string name="icon_descr_waiting_for_image">Čekání na obrázek</string>
<string name="icon_descr_asked_to_receive">Požádáno o přijetí obrázku</string>
<string name="icon_descr_image_snd_complete">Obrázek odeslán</string>
<string name="waiting_for_image">Čekáme na obrázek</string>
<string name="image_will_be_received_when_contact_is_online">Obrázek bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string>
<string name="contact_sent_large_file">Váš kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
<string name="maximum_supported_file_size">V současné době je maximální podporovaná velikost souboru <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="error_saving_file">Chyba při ukládání souboru</string>
<string name="voice_message">Hlasová zpráva</string>
<string name="voice_message_send_text">Hlasová zpráva…</string>
<string name="icon_descr_server_status_connected">Připojeno</string>
<string name="icon_descr_server_status_disconnected">Odpojeno</string>
<string name="icon_descr_server_status_error">Chyba</string>
<string name="switch_receiving_address_desc">Tato funkce je experimentální! Bude fungovat pouze v případě, že druhý klient má nainstalovanou verzi 4.2. Po dokončení změny adresy by se měla v konverzaci zobrazit zpráva - zkontrolujte, zda můžete od tohoto kontaktu (nebo člena skupiny) stále přijímat zprávy.</string>
<string name="switch_receiving_address_question">Přepnout přijímací adresu\?</string>
<string name="send_verb">Poslat</string>
<string name="you_need_to_allow_to_send_voice">Abyste mohli odesílat hlasové zprávy, musíte je povolit svému kontaktu.</string>
<string name="icon_descr_cancel_live_message">Zrušit živou zprávu</string>
<string name="back">Zpět</string>
<string name="cancel_verb">Zrušit</string>
<string name="reset_verb">Obnovit</string>
<string name="ok">OK</string>
<string name="no_details">bez podrobností</string>
<string name="add_contact">Jednorázový zvací odkaz</string>
<string name="copied">Zkopírováno do schránky</string>
<string name="add_contact_or_create_group">Začít novou konverzaci</string>
<string name="create_group">Vytvořit tajnou skupinu</string>
<string name="to_share_with_your_contact">(sdílet s kontaktem)</string>
<string name="only_stored_on_members_devices">(uloženo pouze členy skupiny)</string>
<string name="toast_permission_denied">Oprávnění zamítnuto!</string>
<string name="use_camera_button">Použít fotoaparát</string>
<string name="from_gallery_button">Z Galerie</string>
<string name="choose_file">Vybrat soubor</string>
<string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string>
<string name="chat_help_tap_button">Klepněte na tlačítko</string>
<string name="above_then_preposition_continuation">nad, potom:</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Přidat nový kontakt</b>: vytvořit jednorázový kód QR.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Skenovat QR kód</b>: připojení ke kontaktu, který vám ukáže QR kód.</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scan displayed QR code from the app, via <b>Scan QR code</b>.</string>
<string name="clear_chat_question">Vyčistit konverzaci\?</string>
<string name="clear_verb">Čistý</string>
<string name="mark_read">Označit přečtení</string>
<string name="mark_unread">Označit jako nepřečtené</string>
<string name="mute_chat">Ztlumit</string>
<string name="unmute_chat">Zrušit ztlumení</string>
<string name="you_invited_your_contact">Pozvali jste svůj kontakt</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Kontakt, se kterým jste tento odkaz sdíleli, se NEBUDE moci připojit!</string>
<string name="connection_you_accepted_will_be_cancelled">Připojení, které jste přijali, bude zrušeno!</string>
<string name="icon_descr_help">help</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> Tým</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> Adresa</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Budete připojeni, jakmile bude vaše žádost o připojení přijata, vyčkejte prosím nebo se podívejte později!</string>
<string name="connection_request_sent">Požadavek na připojení byl odeslán!</string>
<string name="your_profile_will_be_sent">Váš profil v chatu bude odeslán vašemu kontaktu</string>
<string name="create_one_time_link">Vytvořit jednorázovou pozvánku</string>
<string name="one_time_link">Vytvořit jednorázový zvací odkaz</string>
<string name="security_code">Bezpečnostní kód</string>
<string name="is_verified">\"%s je ověřeno</string>
<string name="chat_console">Konzola pro chat</string>
<string name="smp_servers">SMP servery</string>
<string name="smp_servers_preset_address">Přednastavená adresa serveru</string>
<string name="smp_servers_test_failed">Test serveru se nezdařil!</string>
<string name="smp_servers_test_some_failed">Některé servery neprošly testem:</string>
<string name="smp_servers_scan_qr">Naskenujte QR kód serveru</string>
<string name="smp_servers_enter_manually">Zadejte server ručně</string>
<string name="smp_servers_invalid_address">Neplatná adresa serveru!</string>
<string name="smp_servers_check_address">Zkontrolujte adresu serveru a zkuste to znovu.</string>
<string name="smp_servers_delete_server">Smazat server</string>
<string name="contribute">Přispějte na</string>
<string name="how_to">Jak na to</string>
<string name="how_to_use_your_servers">Jak používat servery</string>
<string name="your_ICE_servers">Vaše servery ICE</string>
<string name="configure_ICE_servers">Konfigurace serverů ICE</string>
<string name="network_settings_title">Nastavení sítě</string>
<string name="network_enable_socks">Použít proxy server SOCKS\?</string>
<string name="network_disable_socks">Použít přímé připojení k internetu\?</string>
<string name="network_use_onion_hosts_no">Ne</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hostitelé nebudou použiti.</string>
<string name="network_session_mode_user">Profil chatu</string>
<string name="network_session_mode_entity">Připojení</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="create_address">Vytvořit adresu</string>
<string name="accept_automatically">Automaticky</string>
<string name="section_title_welcome_message">UVÍTACÍ ZPRÁVA</string>
<string name="save_and_notify_group_members">Uložit a upozornit členy skupiny</string>
<string name="exit_without_saving">Ukončit bez uložení</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platforma pro zasílání zpráv a aplikace chránící vaše soukromí a bezpečnost.</string>
<string name="create_profile">Vytvoření profilu</string>
<string name="profile_is_only_shared_with_your_contacts">Profil je sdílen pouze s vašimi kontakty.</string>
<string name="display_name_cannot_contain_whitespace">Zobrazované jméno nesmí obsahovat bílé znaky.</string>
<string name="bold">tučně</string>
<string name="callstatus_in_progress">probíhající hovor</string>
<string name="decentralized">Decentralizované</string>
<string name="how_it_works">Jak to funguje</string>
<string name="how_simplex_works">Jak funguje <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odesílané pomocí <b>2vrstvého end-to-end šifrování</b>.</string>
<string name="onboarding_notifications_mode_title">Soukromá oznámení</string>
<string name="onboarding_notifications_mode_periodic">Pravidelný</string>
<string name="ignore">Ignorovat</string>
<string name="call_already_ended">Hovor již skončil!</string>
<string name="icon_descr_video_call">videohovor</string>
<string name="icon_descr_audio_call">audio hovor</string>
<string name="settings_audio_video_calls">Audio a video hovory</string>
<string name="call_on_lock_screen">Hovory na uzamčené obrazovce:</string>
<string name="open_simplex_chat_to_accept_call">Otevřete <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro přijetí hovoru</string>
<string name="allow_accepting_calls_from_lock_screen">Povolte volání ze zamčené obrazovky prostřednictvím Nastavení.</string>
<string name="open_verb">Otevřete stránku</string>
<string name="icon_descr_audio_on">Zvuk zapnut</string>
<string name="icon_descr_speaker_off">Reproduktor vypnut</string>
<string name="icon_descr_speaker_on">Zapnutý reproduktor</string>
<string name="icon_descr_call_progress">Probíhající hovor</string>
<string name="auto_accept_images">Automatické přijímání obrázků</string>
<string name="settings_section_title_settings">NASTAVENÍ</string>
<string name="settings_section_title_help">NÁPOVĚDA</string>
<string name="settings_section_title_device">ZAŘÍZENÍ</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="settings_experimental_features">Experimentální funkce</string>
<string name="settings_section_title_socks">SOCKS PROXY</string>
<string name="settings_section_title_icon">IKONA APLIKACE</string>
<string name="settings_section_title_themes">TÉMATA</string>
<string name="settings_section_title_messages">ZPRÁVY</string>
<string name="settings_section_title_calls">VOLÁNÍ</string>
<string name="export_database">Export databáze</string>
<string name="import_database">Import databáze</string>
<string name="delete_database">Odstranění databáze</string>
<string name="error_exporting_chat_database">Chyba při exportu databáze chatu</string>
<string name="import_database_confirmation">Import</string>
<string name="restart_the_app_to_use_imported_chat_database">Restartujte aplikaci, abyste mohli používat importovanou databázi chatu.</string>
<string name="delete_chat_profile_question">Smazat profil chatu\?</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Restartujte aplikaci a vytvořte nový profil chatu.</string>
<string name="you_must_use_the_most_recent_version_of_database">Nejnovější verzi databáze chatu musíte používat POUZE v jednom zařízení, jinak se může stát, že přestanete přijímat zprávy od některých kontaktů.</string>
<string name="stop_chat_to_enable_database_actions">Zastavte chat a povolte akce s databází.</string>
<string name="files_and_media_section">Soubory a média</string>
<string name="delete_files_and_media_question">Smazat soubory a média\?</string>
<string name="delete_messages">Odstranění zpráv</string>
<string name="remove_passphrase_from_keychain">Odstranit přístupovou frázi z úložiště klíčů\?</string>
<string name="notifications_will_be_hidden">Oznámení budou doručována pouze do doby, než se aplikace zastaví!</string>
<string name="remove_passphrase">Odstranit</string>
<string name="update_database">Aktualizovat</string>
<string name="current_passphrase">Aktuální přístupová fráze…</string>
<string name="update_database_passphrase">Aktualizovat přístupovou frázi databáze</string>
<string name="enter_correct_current_passphrase">Zadejte prosím správnou aktuální přístupovou frázi.</string>
<string name="database_is_not_encrypted">Váš databáze konverzace není zašifrována - nastavte přístupovou frázi pro její ochranu.</string>
<string name="keychain_is_storing_securely">K bezpečnému uložení heslové fráze slouží úložiště klíčů Android - umožňuje fungování služby oznámení.</string>
<string name="impossible_to_recover_passphrase"><b>Upozornění</b>: pokud přístupovou frázi ztratíte, NEBUDE možné ji obnovit ani změnit.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">Databáze bude zašifrována a přístupová fráze bude uložena v úložišti klíčů.</string>
<string name="store_passphrase_securely">Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit.</string>
<string name="file_with_path">Soubor: %s</string>
<string name="database_passphrase_is_required">Pro otevření chatu je vyžadována přístupová fráze databáze.</string>
<string name="unknown_error">Neznámá chyba</string>
<string name="open_chat">Otevřete chat</string>
<string name="restore_database">Obnovte zálohu databáze</string>
<string name="restore_database_alert_desc">Po obnovení zálohy databáze zadejte předchozí heslo. Tuto akci nelze vrátit zpět.</string>
<string name="chat_is_stopped_indication">Chat je zastaven</string>
<string name="chat_archive_header">Chat se archivuje</string>
<string name="delete_chat_archive_question">Smazat archiv chatu\?</string>
<string name="join_group_question">Připojit se ke skupině\?</string>
<string name="join_group_button">Připojte se na</string>
<string name="leave_group_button">Opustit</string>
<string name="icon_descr_add_members">Pozvat členy</string>
<string name="alert_title_no_group">Skupina nebyla nalezena!</string>
<string name="alert_title_cant_invite_contacts">Nelze pozvat kontakty!</string>
<string name="snd_group_event_changed_member_role">změnili jste roli %s na %s</string>
<string name="snd_group_event_changed_role_for_yourself">změnili jste svou roli na %s</string>
<string name="snd_group_event_member_deleted">odstranili jste <xliff:g id="profil člena" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_user_left">odešli jste</string>
<string name="rcv_conn_event_switch_queue_phase_completed">změnila se vaše adresa</string>
<string name="icon_descr_expand_role">Rozšířit výběr rolí</string>
<string name="invite_prohibited">Nelze pozvat kontakt!</string>
<string name="failed_to_create_user_duplicate_desc">Již máte profil chatu se stejným zobrazovacím názvem. Zvolte prosím jiné jméno.</string>
<string name="smp_server_test_create_queue">Vytvořit frontu</string>
<string name="smp_server_test_secure_queue">Zabezpečit frontu</string>
<string name="service_notifications">Okamžitá oznámení!</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>V nastavení ji lze vypnout</b> - oznámení se budou zobrazovat pokud aplikace běží.</string>
<string name="turn_off_battery_optimization">Chcete-li ji používat, <b>vypněte optimalizaci baterie</b> pro <xliff:g id="appName">SimpleX</xliff:g> v dalším dialogu. V opačném případě budou oznámení vypnuta.</string>
<string name="periodic_notifications_desc">Aplikace pravidelně načítá nové zprávy - denně spotřebuje několik procent baterie. Aplikace nepoužívá push oznámení - data ze zařízení nejsou odesílána na servery.</string>
<string name="enter_passphrase_notification_title">Je vyžadována přístupová fráze</string>
<string name="enter_passphrase_notification_desc">Chcete-li dostávat oznámení, zadejte přístupovou frázi do databáze.</string>
<string name="database_initialization_error_title">Nelze inicializovat databázi</string>
<string name="hide_notification">Skrýt</string>
<string name="ntf_channel_calls">Volání SimpleX Chat</string>
<string name="notification_preview_new_message">nová zpráva</string>
<string name="notification_new_contact_request">Žádost o nový kontakt</string>
<string name="delete_message_cannot_be_undone_warning">Zpráva bude smazána - nelze to vzít zpět!</string>
<string name="confirm_verb">Potvrdit</string>
<string name="send_us_an_email">Pošlete nám e-mail</string>
<string name="chat_lock">Zámek SimpleX</string>
<string name="install_simplex_chat_for_terminal">Instalace <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro terminál</string>
<string name="star_on_github">Hvězda na GitHubu</string>
<string name="rate_the_app">Ohodnoťte aplikaci</string>
<string name="your_SMP_servers">Vaše servery SMP</string>
<string name="network_disable_socks_info">Pokud potvrdíte, budou servery pro zasílání zpráv vidět vaši IP adresu a váš poskytovatel - ke kterým serverům se připojujete.</string>
<string name="colored">barevné</string>
<string name="secret">secret</string>
<string name="callstatus_calling">volání…</string>
<string name="callstate_connected">připojeno</string>
<string name="callstate_ended">ukončeno</string>
<string name="next_generation_of_private_messaging">Nová generace soukromých zpráv</string>
<string name="people_can_connect_only_via_links_you_share">Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte.</string>
<string name="integrity_msg_bad_hash">špatný hash zprávy</string>
<string name="chat_database_imported">Importovaná databáze chatu</string>
<string name="new_passphrase">Nová přístupová fráze…</string>
<string name="save_passphrase_and_open_chat">Uložte heslo a otevřete chat</string>
<string name="chat_archive_section">ARCHIV CHATU</string>
<string name="no_contacts_selected">Nebyl vybrán žádný kontakt</string>
<string name="invite_prohibited_description">Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil</string>
<string name="info_row_group">Skupina</string>
<string name="network_options_revert">Vrátit</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Aktualizací nastavení se klient znovu připojí ke všem serverům.</string>
<string name="accept_feature_set_1_day">Nastavit 1 den</string>
<string name="connection_error_auth">Chyba spojení (AUTH)</string>
<string name="sender_may_have_deleted_the_connection_request">Odesílatel možná smazal požadavek připojení</string>
<string name="error_smp_test_server_auth">Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo</string>
<string name="smp_server_test_delete_queue">Odstranit frontu</string>
<string name="delete_group_menu_action">Smazat</string>
<string name="delete_pending_connection__question">Smazat čekající připojení\?</string>
<string name="icon_descr_settings">Nastavení</string>
<string name="image_descr_qr_code">QR kód</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Váš kontakt může z aplikace naskenovat QR kód.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Pokud se nemůžete setkat osobně, ukažte <b>ve videohovoru QR kód</b> nebo sdílejte odkaz.</string>
<string name="scan_code">Skenovat kód</string>
<string name="incorrect_code">Nesprávný bezpečnostní kód!</string>
<string name="scan_code_from_contacts_app">Naskenujte bezpečnostní kód z aplikace vašeho kontaktu.</string>
<string name="mark_code_verified">Označit jako ověřený</string>
<string name="clear_verification">Zrušte ověření</string>
<string name="to_verify_compare">Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</string>
<string name="your_settings">Vaše nastavení</string>
<string name="your_simplex_contact_address">Vaše <xliff:g id="appName">Adresa kontaktu SimpleX</xliff:g></string>
<string name="database_passphrase_and_export">Databázová hesla a export</string>
<string name="your_chat_profiles">Vaše profily v chatu</string>
<string name="chat_with_the_founder">Zasílání otázek a nápadů</string>
<string name="smp_servers_test_server">Testovací server</string>
<string name="enter_one_ICE_server_per_line">Servery ICE (jeden na řádek)</string>
<string name="network_use_onion_hosts_required_desc">Pro připojení budou vyžadováni Onion hostitelé.</string>
<string name="update_network_session_mode_question">Aktualizovat režim izolace\?</string>
<string name="app_version_code">Sestavení aplikace: %s</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit. O své kontakty nepřijdete, pokud ji později smažete.</string>
<string name="share_link">Sdílet odkaz</string>
<string name="delete_address">Odstranit adresu</string>
<string name="full_name__field">Celé jméno:</string>
<string name="your_current_profile">Váš současný profil</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Pro zachování soukromí má aplikace místo push oznámení <b><xliff:g id="appName">SimpleX</xliff:g> službu na pozadí</b> - denně spotřebuje několik procent baterie.</string>
<string name="periodic_notifications">Pravidelná oznámení</string>
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> služba</string>
<string name="simplex_service_notification_text">Příjem zpráv…</string>
<string name="ntf_channel_messages">Zprávy SimpleX Chat</string>
<string name="settings_notification_preview_mode_title">Zobrazení náhledu</string>
<string name="settings_notification_preview_title">Náhled oznámení</string>
<string name="notifications_mode_off">Spustí se při otevření aplikace</string>
<string name="notifications_mode_periodic">Spouští se pravidelně</string>
<string name="notifications_mode_service">Vždy zapnuto</string>
<string name="notifications_mode_periodic_desc">Kontroluje nové zprávy každých 10 minut po dobu až 1 minuty</string>
<string name="notification_preview_mode_contact_desc">Zobrazit pouze kontakt</string>
<string name="notification_preview_somebody">Skrytý kontakt:</string>
<string name="la_notice_turn_on">Zapněte funkci</string>
<string name="auth_simplex_lock_turned_on">Zapnutý zámek SimpleX Lock</string>
<string name="auth_unlock">Odemknutí stránky</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Ověřování zařízení není povoleno. Jakmile povolíte ověřování zařízení, můžete zámek SimpleX Lock zapnout prostřednictvím Nastavení.</string>
<string name="auth_device_authentication_is_disabled_turning_off">Ověřování zařízení je zakázáno. Vypnutí zámku SimpleX Lock.</string>
<string name="edit_verb">Upravit</string>
<string name="delete_verb">Smazat</string>
<string name="for_everybody">Pro všechny</string>
<string name="icon_descr_sent_msg_status_sent">odesláno</string>
<string name="contact_connection_pending">připojení…</string>
<string name="images_limit_desc">Současně lze odeslat pouze 10 obrázků</string>
<string name="image_decoding_exception_title">Chyba dekódování</string>
<string name="image_saved">Obrázek uložen do galerie</string>
<string name="icon_descr_file">Soubor</string>
<string name="large_file">Velký soubor!</string>
<string name="file_will_be_received_when_contact_is_online">Soubor bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string>
<string name="file_saved">Soubor uložen</string>
<string name="file_not_found">Soubor nebyl nalezen</string>
<string name="voice_message_with_duration">Hlasová zpráva (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontakt a všechny zprávy budou smazány - nelze to vzít zpět!</string>
<string name="button_delete_contact">Smazat kontakt</string>
<string name="text_field_set_contact_placeholder">Nastavení jména kontaktu…</string>
<string name="view_security_code">Zobrazení bezpečnostního kódu</string>
<string name="icon_descr_record_voice_message">Nahrát hlasovou zprávu</string>
<string name="voice_messages_prohibited">Hlasové zprávy jsou zakázány!</string>
<string name="ask_your_contact_to_enable_voice">Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv.</string>
<string name="send_live_message_desc">Poslat živou zprávu - zpráva se bude aktualizovat pro příjemce během psaní.</string>
<string name="share_one_time_link">Vytvořit jednorázovou pozvánku</string>
<string name="scan_QR_code">Skenovat QR kód</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">( skenovat nebo vložit ze schránky)</string>
<string name="edit_image">Upravit obrázek</string>
<string name="delete_image">Smazat obrázek</string>
<string name="callstatus_error">chyba volání</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</string>
<string name="create_your_profile">Vytvořte si svůj profil</string>
<string name="make_private_connection">Vytvořte si soukromé připojení</string>
<string name="encrypted_video_call">Videohovor šifrovaný e2e</string>
<string name="encrypted_audio_call">e2e šifrovaný audio hovor</string>
<string name="status_contact_has_e2e_encryption">kontakt má šifrování e2e</string>
<string name="status_contact_has_no_e2e_encryption">kontakt nemá šifrování e2e</string>
<string name="call_connection_peer_to_peer">peer-to-peer</string>
<string name="call_connection_via_relay">přes relé</string>
<string name="icon_descr_hang_up">Zavěsit</string>
<string name="icon_descr_video_off">Video vypnuto</string>
<string name="icon_descr_video_on">Video zapnuto</string>
<string name="icon_descr_audio_off">Zvuk vypnutý</string>
<string name="integrity_msg_bad_id">špatné ID zprávy</string>
<string name="integrity_msg_duplicate">duplicitní zpráva</string>
<string name="alert_title_skipped_messages">Přeskočené zprávy</string>
<string name="privacy_and_security">Ochrana osobních údajů a zabezpečení</string>
<string name="your_privacy">Vaše soukromí</string>
<string name="protect_app_screen">Ochrana obrazovky aplikace</string>
<string name="send_link_previews">Odesílání náhledů odkazů</string>
<string name="full_backup">Zálohování dat aplikace</string>
<string name="confirm_new_passphrase">Potvrdit novou heslovou frázi…</string>
<string name="error_with_info">Chyba: %s</string>
<string name="leave_group_question">Opustit skupinu\?</string>
<string name="icon_descr_group_inactive">Skupina je neaktivní</string>
<string name="rcv_group_event_member_left">left</string>
<string name="clear_contacts_selection_button">Vymazat</string>
<string name="switch_verb">Přepnout</string>
<string name="member_role_will_be_changed_with_notification">Role bude změněna na \"%s\". Všichni ve skupině budou informováni.</string>
<string name="error_removing_member">Chyba při odebrání člena</string>
<string name="error_saving_group_profile">Chyba při ukládání profilu skupiny</string>
<string name="network_option_seconds_label">sec</string>
<string name="incognito_info_allows">Umožňuje mít v jednom profilu chatu mnoho anonymních spojení bez jakýchkoli sdílených údajů mezi nimi.</string>
<string name="incognito_info_share">Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve.</string>
<string name="theme_system">Systém</string>
<string name="voice_messages">Hlasové zprávy</string>
<string name="both_you_and_your_contacts_can_delete">Vy i váš kontakt můžete nevratně mazat odeslané zprávy.</string>
<string name="ttl_m">\"%dm</string>
<string name="ttl_mth">\"%dmth</string>
<string name="ttl_hours">\"%d hodin</string>
<string name="ttl_h">\"%dh</string>
<string name="ttl_d">\"%dd</string>
<string name="v4_2_security_assessment">Posouzení bezpečnosti</string>
<string name="v4_2_security_assessment_desc">Bezpečnost SimpleX Chat byla prověřena společností Trail of Bits.</string>
<string name="v4_3_irreversible_message_deletion">Nevratné mazání zpráv</string>
<string name="v4_3_improved_server_configuration">Vylepšená konfigurace serveru</string>
<string name="v4_3_improved_privacy_and_security">Vylepšená ochrana soukromí a zabezpečení</string>
<string name="v4_3_improved_privacy_and_security_desc">Skrytí obrazovky aplikace v posledních aplikacích.</string>
<string name="v4_4_disappearing_messages_desc">Odeslané zprávy se po uplynutí nastavené doby odstraní.</string>
<string name="v4_4_live_messages">Živé zprávy</string>
<string name="v4_4_live_messages_desc">Příjemci uvidí aktualizace během jejich psaní.</string>
<string name="v4_4_verify_connection_security">Ověření zabezpečení připojení</string>
<string name="v4_4_french_interface">Francouzské rozhraní</string>
<string name="v4_4_french_interface_descr">Díky uživatelům - přispívejte prostřednictvím Weblate!</string>
<string name="v4_5_multiple_chat_profiles">Více chatovacích profilů</string>
<string name="v4_5_multiple_chat_profiles_descr">Různá jména, avatary a dopravní izolace.</string>
<string name="v4_5_message_draft">Návrh zprávy</string>
<string name="v4_5_message_draft_descr">Zachování posledního návrhu zprávy s přílohami.</string>
<string name="v4_5_transport_isolation">Izolace transportu</string>
<string name="v4_5_transport_isolation_descr">Podle profilu chatu (výchozí) nebo podle připojení (BETA).</string>
<string name="you_will_join_group">Připojíte se ke skupině, na kterou tento odkaz odkazuje, a spojíte se s jejími členy.</string>
<string name="connect_via_link_verb">Připojení</string>
<string name="connected_to_server_to_receive_messages_from_contact">Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Pokoušíte se připojit k serveru používanému pro příjem zpráv od tohoto kontaktu (chyba: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="marked_deleted_description">označeno jako smazáno</string>
<string name="sending_files_not_yet_supported">Odesílání souborů zatím není podporováno</string>
<string name="receiving_files_not_yet_supported">přijímání souborů zatím není podporováno</string>
<string name="sender_you_pronoun">ty</string>
<string name="unknown_message_format">neznámý formát zprávy</string>
<string name="invalid_message_format">neplatný formát zprávy</string>
<string name="live">ŽIVĚ</string>
<string name="description_via_group_link_incognito">inkognito přes skupinový odkaz</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů SMP jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
<string name="failed_to_create_user_title">Chyba při vytváření profilu!</string>
<string name="failed_to_create_user_duplicate_title">Duplicitní zobrazované jméno!</string>
<string name="failed_to_active_user_title">Chyba při přepínání profilu!</string>
<string name="error_joining_group">Chyba při připojování ke skupině</string>
<string name="cannot_receive_file">Nelze přijmout soubor</string>
<string name="sender_cancelled_file_transfer">Odesílatel zrušil přenos souboru.</string>
<string name="error_receiving_file">Chyba při příjmu souboru</string>
<string name="error_creating_address">Chyba při vytváření adresy</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný.</string>
<string name="connection_error_auth_desc">Pokud váš kontakt spojení nesmazal nebo tento odkaz již byl použit, může se jednat o chybu - nahlaste ji prosím. Chcete-li se připojit, požádejte prosím svůj kontakt o vytvoření jiného odkazu pro připojení a zkontrolujte, zda máte stabilní připojení k síti.</string>
<string name="error_deleting_contact">Chyba mazání kontaktu</string>
<string name="error_deleting_group">Chyba mazání skupiny</string>
<string name="error_deleting_contact_request">Chyba mazání žádosti kontaktu</string>
<string name="error_deleting_pending_contact_connection">Chyba mazání probíhajícího připojení kontaktu</string>
<string name="error_smp_test_failed_at_step">Test selhal v kroku %s.</string>
<string name="error_smp_test_certificate">Je možné, že otisk certifikátu v adrese serveru je nesprávný.</string>
<string name="smp_server_test_connect">Připojit</string>
<string name="smp_server_test_disconnect">Odpojit</string>
<string name="error_deleting_user">Chyba mazání uživatelského profilu</string>
<string name="icon_descr_instant_notifications">Okamžitá oznámení</string>
<string name="service_notifications_disabled">Okamžitá oznámení jsou vypnutá!</string>
<string name="turning_off_service_and_periodic">Je aktivní optimalizace baterie, která vypíná službu na pozadí a pravidelné požadavky na nové zprávy. Můžete je znovu povolit prostřednictvím nastavení.</string>
<string name="periodic_notifications_disabled">Pravidelná oznámení jsou vypnuta!</string>
<string name="database_initialization_error_desc">Databáze nefunguje správně. Klepnutím na se dozvíte více</string>
<string name="notifications_mode_off_desc">Aplikace může přijímat oznámení pouze při svém spuštění, žádná služba na pozadí se nespustí</string>
<string name="notification_display_mode_hidden_desc">Skrýt kontakt a zprávu</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Chcete-li chránit své informace, zapněte zámek SimpleX Lock. Před zapnutím této funkce budete vyzváni k dokončení ověření.</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Při spuštění nebo obnovení aplikace po 30 sekundách na pozadí budete vyzváni k ověření.</string>
<string name="auth_disable_simplex_lock">Vypnutí zámku SimpleX</string>
<string name="auth_confirm_credential">Potvrďte své pověření</string>
<string name="auth_unavailable">Ověřování není k dispozici</string>
<string name="auth_stop_chat">Zastavení chatu</string>
<string name="auth_open_chat_console">Otevřete konzolu chatu</string>
<string name="message_delivery_error_title">Chyba doručení zprávy</string>
<string name="message_delivery_error_desc">Tento kontakt s největší pravděpodobností smazal spojení s vámi.</string>
<string name="save_verb">Uložit</string>
<string name="reveal_verb">Odhalit</string>
<string name="hide_verb">Skrýt</string>
<string name="delete_message__question">Smazat zprávu\?</string>
<string name="delete_message_mark_deleted_warning">Zpráva bude označena ke smazání. Příjemce (příjemci) bude moci tuto zprávu odhalit.</string>
<string name="for_me_only">Smazat pro mě</string>
<string name="icon_descr_edited">upraveno</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">neautorizované odeslání</string>
<string name="group_preview_you_are_invited">jste pozváni do skupiny</string>
<string name="group_preview_join_as">připojit jako %s</string>
<string name="group_connection_pending">připojuje se…</string>
<string name="tap_to_start_new_chat">Klepnutím na zahájíte nový chat</string>
<string name="chat_with_developers">Chatujte s vývojáři</string>
<string name="you_have_no_chats">Nemáte žádné konverzace</string>
<string name="icon_descr_cancel_image_preview">Zrušit náhled obrázku</string>
<string name="share_message">Sdílet zprávu…</string>
<string name="share_image">Sdílet obrázek…</string>
<string name="icon_descr_cancel_file_preview">Zrušit náhled souboru</string>
<string name="images_limit_title">Příliš mnoho obrázků!</string>
<string name="waiting_for_file">Čekání na soubor</string>
<string name="notifications">Oznámení</string>
<string name="delete_contact_question">Smazat kontakt\?</string>
<string name="icon_descr_server_status_pending">Čeká na vyřízení</string>
<string name="verify_security_code">Ověření bezpečnostního kódu</string>
<string name="icon_descr_send_message">Odeslat zprávu</string>
<string name="only_group_owners_can_enable_voice">Pouze majitelé skupin mohou povolit zasílání hlasových zpráv.</string>
<string name="send_live_message">Odeslat živou zprávu</string>
<string name="live_message">Živé zprávy!</string>
<string name="connect_via_link_or_qr">Připojit se prostřednictvím odkazu / QR kódu</string>
<string name="thank_you_for_installing_simplex">Děkujeme za instalaci <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="you_can_connect_to_simplex_chat_founder">Můžete se <font color="#0088ff">připojit k <xliff:g id="appNameFull">SimpleX Chat</xliff:g> vývojářům a položit jim případné dotazy a získat aktualizace</font>.</string>
<string name="to_connect_via_link_title">K připojení prostřednictvím odkazu</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Pokud jste dostal <xliff:g id="appName">SimpleX Chat</xliff:g> zvací odkaz, Můžete ho otevřít v prohlížeči:</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Pokud zvolíte odmítnutí, odesílatel NEBUDE upozorněn.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobilní telefon: tap <b>Otevřete v mobilní aplikaci</b>, potom klikněte <b>Připojit</b>.</string>
<string name="reject_contact_button">Odmítnout</string>
<string name="clear_chat_button">Čistá konverzace</string>
<string name="clear_chat_menu_action">Čistý</string>
<string name="delete_contact_menu_action">Smazat</string>
<string name="set_contact_name">Nastavit jméno kontaktu</string>
<string name="you_accepted_connection">Přijali jste spojení</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Aby se připojení dokončilo, musí být váš kontakt online. Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s novým odkazem).</string>
<string name="contact_wants_to_connect_with_you">Chce se s vámi spojit!</string>
<string name="icon_descr_profile_image_placeholder">Zástupce profilového obrázku</string>
<string name="image_descr_profile_image">profilový obrázek</string>
<string name="icon_descr_close_button">Tlačítko Zavřít</string>
<string name="alert_title_contact_connection_pending">Kontakt ještě není připojen!</string>
<string name="image_descr_link_preview">náhledový obrázek odkazu</string>
<string name="icon_descr_cancel_link_preview">Zrušit náhled odkazu</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> Logo</string>
<string name="icon_descr_email">E-mail</string>
<string name="icon_descr_more_button">Více na</string>
<string name="show_QR_code">Zobrazit QR kód</string>
<string name="invalid_QR_code">Neplatný QR kód</string>
<string name="this_QR_code_is_not_a_link">Tento QR kód není odkaz!</string>
<string name="invalid_contact_link">Neplatný odkaz!</string>
<string name="this_link_is_not_a_valid_connection_link">Tento odkaz není platným odkazem pro připojení!</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Pokud se nemůžete setkat osobně, můžete <b>skenovat QR kód ve videohovoru</b> nebo může váš kontakt sdílet pozvánku.</string>
<string name="connect_via_link">Připojte se prostřednictvím odkazu</string>
<string name="connect_button">Připojit</string>
<string name="paste_button">Vložit</string>
<string name="this_string_is_not_a_connection_link">Tento řetězec není odkazem na připojení!</string>
<string name="you_can_also_connect_by_clicking_the_link">Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko <b>Otevřít v mobilní aplikaci</b>.</string>
<string name="is_not_verified">\"%s není ověřeno</string>
<string name="how_to_use_simplex_chat">Jak ji používat</string>
<string name="markdown_help">Nápověda k markdown</string>
<string name="smp_servers_save">Uložit servery</string>
<string name="markdown_in_messages">Markdown ve zprávách</string>
<string name="smp_servers_test_servers">Testovací servery</string>
<string name="smp_servers_preset_server">Přednastavený server</string>
<string name="smp_servers_your_server">Váš server</string>
<string name="smp_servers_your_server_address">Adresa vašeho serveru</string>
<string name="smp_servers_use_server">Použít server</string>
<string name="smp_servers_use_server_for_new_conn">Použít pro nová připojení</string>
<string name="smp_servers_per_user">Servery pro nová připojení vašeho aktuálního profilu chatu.</string>
<string name="use_simplex_chat_servers__question">Použít <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servery\?</string>
<string name="using_simplex_chat_servers">Použití <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serverů.</string>
<string name="saved_ICE_servers_will_be_removed">Uložené servery WebRTC ICE budou odstraněny.</string>
<string name="error_saving_ICE_servers">Chyba při ukládání serverů ICE</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
<string name="save_servers_button">Uložit</string>
<string name="network_and_servers">Síť a servery</string>
<string name="network_socks_toggle">Použít proxy server SOCKS (port 9050)</string>
<string name="update_onion_hosts_settings_question">Aktualizovat nastavení hostitelů .onion\?</string>
<string name="network_use_onion_hosts">Použít hostitele .onion</string>
<string name="network_use_onion_hosts_prefer">Když je k dispozici</string>
<string name="network_use_onion_hosts_required">Povinné</string>
<string name="network_use_onion_hosts_prefer_desc">Onion hostitelé se použijí, pokud jsou k dispozici.</string>
<string name="network_use_onion_hosts_no_desc">Onion hostitelé nebudou použiti.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hostitelé budou použiti, pokud budou k dispozici.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Pro připojení budou vyžadováni Onion hostitelé.</string>
<string name="network_session_mode_transport_isolation">Izolace přenosu</string>
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
<string name="network_session_mode_entity_description">Oddělit TCP připojení (a SOCKS pověření) bude použito <b>pro všechny kontakty a členy skupin</b>. <b>Upozornění</b>: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.</string>
<string name="appearance_settings">Vzhled</string>
<string name="app_version_title">Verze aplikace</string>
<string name="app_version_name">Verze aplikace: v%s</string>
<string name="core_version">Verze jádra: v%s</string>
<string name="core_build_timestamp">Jádro sestaveno na: %s</string>
<string name="delete_address__question">Smazat adresu\?</string>
<string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string>
<string name="contact_requests">Žádosti o kontakt</string>
<string name="display_name__field">Zobrazované jméno:</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty. <xliff:g id="appName">SimpleX</xliff:g> servery váš profil nevidí.</string>
<string name="save_preferences_question">Uložit předvolby\?</string>
<string name="save_and_notify_contact">Uložit a upozornit kontakt</string>
<string name="save_and_notify_contacts">Uložit a upozornit kontakty</string>
<string name="you_control_your_chat">Chat máte pod kontrolou!</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Na serverech neukládáme žádné vaše kontakty ani zprávy (po doručení).</string>
<string name="your_profile_is_stored_on_your_device">Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</string>
<string name="display_name">Zobrazované jméno</string>
<string name="full_name_optional__prompt">Celé jméno (volitelné)</string>
<string name="create_profile_button">Vytvořit</string>
<string name="how_to_use_markdown">Jak používat markdown</string>
<string name="you_can_use_markdown_to_format_messages__prompt">K formátování zpráv můžete použít markdown:</string>
<string name="italic">kurzíva</string>
<string name="strikethrough">přeškrtnout</string>
<string name="callstatus_missed">zmeškané volání</string>
<string name="callstatus_rejected">odmítnutý hovor</string>
<string name="callstatus_connecting">spojovací hovor…</string>
<string name="callstatus_ended">hovor ukončen <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstate_starting">začíná…</string>
<string name="callstate_waiting_for_answer">čeká na odpověď…</string>
<string name="callstate_waiting_for_confirmation">čekání na potvrzení…</string>
<string name="callstate_received_answer">obdržel odpověď…</string>
<string name="callstate_received_confirmation">obdržel potvrzení…</string>
<string name="callstate_connecting">připojení…</string>
<string name="privacy_redefined">Nové vymezení soukromí</string>
<string name="first_platform_without_user_ids">1. Platforma bez identifikátorů uživatelů - soukromá už od záměru.</string>
<string name="immune_to_spam_and_abuse">Odolná vůči spamu a zneužití</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">K ochraně soukromí, místo uživatelských ID používaných všemi ostatními platformami, <xliff:g id="appName">SimpleX</xliff:g> má identifikátory pro fronty zpráv, zvlášť pro každý z vašich kontaktů.</string>
<string name="many_people_asked_how_can_it_deliver">Mnoho lidí se ptalo: <i>if <xliff:g id="appName">SimpleX</xliff:g> nemá žádné identifikátory uživatelů, jak může doručovat zprávy\?</i></string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Vy kontrolujete přez který server(y) <b>přijímat</b> zprávy, vaše kontakty - servery které používáte pro konverzaci.</string>
<string name="read_more_in_github">Další informace najdete v našem repozitáři GitHub.</string>
<string name="read_more_in_github_with_link">Další informace najdete v našem <font color="#0088ff">úložišti GitHub</font>.</string>
<string name="use_chat">Konverzovat</string>
<string name="onboarding_notifications_mode_subtitle">Lze změnit později v nastavení.</string>
<string name="onboarding_notifications_mode_off">Když je aplikace spuštěná</string>
<string name="onboarding_notifications_mode_service">Okamžitý</string>
<string name="onboarding_notifications_mode_off_desc"><b>Nejlepší pro baterii</b>. Budete přijímat oznámení pouze když aplikace běží, služba na pozadí NEBUDE použita.</string>
<string name="onboarding_notifications_mode_periodic_desc">&lt;Dobré pro baterii&lt;/b&gt;. Služba na pozadí kontroluje nové zprávy každých 10 minut. Můžete zmeškat hovory a naléhavé zprávy.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Využívá více baterie</b>! Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
<string name="paste_the_link_you_received">Vložení přijatého odkazu</string>
<string name="incoming_video_call">Příchozí videohovor</string>
<string name="incoming_audio_call">Příchozí zvukový hovor</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> se s vámi chce spojit prostřednictvím</string>
<string name="video_call_no_encryption">videohovoru (nešifrovaného e2e).</string>
<string name="audio_call_no_encryption">zvukový hovor (nešifrovaný e2e)</string>
<string name="reject">Odmítnout</string>
<string name="your_calls">Vaše hovory</string>
<string name="connect_calls_via_relay">Spojení přes relé</string>
<string name="show_call_on_lock_screen">Zobrazit</string>
<string name="no_call_on_lock_screen">Zakázat</string>
<string name="your_ice_servers">Vaše servery ICE</string>
<string name="webrtc_ice_servers">WebRTC servery ICE</string>
<string name="status_no_e2e_encryption">bez šifrování e2e</string>
<string name="icon_descr_flip_camera">Flipová kamera</string>
<string name="icon_descr_call_pending_sent">Probíhající hovor</string>
<string name="icon_descr_call_missed">Zmeškaný hovor</string>
<string name="icon_descr_call_rejected">Odmítnutý hovor</string>
<string name="icon_descr_call_connecting">Spojení hovoru</string>
<string name="icon_descr_call_ended">Ukončený hovor</string>
<string name="answer_call">Přijmout hovor</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> přeskočená zpráva (zprávy)</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Může se to stát, když: 1. Zprávy na serveru vyprší, pokud nebyly přijaty po dobu 30 dnů, 2. Server, který používáte pro příjem zpráv od tohoto kontaktu, byl aktualizován a restartován. 3. Spojení je narušeno. Připojte se k vývojářům prostřednictvím Nastavení, abyste mohli dostávat aktualizace o serverech. Budeme přidávat redundantní servery, abychom zabránili ztrátě zpráv.</string>
<string name="settings_section_title_you">VY</string>
<string name="settings_section_title_support">PODPORA SIMPLEX CHAT</string>
<string name="settings_section_title_develop">DEVELOP</string>
<string name="settings_developer_tools">Nástroje pro vývojáře</string>
<string name="settings_section_title_incognito">Režim inkognito</string>
<string name="your_chat_database">Vaše chatovací databáze</string>
<string name="run_chat_section">SPUSTIT CHAT</string>
<string name="chat_is_running">Chat je spuštěn</string>
<string name="chat_is_stopped">Chat je zastaven</string>
<string name="chat_database_section">CHAT DATABÁZE</string>
<string name="database_passphrase">Heslo databáze</string>
<string name="new_database_archive">Archiv nové databáze</string>
<string name="old_database_archive">Archiv staré databáze</string>
<string name="error_starting_chat">Chyba při spuštění chatu</string>
<string name="stop_chat_question">Zastavit chat\?</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Zastavení chatu pro export, import nebo smazání databáze chatu. Během zastavení chatu nebudete moci přijímat a odesílat zprávy.</string>
<string name="stop_chat_confirmation">Zastavit</string>
<string name="set_password_to_export">Nastavení přístupové fráze pro export</string>
<string name="set_password_to_export_desc">Databáze je šifrována pomocí náhodné přístupové fráze. Před exportem ji změňte.</string>
<string name="error_stopping_chat">Chyba při zastavení chatu</string>
<string name="import_database_question">Importovat databázi chatu\?</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Vaše aktuální databáze chatu bude Smazána a Nahrazena importovanou databází. Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
<string name="error_deleting_database">Chyba při mazání databáze chatu</string>
<string name="error_importing_database">Chyba při importu databáze chatu</string>
<string name="chat_database_deleted">Databáze chatu odstraněna</string>
<string name="delete_files_and_media_for_all_users">Odstranění souborů pro všechny profily chatu</string>
<string name="delete_files_and_media_all">Odstranit všechny soubory</string>
<string name="delete_files_and_media_desc">Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány.</string>
<string name="no_received_app_files">Žádné přijaté ani odeslané soubory</string>
<string name="total_files_count_and_size">%d soubor(ů) s celkovou velikostí %s</string>
<string name="chat_item_ttl_none">nikdy</string>
<string name="chat_item_ttl_seconds">\"%s vteřin(y)</string>
<string name="messages_section_title">Zprávy</string>
<string name="messages_section_description">Toto nastavení se vztahuje na zprávy ve vašem aktuálním profilu chatu.</string>
<string name="delete_messages_after">Smazat zprávy po</string>
<string name="enable_automatic_deletion_question">Povolit automatické mazání zpráv\?</string>
<string name="enable_automatic_deletion_message">Tuto akci nelze vzít zpět - zprávy odeslané a přijaté dříve, než bylo zvoleno, budou smazány. Může to trvat několik minut.</string>
<string name="error_changing_message_deletion">Chyba změny nastavení</string>
<string name="save_passphrase_in_keychain">Uložit přístupovou frázi do úložiště klíčů</string>
<string name="database_encrypted">Databáze zašifrována!</string>
<string name="error_encrypting_database">Chyba šifrování databáze</string>
<string name="encrypt_database">Šifrovat</string>
<string name="encrypted_with_random_passphrase">Databáze je zašifrována pomocí náhodné přístupové fráze, můžete ji změnit.</string>
<string name="keychain_allows_to_receive_ntfs">K bezpečnému uložení přístupové fráze se použije úložiště klíčů Android, po restartování aplikace nebo změně přístupové fráze - umožní přijímání oznámení.</string>
<string name="you_have_to_enter_passphrase_every_time">Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení.</string>
<string name="encrypt_database_question">Šifrovat databázi\?</string>
<string name="change_database_passphrase_question">Změnit přístupovou frázi databáze\?</string>
<string name="database_will_be_encrypted">Databáze bude zašifrována.</string>
<string name="database_encryption_will_be_updated">Šifrovací heslová fráze databáze bude aktualizována a uložena do úložiště klíčů.</string>
<string name="database_passphrase_will_be_updated">Šifrovací heslová fráze databáze bude aktualizována.</string>
<string name="store_passphrase_securely_without_recover">Uložte prosím bezpečně přístupovou frázi, pokud ji ztratíte, NEBUDE možné přistupovat k chatu.</string>
<string name="wrong_passphrase">Špatná přístupová fráze k databázi</string>
<string name="encrypted_database">Zašifrovaná databáze</string>
<string name="database_error">Chyba databáze</string>
<string name="keychain_error">Chyba klíčenky</string>
<string name="passphrase_is_different">Databázová heslová fráze se liší od uložené v klíčence.</string>
<string name="cannot_access_keychain">Nelze získat přístup k úložišti klíčů pro uložení hesla k databázi.</string>
<string name="unknown_database_error_with_info">Neznámá chyba databáze: %s</string>
<string name="wrong_passphrase_title">Špatná přístupová fráze!</string>
<string name="enter_correct_passphrase">Zadejte správnou přístupovou frázi.</string>
<string name="enter_passphrase">Zadejte přístupovou frázi…</string>
<string name="database_backup_can_be_restored">Pokus o změnu přístupové fráze databáze nebyl dokončen.</string>
<string name="restore_database_alert_title">Obnovit zálohu databáze\?</string>
<string name="restore_database_alert_confirm">Obnovit</string>
<string name="database_restore_error">Chyba obnovení databáze</string>
<string name="restore_passphrase_not_found_desc">Heslo nebylo nalezeno v úložišti klíčů, zadejte jej prosím ručně. K této situaci mohlo dojít, pokud jste obnovili data aplikace pomocí zálohovacího nástroje. Pokud tomu tak není, obraťte se na vývojáře.</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Chat můžete spustit přes Nastavení aplikace / Databáze nebo restartováním aplikace.</string>
<string name="save_archive">Uložit archiv</string>
<string name="delete_archive">Smazat archiv</string>
<string name="group_invitation_item_description">pozvánka do skupiny <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="archive_created_on_ts">Vytvořeno dne <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Jste zváni do skupiny. Připojte se a spojte se s členy skupiny.</string>
<string name="join_group_incognito_button">Připojte se inkognito</string>
<string name="joining_group">Připojení ke skupině</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Připojili jste se k této skupině. Připojení k pozvání člena skupiny.</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Přestanete dostávat zprávy z této skupiny. Historie chatu bude zachována.</string>
<string name="alert_title_group_invitation_expired">Platnost pozvánky vypršela!</string>
<string name="alert_message_group_invitation_expired">Pozvánka do skupiny již není platná, byla odstraněna odesílatelem.</string>
<string name="alert_message_no_group">Tato skupina již neexistuje.</string>
<string name="alert_title_cant_invite_contacts_descr">Pro tuto skupinu používáte inkognito profil - abyste zabránili sdílení svého hlavního profilu, není pozvání kontaktů povoleno.</string>
<string name="you_sent_group_invitation">Odeslali jste pozvánku do skupiny</string>
<string name="you_are_invited_to_group">Jste pozváni do skupiny</string>
<string name="group_invitation_tap_to_join">Klepnutím se připojíte</string>
<string name="group_invitation_tap_to_join_incognito">Klepnutím se připojíte inkognito</string>
<string name="you_joined_this_group">Připojili jste se k této skupině</string>
<string name="you_rejected_group_invitation">Odmítli jste pozvánku do skupiny</string>
<string name="group_invitation_expired">Platnost pozvánky do skupiny vypršela</string>
<string name="rcv_group_event_member_added">pozvánka <xliff:g id="profil člena" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">připojeno</string>
<string name="rcv_group_event_changed_member_role">změnil roli %s na %s</string>
<string name="rcv_group_event_changed_your_role">změnil svou roli na %s</string>
<string name="rcv_group_event_member_deleted">odstraněno <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_user_deleted">odstranil vás</string>
<string name="rcv_group_event_group_deleted">odstraněna skupina</string>
<string name="rcv_group_event_updated_group_profile">aktualizoval profil skupiny</string>
<string name="rcv_group_event_invited_via_your_group_link">pozváni prostřednictvím odkazu na vaši skupinu</string>
<string name="snd_group_event_group_profile_updated">profil skupiny aktualizován</string>
<string name="rcv_conn_event_switch_queue_phase_changing">změna adresy…</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">jste změnili adresu pro %s</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">změna adresy pro %s…</string>
<string name="snd_conn_event_switch_queue_phase_completed">změnili jste adresu</string>
<string name="snd_conn_event_switch_queue_phase_changing">změna adresy…</string>
<string name="group_member_role_member">člen</string>
<string name="group_member_role_owner">vlastník</string>
<string name="group_member_status_removed">odstraněno</string>
<string name="group_member_status_left">opustil</string>
<string name="group_member_status_group_deleted">skupina smazána</string>
<string name="group_member_status_invited">pozvánka</string>
<string name="group_member_status_introduced">připojující (zavedený)</string>
<string name="group_member_status_intro_invitation">připojení (pozvánka na představení)</string>
<string name="group_member_status_accepted">připojení (přijato)</string>
<string name="group_member_status_announced">připojení (oznámeno)</string>
<string name="group_member_status_connected">připojen</string>
<string name="group_member_status_complete">kompletní</string>
<string name="group_member_status_creator">tvůrce</string>
<string name="group_member_status_connecting">připojování</string>
<string name="no_contacts_to_add">Žádné kontakty k přidání</string>
<string name="new_member_role">Nová role člena</string>
<string name="invite_to_group_button">Pozvat do skupiny</string>
<string name="skip_inviting_button">Přeskočit pozvání členů</string>
<string name="select_contacts">Vybrat kontakty</string>
<string name="icon_descr_contact_checked">Zkontrolované kontakty</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> kontakt(y) vybrán(y)</string>
<string name="button_add_members">Pozvat členy</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBERS</string>
<string name="group_info_member_you">vy: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="button_delete_group">Smazat skupinu</string>
<string name="delete_group_question">Smazat skupinu\?</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Skupina bude smazána pro všechny členy - nelze to vzít zpět!</string>
<string name="delete_group_for_self_cannot_undo_warning">Skupina bude smazána pro vás - toto nelze vzít zpět!</string>
<string name="button_leave_group">Opustit skupinu</string>
<string name="button_edit_group_profile">Upravit profil skupiny</string>
<string name="group_link">Odkaz na skupinu</string>
<string name="create_group_link">Vytvořit odkaz na skupinu</string>
<string name="delete_link">Smazat odkaz</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud ji později odstraníte.</string>
<string name="error_creating_link_for_group">Chyba při vytváření odkazu skupiny</string>
<string name="error_deleting_link_for_group">Chyba při odstraňování odkazu skupiny</string>
<string name="only_group_owners_can_change_prefs">Předvolby skupiny mohou měnit pouze vlastníci skupiny.</string>
<string name="section_title_for_console">PRO KONSOLE</string>
<string name="info_row_local_name">Místní název</string>
<string name="info_row_database_id">ID databáze</string>
<string name="button_remove_member">Odstranit člena</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Člen bude odstraněn ze skupiny - toto nelze vzít zpět!</string>
<string name="remove_member_confirmation">Odstranit</string>
<string name="role_in_group">Role</string>
<string name="change_role">Změnit roli</string>
<string name="change_verb">Změnit</string>
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na \"%s\". Člen obdrží novou pozvánku.</string>
<string name="error_changing_role">Chyba při změně role</string>
<string name="conn_level_desc_direct">přímo</string>
<string name="sending_via">Odesílání přes</string>
<string name="network_status">Stav sítě</string>
<string name="switch_receiving_address">Přepínač přijímací adresy</string>
<string name="group_is_decentralized">Skupina je plně decentralizovaná - je viditelná pouze pro členy.</string>
<string name="group_unsupported_incognito_main_profile_sent">Zde není podporován režim inkognito - členům skupiny bude zaslán váš hlavní profil.</string>
<string name="save_group_profile">Uložení profilu skupiny</string>
<string name="network_options_reset_to_defaults">Obnovení výchozího nastavení</string>
<string name="network_option_tcp_connection_timeout">Časový limit připojení TCP</string>
<string name="network_option_protocol_timeout">Časový limit protokolu</string>
<string name="network_option_ping_interval">Interval PING</string>
<string name="network_option_ping_count">Počet PING</string>
<string name="network_option_enable_tcp_keep_alive">Povolit TCP keep-alive</string>
<string name="update_network_settings_confirmation">Aktualizovat</string>
<string name="users_delete_question">Smazat profil chatu\?</string>
<string name="users_delete_profile_for">Smazat profil chatu pro</string>
<string name="users_delete_with_connections">Profil a připojení k serveru</string>
<string name="users_delete_data_only">Pouze lokální profilová data</string>
<string name="incognito_random_profile_from_contact_description">Náhodný profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz.</string>
<string name="incognito_info_protects">Režim inkognito chrání soukromí vašeho hlavního profilového jména a obrázku - pro každý nový kontakt je vytvořen nový náhodný profil.</string>
<string name="incognito_info_find">Chcete-li najít profil použitý pro inkognito připojení, klepněte na název kontaktu nebo skupiny v horní části chatu.</string>
<string name="theme_light">Světlo</string>
<string name="theme_dark">Tmavý</string>
<string name="theme">Téma</string>
<string name="chat_preferences_contact_allows">Kontakt povoluje</string>
<string name="chat_preferences_on">na</string>
<string name="chat_preferences_off">vypnuto</string>
<string name="chat_preferences">Předvolby chatu</string>
<string name="contact_preferences">Předvolby kontaktů</string>
<string name="group_preferences">Předvolby skupiny</string>
<string name="direct_messages">Přímé zprávy</string>
<string name="full_deletion">Smazat pro všechny</string>
<string name="feature_enabled">povoleno</string>
<string name="feature_enabled_for_you">povoleno pro vás</string>
<string name="feature_off">vypnuto</string>
<string name="prohibit_sending_disappearing_messages">Zakázat zasílání mizejících zpráv.</string>
<string name="contacts_can_mark_messages_for_deletion">Kontakty mohou označit zprávy ke smazání; vy je budete moci zobrazit.</string>
<string name="prohibit_sending_voice_messages">Zakázat odesílání hlasových zpráv.</string>
<string name="only_you_can_send_disappearing">Mizící zprávy můžete odesílat pouze vy.</string>
<string name="disappearing_prohibited_in_this_chat">Mizící zprávy jsou v tomto chatu zakázány.</string>
<string name="only_your_contact_can_delete">Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání).</string>
<string name="both_you_and_your_contact_can_send_voice">Hlasové zprávy můžete posílat vy i váš kontakt.</string>
<string name="only_you_can_send_voice">Hlasové zprávy můžete posílat pouze vy.</string>
<string name="only_your_contact_can_send_voice">Hlasové zprávy může odesílat pouze váš kontakt.</string>
<string name="voice_prohibited_in_this_chat">Hlasové zprávy jsou v tomto chatu zakázány.</string>
<string name="prohibit_sending_disappearing">Zakázat posílání mizejících zpráv.</string>
<string name="prohibit_message_deletion">Zakázat nevratné mazání zpráv.</string>
<string name="prohibit_sending_voice">Zakázat odesílání hlasových zpráv.</string>
<string name="group_members_can_send_disappearing">Členové skupiny mohou posílat mizející zprávy.</string>
<string name="disappearing_messages_are_prohibited">Mizící zprávy jsou v této skupině zakázány.</string>
<string name="group_members_can_send_dms">Členové skupiny mohou posílat přímé zprávy.</string>
<string name="direct_messages_are_prohibited_in_chat">Přímé zprávy mezi členy jsou v této skupině zakázány.</string>
<string name="group_members_can_delete">Členové skupiny mohou nevratně mazat odeslané zprávy.</string>
<string name="message_deletion_prohibited_in_chat">Nevratné mazání zpráv je v této skupině zakázáno.</string>
<string name="group_members_can_send_voice">Členové skupiny mohou posílat hlasové zprávy.</string>
<string name="voice_messages_are_prohibited">Hlasové zprávy jsou v této skupině zakázány.</string>
<string name="delete_after">Smazat po</string>
<string name="ttl_month">\"%d měsíc</string>
<string name="ttl_months">\"%d měsíců</string>
<string name="ttl_day">\"%d den</string>
<string name="ttl_days">\"%d dnů</string>
<string name="ttl_week">\"%d týden</string>
<string name="ttl_weeks">\"%d týdnů</string>
<string name="ttl_w">\"%dw</string>
<string name="feature_offered_item">nabízeno %s</string>
<string name="feature_cancelled_item">zrušeno %s</string>
<string name="whats_new">Co je nového</string>
<string name="new_in_version">Novinky v %s</string>
<string name="v4_2_auto_accept_contact_requests">Automatické přijímání žádostí o kontakt</string>
<string name="v4_2_auto_accept_contact_requests_desc">S volitelnou uvítací zprávou.</string>
<string name="v4_3_voice_messages_desc">Max. 40 sekund, přijímá se okamžitě.</string>
<string name="v4_5_private_filenames">Soukromé názvy souborů</string>
<string name="v4_5_private_filenames_descr">Pro ochranu časového pásma, obrazové/hlasové soubory používají UTC.</string>
<string name="v4_5_reduced_battery_usage">Snížení spotřeby baterie</string>
<string name="v4_5_reduced_battery_usage_descr">Další vylepšení se chystají již brzy!</string>
<string name="v4_5_italian_interface">Italské rozhraní</string>
<string name="v4_5_italian_interface_descr">Díky uživatelům - přispívejte prostřednictvím Weblate!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Budete připojeni, až bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později!</string>
<string name="your_contact_address">Vaše kontaktní adresa</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Váš profil v chatu bude odeslán vašemu kontaktu</string>
<string name="your_chat_profiles_stored_locally">Vaše profily v chatu jsou uloženy lokálně, pouze ve vašem zařízení.</string>
<string name="your_chats">Vaše konverzace</string>
</resources>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,969 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="thousand_abbreviation">k</string>
<string name="connect_via_contact_link">Se connecter via le lien du contact \?</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="profile_will_be_sent_to_contact_sending_link">Votre profil va être envoyé au contact qui vous a envoyé ce lien.</string>
<string name="you_will_join_group">Vous allez rejoindre le groupe correspondant à ce lien et être mis en relation avec les autres membres du groupe.</string>
<string name="connect_via_link_verb">Se connecter</string>
<string name="connect_via_group_link">Se connecter via le lien du groupe \?</string>
<string name="connect_via_invitation_link">Se connecter via un lien d\'invitation \?</string>
<string name="server_error">erreur</string>
<string name="server_connecting">connexion</string>
<string name="server_connected">connecté</string>
<string name="display_name_connection_established">connexion établie</string>
<string name="display_name_invited_to_connect">invité à se connecter</string>
<string name="simplex_link_invitation">Invitation unique SimpleX</string>
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode">Liens SimpleX</string>
<string name="simplex_link_mode_description">Description</string>
<string name="error_deleting_contact">Erreur lors de la suppression du contact</string>
<string name="error_joining_group">Erreur lors de la liaison avec le groupe</string>
<string name="sender_cancelled_file_transfer">L\'expéditeur a annulé le transfert de fichiers.</string>
<string name="deleted_description">supprimé</string>
<string name="marked_deleted_description">supprimé</string>
<string name="unknown_message_format">format de message inconnu</string>
<string name="display_name_connecting">connexion…</string>
<string name="description_you_shared_one_time_link_incognito">vous avez partagé un lien unique en incognito</string>
<string name="description_via_group_link">via le lien de groupe</string>
<string name="description_via_contact_address_link">via le lien d\'adresse du contact</string>
<string name="description_via_contact_address_link_incognito">mode incognito via le lien d\'adresse du contact</string>
<string name="simplex_link_group">Lien de groupe SimpleX</string>
<string name="simplex_link_mode_browser">Via navigateur</string>
<string name="simplex_link_mode_browser_warning">Ouvrir le lien dans le navigateur peut réduire la confidentialité et la sécurité de la connexion. Les liens SimpleX non fiables seront en rouge.</string>
<string name="network_error_desc">Vérifiez votre connexion réseau avec <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> et réessayez.</string>
<string name="error_receiving_file">Erreur lors de la réception du fichier</string>
<string name="sender_may_have_deleted_the_connection_request">L\'expéditeur a peut-être supprimé la demande de connexion.</string>
<string name="connected_to_server_to_receive_messages_from_contact">Vous êtes connecté·e au serveur utilisé pour recevoir les messages de ce contact.</string>
<string name="sending_files_not_yet_supported">l\'envoi de fichiers n\'est pas encore supporté</string>
<string name="sender_you_pronoun">vous</string>
<string name="description_via_group_link_incognito">mode incognito via le lien de groupe</string>
<string name="simplex_link_contact">Adresse de contact SimpleX</string>
<string name="trying_to_connect_to_server_to_receive_messages">Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact.</string>
<string name="receiving_files_not_yet_supported">la réception de fichiers n\'est pas encore supportée</string>
<string name="connection_local_display_name">connexion <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="description_you_shared_one_time_link">vous avez partagé un lien unique</string>
<string name="description_via_one_time_link">via un lien unique</string>
<string name="description_via_one_time_link_incognito">mode incognito via un lien unique</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs SMP sont au bon format, séparées par des lignes et ne sont pas dupliquées.</string>
<string name="error_setting_network_config">Erreur lors de la mise à jour de la configuration réseau</string>
<string name="error_creating_address">Erreur lors de la création de l\'adresse</string>
<string name="contact_already_exists">Contact déjà existant</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre.</string>
<string name="connection_error">Erreur de connexion</string>
<string name="error_adding_members">Erreur lors de l\'ajout de membre·s</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="invalid_message_format">format de message invalide</string>
<string name="simplex_link_mode_full">Lien entier</string>
<string name="error_saving_smp_servers">Erreur lors de la sauvegarde des serveurs SMP</string>
<string name="cannot_receive_file">Impossible de recevoir le fichier</string>
<string name="invalid_connection_link">Lien de connection invalide</string>
<string name="connection_timeout">Délai de connexion</string>
<string name="error_sending_message">Erreur lors de l\'envoi du message</string>
<string name="you_are_already_connected_to_vName_via_this_link">Vous êtes déjà connecté à <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="connection_error_auth">Erreur de connexion (AUTH)</string>
<string name="connection_error_auth_desc">A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s\'agir d\'un bug - veuillez le signaler.
\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d\'une connexion réseau stable.</string>
<string name="error_accepting_contact_request">Erreur de validation de la demande de contact</string>
<string name="error_deleting_group">Erreur lors de la suppression du groupe</string>
<string name="error_deleting_contact_request">Erreur lors de la suppression du contact</string>
<string name="error_deleting_pending_contact_connection">Erreur lors de la suppression de la connexion en attente</string>
<string name="error_changing_address">Erreur de changement d\'adresse</string>
<string name="error_smp_test_failed_at_step">Échec du test à l\'étape %s.</string>
<string name="error_smp_test_certificate">Il est possible que l\'empreinte du certificat dans l\'adresse du serveur soit incorrecte</string>
<string name="smp_server_test_connect">Se connecter</string>
<string name="smp_server_test_create_queue">Créer une file d\'attente</string>
<string name="smp_server_test_secure_queue">File d\'attente sécurisée</string>
<string name="smp_server_test_delete_queue">Supprimer la file d\'attente</string>
<string name="smp_server_test_disconnect">Se déconnecter</string>
<string name="icon_descr_instant_notifications">Notifications instantanées</string>
<string name="service_notifications">Notifications instantanées !</string>
<string name="service_notifications_disabled">Les notifications instantanées sont désactivées !</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Il peut être désactivé via les paramètres</b> - les notifications seront toujours affichées lorsque l\'application est en cours d\'exécution.</string>
<string name="turning_off_service_and_periodic">L\'optimisation de la batterie est active et désactive le service de fond et les demandes périodiques de nouveaux messages. Vous pouvez les réactiver via les paramètres.</string>
<string name="periodic_notifications">Notifications périodiques</string>
<string name="periodic_notifications_disabled">Les notifications périodiques sont désactivées !</string>
<string name="enter_passphrase_notification_title">Une phrase secrète est nécessaire</string>
<string name="turn_off_battery_optimization">Pour l\'utiliser, veuillez <b>désactiver l\'optimisation de la batterie</b> pour <xliff:g id="appName">SimpleX</xliff:g> dans la prochaine fenêtre de dialogue. Sinon, les notifications seront désactivées.</string>
<string name="error_smp_test_server_auth">Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe</string>
<string name="periodic_notifications_desc">L\'application récupère périodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les données de votre appareil ne sont pas envoyées aux serveurs.</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Pour protéger votre vie privée, au lieu des notifications push, l\'application possède un <b><xliff:g id="appName">SimpleX</xliff:g> service de fond</b> - il utilise quelques pour cent de la batterie par jour.</string>
<string name="hide_notification">Cacher</string>
<string name="settings_notification_preview_mode_title">Montrer l\'aperçu</string>
<string name="notification_preview_mode_contact">Nom du contact</string>
<string name="notification_preview_somebody">Contact masqué :</string>
<string name="notification_preview_new_message">nouveau message</string>
<string name="notification_new_contact_request">Nouvelle demande de contact</string>
<string name="notification_contact_connected">Connecté</string>
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
<string name="la_notice_turn_on">Activer</string>
<string name="auth_simplex_lock_turned_on">SimpleX Lock activé</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Il vous sera demandé de vous authentifier lorsque vous démarrez ou reprenez l\'application après 30 secondes en arrière-plan.</string>
<string name="auth_unlock">Déverrouiller</string>
<string name="auth_enable_simplex_lock">Activer SimpleX Lock</string>
<string name="auth_disable_simplex_lock">Désactiver SimpleX Lock</string>
<string name="auth_unavailable">Authentification indisponible</string>
<string name="auth_device_authentication_is_disabled_turning_off">L\'authentification de l\'appareil est désactivée. Désactivation de SimpleX Lock.</string>
<string name="auth_open_chat_console">Ouvrir la console du chat</string>
<string name="message_delivery_error_title">Erreur de distribution du message</string>
<string name="message_delivery_error_desc">Il est fort probable que ce contact ait supprimé la connexion avec vous.</string>
<string name="reply_verb">Répondre</string>
<string name="share_verb">Partager</string>
<string name="copy_verb">Copier</string>
<string name="delete_verb">Supprimer</string>
<string name="save_verb">Sauvegarder</string>
<string name="edit_verb">Modifier</string>
<string name="reveal_verb">Révéler</string>
<string name="hide_verb">Cacher</string>
<string name="allow_verb">Autoriser</string>
<string name="delete_message__question">Supprimer le message\?</string>
<string name="for_me_only">Supprimer pour moi</string>
<string name="your_chats">Vos chats</string>
<string name="notification_preview_mode_message">Texte du message</string>
<string name="notification_preview_mode_hidden">Caché</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Pour protéger vos informations, activez la fonction SimpleX Lock.
\nVous serez invité à confirmer l\'authentification avant que cette fonction ne soit activée.</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">L\'authentification de l\'appareil n\'est pas activée. Vous pouvez activer SimpleX Lock via Paramètres, une fois que vous avez activé l\'authentification de l\'appareil.</string>
<string name="database_initialization_error_desc">La base de données ne fonctionne pas correctement. Appuyez ici pour en savoir plus.</string>
<string name="ntf_channel_calls">Appels SimpleX Chat</string>
<string name="ntf_channel_messages">Messages SimpleX Chat</string>
<string name="settings_notifications_mode_title">Service de notification</string>
<string name="notifications_mode_periodic">Lancer périodiquement</string>
<string name="notifications_mode_off">Exécuter lorsque lapp est ouverte</string>
<string name="notifications_mode_service">Toujours activé</string>
<string name="failed_to_parse_chat_title">Échec du chargement du chat</string>
<string name="failed_to_parse_chats_title">Échec du chargement des chats</string>
<string name="contact_developers">Veuillez mettre à jour lapp et contacter les développeurs.</string>
<string name="simplex_service_notification_text">Récupération des messages…</string>
<string name="settings_notification_preview_title">Aperçu de notification</string>
<string name="database_initialization_error_title">Échec dinitialisation de la base de données</string>
<string name="enter_passphrase_notification_desc">Pour recevoir des notifications, veuillez entrer la phrase secrète de la base de données</string>
<string name="simplex_service_notification_title">service <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="this_text_is_available_in_settings">Ce texte est disponible dans les paramètres</string>
<string name="group_preview_join_as">rejoindre en tant que %s</string>
<string name="group_preview_you_are_invited">vous êtes invité·e au groupe</string>
<string name="chat_with_developers">Discuter avec les développeurs</string>
<string name="tap_to_start_new_chat">Appuyez ici pour démarrer une nouvelle discussion</string>
<string name="you_have_no_chats">Vous n\'avez aucune discussion</string>
<string name="images_limit_title">Trop dimages !</string>
<string name="share_file">Partager le fichier…</string>
<string name="attach">Attacher</string>
<string name="icon_descr_cancel_image_preview">Annuler laperçu dimage</string>
<string name="icon_descr_cancel_file_preview">Annuler laperçu du fichier</string>
<string name="icon_descr_sent_msg_status_send_failed">échec denvoi</string>
<string name="icon_descr_received_msg_status_unread">non lu</string>
<string name="welcome">Bienvenue !</string>
<string name="contact_connection_pending">connexion…</string>
<string name="group_connection_pending">connexion…</string>
<string name="share_message">Partager le message…</string>
<string name="share_image">Partager limage…</string>
<string name="images_limit_desc">Envoi de 10 images en même temps maximum</string>
<string name="personal_welcome">Bienvenue <xliff:g>%1$s</xliff:g> !</string>
<string name="notifications_mode_periodic_desc">Vérifie les nouveaux messages toutes les 10 minutes pendant 1 minute au maximum.</string>
<string name="notification_preview_mode_contact_desc">Afficher uniquement le contact</string>
<string name="for_everybody">Pour tous</string>
<string name="icon_descr_sent_msg_status_sent">envoyé</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">envoi non autorisé</string>
<string name="icon_descr_context">Icône contextuelle</string>
<string name="image_descr">Image</string>
<string name="image_decoding_exception_desc">L\'image ne peut pas être décodée. Veuillez essayer une autre image ou contacter les développeurs.</string>
<string name="icon_descr_waiting_for_image">En attente de l\'image</string>
<string name="icon_descr_asked_to_receive">Demandé à recevoir l\'image</string>
<string name="icon_descr_image_snd_complete">Image envoyée</string>
<string name="waiting_for_image">En attente de l\'image</string>
<string name="image_saved">Image enregistrée dans la phototèque</string>
<string name="icon_descr_file">Fichier</string>
<string name="large_file">Fichier trop lourd !</string>
<string name="file_saved">Fichier sauvegardé</string>
<string name="file_not_found">Fichier introuvable</string>
<string name="error_saving_file">Erreur lors de la sauvegarde du fichier</string>
<string name="delete_contact_question">Supprimer le contact \?</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Le contact et tous les messages seront supprimés - impossible de revenir en arrière !</string>
<string name="button_delete_contact">Supprimer le contact</string>
<string name="icon_descr_server_status_connected">Connecté</string>
<string name="icon_descr_send_message">Envoyer un message</string>
<string name="switch_receiving_address_question">Changement d\'adresse de réception \?</string>
<string name="icon_descr_record_voice_message">Enregistrer un message vocal</string>
<string name="allow_voice_messages_question">Autoriser les messages vocaux \?</string>
<string name="you_need_to_allow_to_send_voice">Vous devez autoriser votre contact à envoyer des messages vocaux pour pouvoir en envoyer.</string>
<string name="voice_messages_prohibited">Messages vocaux interdits !</string>
<string name="ask_your_contact_to_enable_voice">Veuillez demander à votre contact de permettre l\'envoi de messages vocaux.</string>
<string name="cancel_verb">Annuler</string>
<string name="only_group_owners_can_enable_voice">Seuls les propriétaires de groupes peuvent activer les messages vocaux.</string>
<string name="back">Retour</string>
<string name="no_details">aucun détail</string>
<string name="add_contact">Lien d\'invitation unique</string>
<string name="copied">Copié dans le presse-papiers</string>
<string name="share_one_time_link">Créer un lien d\'invitation unique</string>
<string name="add_contact_or_create_group">Commencer une nouvelle discussion</string>
<string name="connect_via_link_or_qr">Se connecter via un lien / code QR</string>
<string name="scan_QR_code">Scanner un code QR</string>
<string name="to_share_with_your_contact">(à partager avec votre contact)</string>
<string name="create_group">Créer un groupe secret</string>
<string name="from_gallery_button">Depuis la Phototèque</string>
<string name="choose_file">Choisir le fichier</string>
<string name="to_start_a_new_chat_help_header">Pour démarrer une nouvelle discussion</string>
<string name="chat_help_tap_button">Appuyez sur le bouton</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scanner un code QR</b> : pour vous connecter à votre contact qui vous montre un code QR.</string>
<string name="to_connect_via_link_title">Pour se connecter via un lien</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si vous avez reçu un lien d\'invitation <xliff:g id="appName">SimpleX Chat</xliff:g>, vous pouvez l\'ouvrir dans votre navigateur :</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 bureau : scanner le code QR affiché depuis l\'app, via <b>Scanner le code QR</b>.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobile : appuyez sur <b>Ouvrir dans l\'application</b>, puis appuyez sur <b>se connecter</b> dans l\'app.</string>
<string name="accept_contact_incognito_button">Accepter en incognito</string>
<string name="reject_contact_button">Rejeter</string>
<string name="clear_chat_question">Effacer la conversation \?</string>
<string name="clear_chat_warning">Tous les messages seront supprimés - impossible de revenir en arrière ! Les messages seront supprimés UNIQUEMENT pour vous.</string>
<string name="clear_chat_menu_action">Effacer</string>
<string name="delete_contact_menu_action">Supprimer</string>
<string name="delete_group_menu_action">Supprimer</string>
<string name="mark_read">Marquer comme lu</string>
<string name="mark_unread">Marquer non lu</string>
<string name="set_contact_name">Définir le nom du contact</string>
<string name="you_invited_your_contact">Vous avez invité votre contact</string>
<string name="you_accepted_connection">Vous avez accepté la connexion</string>
<string name="delete_pending_connection__question">Supprimer la connexion en attente \?</string>
<string name="connection_you_accepted_will_be_cancelled">La connexion que vous avez acceptée sera annulée !</string>
<string name="alert_title_contact_connection_pending">Le contact n\'est pas encore connecté !</string>
<string name="icon_descr_close_button">Bouton fermer</string>
<string name="image_descr_profile_image">image de profil</string>
<string name="image_descr_link_preview">image d\'aperçu du lien</string>
<string name="icon_descr_cancel_link_preview">annuler l\'aperçu du lien</string>
<string name="icon_descr_settings">Paramètres</string>
<string name="icon_descr_address">Adresse <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_help">aide</string>
<string name="icon_descr_simplex_team">Équipe <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_more_button">Plus</string>
<string name="show_QR_code">Afficher le code QR</string>
<string name="invalid_QR_code">Code QR invalide</string>
<string name="this_QR_code_is_not_a_link">Ce code QR n\'est pas un lien !</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Vous serez connecté·e lorsque votre demande de connexion sera acceptée, veuillez attendre ou vérifier plus tard !</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Vous serez connecté·e lorsque l\'appareil de votre contact sera en ligne, veuillez attendre ou vérifier plus tard !</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Votre contact peut scanner le code QR depuis l\'app.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Votre profil de chat sera envoyé
\nà votre contact</string>
<string name="share_invitation_link">Partager le lien d\'invitation</string>
<string name="your_profile_will_be_sent">Votre profil de chat sera envoyé à votre contact</string>
<string name="paste_button">Coller</string>
<string name="this_string_is_not_a_connection_link">Cette chaîne n\'est pas un lien de connexion !</string>
<string name="you_can_also_connect_by_clicking_the_link">Vous pouvez aussi vous connecter en cliquant sur le lien. Si il s\'ouvre dans le navigateur, cliquez sur <b>Ouvrir dans l\'app mobile</b>.</string>
<string name="create_one_time_link">Créer un lien d\'invitation unique</string>
<string name="text_field_set_contact_placeholder">Définir le nom du contact…</string>
<string name="icon_descr_server_status_disconnected">Déconnecté</string>
<string name="icon_descr_server_status_error">Erreur</string>
<string name="icon_descr_server_status_pending">En attente</string>
<string name="accept_connection_request__question">Accepter la demande de connexion \?</string>
<string name="clear_verb">Effacer</string>
<string name="clear_chat_button">Effacer la conversation</string>
<string name="paste_connection_link_below_to_connect">Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact.</string>
<string name="connect_via_link">Se connecter via un lien</string>
<string name="clear_verification">Retirer la vérification</string>
<string name="one_time_link">Lien d\'invitation unique</string>
<string name="your_contact_address">Votre adresse de contact</string>
<string name="scan_code">Scanner le code</string>
<string name="incorrect_code">Code de sécurité incorrect !</string>
<string name="security_code">Code de sécurité</string>
<string name="mark_code_verified">Marquer comme vérifié</string>
<string name="view_security_code">Afficher le code de sécurité</string>
<string name="verify_security_code">Vérifier le code de sécurité</string>
<string name="confirm_verb">Confirmer</string>
<string name="reset_verb">Réinitialisation</string>
<string name="ok">OK</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scanner ou coller depuis le presse-papiers)</string>
<string name="only_stored_on_members_devices">(uniquement stocké par les membres du groupe)</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Votre contact a besoin d\'être en ligne pour completer la connexion.
\nVous pouvez annuler la connexion et supprimer le contact (et réessayer plus tard avec un autre lien).</string>
<string name="contact_wants_to_connect_with_you">veut établir une connexion !</string>
<string name="icon_descr_profile_image_placeholder">image de profil (placeholder)</string>
<string name="image_descr_qr_code">Code QR</string>
<string name="image_descr_simplex_logo">Logo <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_email">E-mail</string>
<string name="connect_button">Se connecter</string>
<string name="notifications_mode_off_desc">L\'application peut recevoir des notifications uniquement lorsqu\'elle est en cours d\'exécution, aucun service d\'arrière-plan ne sera lancé.</string>
<string name="notifications_mode_service_desc">Le service d\'arrière-plan fonctionne en permanence. Les notifications s\'affichent dès que les messages sont disponibles.</string>
<string name="notification_preview_mode_message_desc">Afficher le contact et le message</string>
<string name="notification_display_mode_hidden_desc">Masquer le contact et le message</string>
<string name="auth_log_in_using_credential">Connectez-vous en utilisant votre identifiant</string>
<string name="auth_confirm_credential">Confirmez vos identifiants</string>
<string name="auth_stop_chat">Arrêter le chat</string>
<string name="delete_message_cannot_be_undone_warning">Le message sera supprimé - impossible de revenir en arrière !</string>
<string name="delete_message_mark_deleted_warning">Le message sera marqué comme supprimé. Le·s destinataire·s pourrai·ent révéler ce message.</string>
<string name="icon_descr_edited">modifié</string>
<string name="image_will_be_received_when_contact_is_online">L\'image sera reçue quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tard!</string>
<string name="image_decoding_exception_title">Erreur de décodage</string>
<string name="contact_sent_large_file">Votre contact a envoyé un fichier dont la taille est supérieure à la taille maximale actuellement prise en charge (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
<string name="waiting_for_file">En attente du fichier</string>
<string name="voice_message">Message vocal</string>
<string name="toast_permission_denied">Autorisation refusée !</string>
<string name="use_camera_button">Utiliser l\'Appareil photo</string>
<string name="thank_you_for_installing_simplex">Merci d\'avoir installé <xliff:g id="appNameFull">SimpleX Chat</xliff:g> !</string>
<string name="you_can_connect_to_simplex_chat_founder">Vous pouvez <font color="#0088ff">vous connecter aux développeurs de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour leur poser des questions et recevoir des réponses :</font>.</string>
<string name="above_then_preposition_continuation">ci-dessus, puis :</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Ajouter un nouveau contact</b> : afin de créer un code QR à usage unique pour votre contact.</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si vous choisissez de la rejeter, l\'expéditeur·rice NE sera PAS notifié·e.</string>
<string name="accept_contact_button">Accepter</string>
<string name="mute_chat">Muet</string>
<string name="unmute_chat">Démute</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Le contact avec lequel vous avez partagé ce lien NE pourra PAS se connecter !</string>
<string name="invalid_contact_link">Lien invalide !</string>
<string name="this_link_is_not_a_valid_connection_link">Ce lien n\'est pas un lien de connexion valide !</string>
<string name="connection_request_sent">Demande de connexion envoyée !</string>
<string name="file_will_be_received_when_contact_is_online">Le fichier sera reçu quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tard!</string>
<string name="voice_message_send_text">Message vocal…</string>
<string name="maximum_supported_file_size">La taille maximale supportés des fichiers actuellement est de <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="voice_message_with_duration">Message vocal (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="notifications">Notifications</string>
<string name="switch_receiving_address_desc">Cette fonctionnalité est expérimentale ! Elle ne fonctionnera que si l\'autre client a la version 4.2 installée. Vous devriez voir le message dans la conversation une fois le changement d\'adresse effectué. Vérifiez que vous pouvez toujours recevoir des messages de ce contact (ou membre du groupe).</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Vous serez connecté·e au groupe lorsque l\'appareil de l\'hôte sera en ligne, veuillez attendre ou vérifier plus tard !</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Si vous ne pouvez pas vous rencontrer en personne, <b>montrez le code QR lors d\'un appel vidéo</b>, ou partagez le lien.</string>
<string name="scan_code_from_contacts_app">Scannez le code de sécurité depuis l\'application de votre contact.</string>
<string name="to_verify_compare">Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils.</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Si vous ne pouvez pas vous rencontrer en personne, vous pouvez <b>scanner un code QR lors d\'un appel vidéo</b>, ou votre contact peut partager un lien d\'invitation.</string>
<string name="smp_servers_add">Ajouter un serveur…</string>
<string name="markdown_in_messages">Markdown dans les messages</string>
<string name="smp_servers_preset_add">Ajouter des serveurs prédéfinis</string>
<string name="use_simplex_chat_servers__question">Utiliser les serveurs <xliff:g id="appNameFull">SimpleX Chat</xliff:g> \?</string>
<string name="smp_servers_delete_server">Supprimer le serveur</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.</string>
<string name="network_enable_socks_info">Accéder aux serveurs via un proxy SOCKS sur le port 9050 \? Le proxy doit être démarré avant d\'activer cette option.</string>
<string name="network_use_onion_hosts">Utiliser les hôtes .onions</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Les hôtes .onion seront nécessaires pour la connexion.</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Vous contrôlez par quel·s serveur·s vous pouvez <b>transmettre</b> ainsi que par quel·s serveur·s vous pouvez <b>recevoir</b> les messages de vos contacts.</string>
<string name="your_settings">Vos paramètres</string>
<string name="chat_lock">SimpleX Lock</string>
<string name="chat_console">Console du chat</string>
<string name="smp_servers">Serveurs SMP</string>
<string name="smp_servers_test_servers">Tester les serveurs</string>
<string name="smp_servers_save">Sauvegarder les serveurs</string>
<string name="smp_servers_scan_qr">Scanner un code QR de serveur</string>
<string name="smp_servers_use_server">Utiliser ce serveur</string>
<string name="smp_servers_use_server_for_new_conn">Utiliser pour les nouvelles connexions</string>
<string name="smp_servers_add_to_another_device">Ajouter à un autre appareil</string>
<string name="install_simplex_chat_for_terminal">Installer <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour terminal</string>
<string name="star_on_github">Star sur GitHub</string>
<string name="contribute">Contribuer</string>
<string name="rate_the_app">Évaluer l\'app</string>
<string name="your_SMP_servers">Vos serveurs SMP</string>
<string name="how_to_use_your_servers">Comment utiliser vos serveurs</string>
<string name="saved_ICE_servers_will_be_removed">Les serveurs WebRTC ICE sauvegardés seront supprimés.</string>
<string name="your_ICE_servers">Vos serveurs ICE</string>
<string name="configure_ICE_servers">Configurer les serveurs ICE</string>
<string name="network_settings">Paramètres réseau avancés</string>
<string name="network_settings_title">Paramètres réseau</string>
<string name="network_socks_toggle">Utiliser un proxy SOCKS (port 9050)</string>
<string name="network_enable_socks">Utiliser un proxy SOCKS \?</string>
<string name="network_disable_socks">Utiliser une connexion Internet directe \?</string>
<string name="network_disable_socks_info">Si vous confirmez, les serveurs de messagerie seront en mesure de voir votre adresse IP, votre fournisseur ainsi que les serveurs auxquels vous vous connectez.</string>
<string name="network_use_onion_hosts_no">Non</string>
<string name="network_use_onion_hosts_required">Requis</string>
<string name="network_use_onion_hosts_prefer_desc">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
<string name="appearance_settings">Apparence</string>
<string name="create_address">Créer une adresse</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Vous pouvez partager votre adresse sous forme de lien ou de code QR - n\'importe qui pourra se connecter à vous. Vous ne perdrez pas vos contacts si vous la supprimez par la suite.</string>
<string name="your_current_profile">Votre profil de chat</string>
<string name="edit_image">Modifier l\'image</string>
<string name="save_and_notify_contacts">Sauvegarder et notifier les contacts</string>
<string name="save_and_notify_group_members">Sauvegarder et en informer les membres du groupe</string>
<string name="your_profile_is_stored_on_your_device">Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.</string>
<string name="profile_is_only_shared_with_your_contacts">Le profil n\'est partagé qu\'avec vos contacts.</string>
<string name="display_name_cannot_contain_whitespace">Le nom d\'affichage ne peut pas contenir d\'espace.</string>
<string name="full_name_optional__prompt">Nom complet (optionnel)</string>
<string name="create_profile_button">Créer</string>
<string name="about_simplex">À propos de SimpleX</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Vous pouvez utiliser le format markdown pour mettre en forme les messages :</string>
<string name="bold">gras</string>
<string name="italic">italique</string>
<string name="strikethrough">barré</string>
<string name="callstatus_accepted">appel accepté</string>
<string name="callstatus_connecting">connexion à l\'appel…</string>
<string name="callstatus_error">erreur d\'appel</string>
<string name="callstate_received_answer">réponse reçu…</string>
<string name="callstate_received_confirmation">confimation reçu…</string>
<string name="callstate_connecting">connexion…</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocole et code open-source n\'importe qui peut heberger un serveur.</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Pour protéger votre vie privée, au lieu d\'IDs utilisés par toutes les autres plateformes, <xliff:g id="appName">SimpleX</xliff:g> possède des IDs pour les queues de messages, distinctes pour chacun de vos contacts.</string>
<string name="read_more_in_github">Plus d\'informations sur notre GitHub.</string>
<string name="paste_the_link_you_received">Coller le lien reçu</string>
<string name="use_chat">Utiliser le chat</string>
<string name="onboarding_notifications_mode_title">Notifications privées</string>
<string name="onboarding_notifications_mode_subtitle">Peut être modifié ultérieurement via les paramètres.</string>
<string name="onboarding_notifications_mode_off">Quand l\'application fonctionne</string>
<string name="onboarding_notifications_mode_periodic">Périodique</string>
<string name="onboarding_notifications_mode_service">Instantanée</string>
<string name="onboarding_notifications_mode_off_desc"><b>Économie de batterie</b>. Vous recevrez des notifications uniquement lorsque l\'application est en cours d\'exécution, le service de fond ne sera PAS utilisé.</string>
<string name="about_simplex_chat">À propos de <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">Comment l\'utiliser</string>
<string name="markdown_help">Aide Markdown</string>
<string name="save_servers_button">Sauvegarder</string>
<string name="network_and_servers">Réseau et serveurs</string>
<string name="save_and_notify_contact">Sauvegarder et en informer les contacts</string>
<string name="exit_without_saving">Quitter sans sauvegarder</string>
<string name="callstatus_rejected">appel rejeté</string>
<string name="callstatus_in_progress">appel en cours</string>
<string name="callstatus_ended">appel terminé <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstate_starting">lancement…</string>
<string name="is_verified">%s est vérifié·e</string>
<string name="is_not_verified">%s n\'est pas vérifié·e</string>
<string name="your_simplex_contact_address">Votre adresse de contact <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="database_passphrase_and_export">Phrase secrète et exportation de la base de données</string>
<string name="chat_with_the_founder">Envoyez vos questions et idées</string>
<string name="send_us_an_email">Envoyez nous un e-mail</string>
<string name="smp_servers_preset_address">Adresse du serveur prédéfinie</string>
<string name="smp_servers_test_server">Tester le serveur</string>
<string name="smp_servers_test_failed">Échec du test du serveur !</string>
<string name="smp_servers_test_some_failed">Certains serveurs n\'ont pas réussi le test :</string>
<string name="smp_servers_enter_manually">Entrer un serveur manuellement</string>
<string name="smp_servers_preset_server">Serveur prédéfini</string>
<string name="smp_servers_your_server">Votre serveur</string>
<string name="smp_servers_your_server_address">Votre adresse de serveur</string>
<string name="smp_servers_invalid_address">Adresse de serveur invalide !</string>
<string name="smp_servers_check_address">Vérifiez l\'adresse du serveur et réessayez.</string>
<string name="using_simplex_chat_servers">Utilise les serveurs <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
<string name="how_to">Comment faire</string>
<string name="enter_one_ICE_server_per_line">Serveurs ICE (un par ligne)</string>
<string name="error_saving_ICE_servers">Erreur lors de la sauvegarde des serveurs ICE</string>
<string name="update_onion_hosts_settings_question">Mettre à jour le paramètre des hôtes .onion \?</string>
<string name="network_use_onion_hosts_prefer">Quand disponible</string>
<string name="network_use_onion_hosts_no_desc">Les hôtes .onion ne seront pas utilisés.</string>
<string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Les hôtes .onion ne seront pas utilisés.</string>
<string name="delete_address__question">Supprimer l\'adresse \?</string>
<string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connectés.</string>
<string name="share_link">Partager le lien</string>
<string name="delete_address">Supprimer l\'adresse</string>
<string name="contact_requests">Demandes de contact</string>
<string name="accept_requests">Accepter les demandes</string>
<string name="accept_automatically">Automatiquement</string>
<string name="section_title_welcome_message">MESSAGE DE BIENVENUE</string>
<string name="display_name__field">Nom affiché :</string>
<string name="full_name__field">Nom complet :</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts.
\n
\nLes serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
<string name="delete_image">Supprimer l\'image</string>
<string name="save_preferences_question">Sauvegarder les préférences \?</string>
<string name="you_control_your_chat">Vous maîtrisez vos discussions !</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plateforme de messagerie et d\'applications qui protège votre vie privée et votre sécurité.</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Nous ne stockons aucun de vos contacts ou messages (une fois délivrés) sur les serveurs.</string>
<string name="create_profile">Créer le profil</string>
<string name="display_name">Nom affiché</string>
<string name="how_to_use_markdown">Comment utiliser markdown</string>
<string name="a_plus_b">a + b</string>
<string name="colored">coloré</string>
<string name="secret">secret</string>
<string name="callstatus_calling">appel…</string>
<string name="callstatus_missed">appel manqué</string>
<string name="callstate_waiting_for_answer">en attente de réponse…</string>
<string name="callstate_waiting_for_confirmation">en attente de confirmation…</string>
<string name="callstate_connected">connecté</string>
<string name="callstate_ended">terminé</string>
<string name="next_generation_of_private_messaging">La nouvelle génération de messagerie privée</string>
<string name="privacy_redefined">La vie privée redéfinie</string>
<string name="first_platform_without_user_ids">La 1ère plateforme sans aucun identifiant d\'utilisateur privée par design.</string>
<string name="immune_to_spam_and_abuse">Protégé du spam et des abus</string>
<string name="people_can_connect_only_via_links_you_share">On ne peut se connecter à vous quavec les liens que vous partagez.</string>
<string name="decentralized">Décentralisé</string>
<string name="create_your_profile">Créez votre profil</string>
<string name="make_private_connection">Établir une connexion privée</string>
<string name="how_it_works">Comment ça fonctionne</string>
<string name="how_simplex_works">Comment <xliff:g id="appName">SimpleX</xliff:g> fonctionne</string>
<string name="many_people_asked_how_can_it_deliver">Beaucoup se demandent : <i>si <xliff:g id="appName">SimpleX</xliff:g> n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?</i></string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un <b>chiffrement de bout en bout à deux couches</b>.</string>
<string name="read_more_in_github_with_link">Pour en savoir plus, consultez notre <font color="#0088ff">GitHub repository</font>.</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Batterie peu utilisée</b>. Le service de fond vérifie les nouveaux messages toutes les 10 minutes. Vous risquez de manquer des appels et des messages urgents.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Batterie plus utilisée </b> ! Le service de fond est toujours en cours d\'exécution - les notifications s\'afficheront dès que les messages seront disponibles.</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> message⸱s manqué⸱s</string>
<string name="integrity_msg_bad_id">ID de message incorrecte</string>
<string name="settings_section_title_settings">PARAMÈTRES</string>
<string name="alert_text_skipped_messages_it_can_happen_when">C\'est possible quand :
\n1. Les messages expirent du serveur (après 30 jours si ils ne sont pas reçu).
\n2. Le serveur que vous utilisez pour recevoir les messages de ce contact a été mise à jour ou redémarré.
\n3. La connection est compromise.
\nVeuillez vous connecter aux développeurs via les Paramètres pour recevoir les mises à jour concernant les serveurs.
\nNous allons ajouter une redondance des serveurs pour éviter la perte de messages.</string>
<string name="icon_descr_call_rejected">Appel rejeté</string>
<string name="rcv_group_event_member_deleted">a retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_member_deleted">vous avez retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_invited_via_your_group_link">invité par votre lien de groupe</string>
<string name="snd_conn_event_switch_queue_phase_completed">vous avez changé d\'adresse</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Arrêtez le chat pour exporter, importer ou supprimer la base de données du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arrêté.</string>
<string name="enable_automatic_deletion_message">Cette action ne peut être annulée - les messages envoyés et reçus avant la date sélectionnée seront supprimés. Cela peut prendre plusieurs minutes.</string>
<string name="encrypted_with_random_passphrase">La base de données est chiffrée à l\'aide d\'une phrase secrète aléatoire, que vous pouvez modifier.</string>
<string name="restore_database">Restaurer la sauvegarde de la base de données</string>
<string name="restore_passphrase_not_found_desc">La phrase secrète n\'a pas été trouvée dans le Keystore, veuillez la saisir manuellement. Cela a pu se produire si vous avez restauré les données de l\'app à l\'aide d\'un outil de sauvegarde. Si ce n\'est pas le cas, veuillez contacter les développeurs.</string>
<string name="restore_database_alert_desc">Veuillez entrer le mot de passe précédent après avoir restauré la sauvegarde de la base de données. Cette action ne peut pas être annulée.</string>
<string name="database_restore_error">Erreur de restauration de la base de données</string>
<string name="archive_created_on_ts">Créé le <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="encrypted_video_call">appel vidéo (chiffrement de bout en bout)</string>
<string name="audio_call_no_encryption">appel audio (sans chiffrement)</string>
<string name="encrypted_audio_call">appel audio (chiffrement de bout en bout)</string>
<string name="accept">Accepter</string>
<string name="reject">Rejeter</string>
<string name="icon_descr_video_call">appel vidéo</string>
<string name="icon_descr_audio_call">appel audio</string>
<string name="accept_call_on_lock_screen">Accepter</string>
<string name="allow_accepting_calls_from_lock_screen">Activer les appels depuis l\'écran verrouillé via les Paramètres.</string>
<string name="open_verb">Ouvrir</string>
<string name="call_connection_via_relay">via relais</string>
<string name="icon_descr_hang_up">Raccrocher</string>
<string name="icon_descr_video_on">Vidéo ON</string>
<string name="icon_descr_video_off">Vidéo OFF</string>
<string name="icon_descr_call_progress">Appel en cours</string>
<string name="icon_descr_call_ended">Appel terminé</string>
<string name="your_privacy">Votre vie privée</string>
<string name="settings_section_title_device">APPAREIL</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="settings_developer_tools">Outils du développeur</string>
<string name="settings_section_title_icon">ICONE DE L\'APP</string>
<string name="your_chat_database">Votre base de données de chat</string>
<string name="run_chat_section">LANCER LE CHAT</string>
<string name="stop_chat_question">Arrêter le chat \?</string>
<string name="restart_the_app_to_use_imported_chat_database">Redémarrez l\'application pour utiliser la base de données de chat importée.</string>
<string name="chat_item_ttl_day">1 jour</string>
<string name="delete_messages">Supprimer les messages</string>
<string name="save_passphrase_in_keychain">Sauvegarder la phrase secrète dans le keystore</string>
<string name="database_encrypted">Base de données chiffrée !</string>
<string name="error_encrypting_database">Erreur lors du chiffrement de la base de données</string>
<string name="update_database">Mise à jour</string>
<string name="encrypt_database">Chiffrer</string>
<string name="enter_correct_current_passphrase">Veuillez entrer la phrase secrète actuelle correcte.</string>
<string name="database_is_not_encrypted">Votre base de données de chat n\'est pas chiffrée - définissez une phrase secrète pour la protéger.</string>
<string name="impossible_to_recover_passphrase"><b>Veuillez noter</b> : vous NE pourrez PAS récupérer ou modifier la phrase secrète si vous la perdez.</string>
<string name="keychain_allows_to_receive_ntfs">Le Keystore d\'Android sera utilisé pour stocker en toute sécurité la phrase secrète après sa modification ou redémarrage de l\'app - cela permettra de recevoir les notifications.</string>
<string name="store_passphrase_securely_without_recover">Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS accéder au chat si vous la perdez.</string>
<string name="passphrase_is_different">La phrase secrète de la base de données est différente de celle enregistrée dans le Keystore.</string>
<string name="unknown_error">Erreur inconnue</string>
<string name="enter_correct_passphrase">Entrez la phrase secrète correcte.</string>
<string name="alert_message_no_group">Ce groupe n\'existe plus.</string>
<string name="you_joined_this_group">Vous avez rejoint ce groupe</string>
<string name="you_rejected_group_invitation">Vous avez rejeté l\'invitation du groupe</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">vous avez changé d\'adresse pour %s</string>
<string name="rcv_conn_event_switch_queue_phase_changing">changement d\'adresse…</string>
<string name="incoming_video_call">Appel vidéo entrant</string>
<string name="video_call_no_encryption">appel vidéo (sans chiffrement)</string>
<string name="ignore">Ignorer</string>
<string name="call_already_ended">Appel déjà terminé !</string>
<string name="settings_audio_video_calls">Appels audio et vidéo</string>
<string name="status_e2e_encrypted">chiffré de bout en bout</string>
<string name="settings_section_title_develop">DEVELOPPER</string>
<string name="settings_experimental_features">Fonctionnalités expérimentales</string>
<string name="settings_section_title_socks">SOCKS PROXY</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_calls">APPELS</string>
<string name="import_database">Importer la base de données</string>
<string name="new_database_archive">Nouvelle archive de base de données</string>
<string name="old_database_archive">Archives de l\'ancienne base de données</string>
<string name="delete_database">Supprimer la base de données</string>
<string name="error_starting_chat">Erreur lors du démarrage du chat</string>
<string name="import_database_confirmation">Importer</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Cette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irréversiblement perdus.</string>
<string name="chat_database_deleted">Base de données du chat supprimée</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Redémarrez l\'application pour créer un nouveau profil de chat.</string>
<string name="you_must_use_the_most_recent_version_of_database">Vous devez utiliser la version la plus récente de votre base de données de chat sur un seul appareil UNIQUEMENT, sinon vous risquez de ne plus recevoir les messages de certains contacts.</string>
<string name="total_files_count_and_size">%d fichier·s avec une taille totale de %s</string>
<string name="chat_item_ttl_none">jamais</string>
<string name="chat_item_ttl_week">1 semaine</string>
<string name="database_will_be_encrypted_and_passphrase_stored">La base de données sera chiffrée et la phrase secrète sera stockée dans le Keystore.</string>
<string name="database_encryption_will_be_updated">La phrase secrète de la base de données sera mise à jour et stockée dans le Keystore.</string>
<string name="database_passphrase_will_be_updated">La phrase secrète de la base de données sera mise à jour.</string>
<string name="store_passphrase_securely">Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS la changer si vous la perdez.</string>
<string name="wrong_passphrase">Mauvaise phrase secrète pour la base de données</string>
<string name="encrypted_database">Base de données chiffrée</string>
<string name="database_error">Erreur de base de données</string>
<string name="error_with_info">Erreur : %s</string>
<string name="cannot_access_keychain">Impossible d\'accéder au Keystore pour enregistrer le mot de passe de la base de données</string>
<string name="unknown_database_error_with_info">Erreur de base de données inconnue : %s</string>
<string name="wrong_passphrase_title">Mauvaise phrase secrète !</string>
<string name="leave_group_question">Quitter le groupe \?</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Vous ne recevrez plus de messages de ce groupe. L\'historique du chat sera conservé.</string>
<string name="icon_descr_add_members">Inviter des membres</string>
<string name="icon_descr_group_inactive">Groupe inactif</string>
<string name="alert_title_group_invitation_expired">Invitation expirée !</string>
<string name="alert_message_group_invitation_expired">L\'invitation du groupe n\'est plus valide, elle a été supprimé par l\'expéditeur.</string>
<string name="alert_title_no_group">Groupe introuvable !</string>
<string name="alert_title_cant_invite_contacts">Impossible d\'inviter les contacts !</string>
<string name="alert_title_cant_invite_contacts_descr">Vous utilisez un profil incognito pour ce groupe - pour éviter de partager votre profil principal ; inviter des contacts n\'est pas possible</string>
<string name="you_sent_group_invitation">Vous avez envoyé une invitation de groupe</string>
<string name="rcv_group_event_member_left">a quitté</string>
<string name="icon_descr_speaker_on">Haut-parleur ON</string>
<string name="send_link_previews">Envoi d\'aperçus de liens</string>
<string name="error_deleting_database">Erreur lors de la suppression de la base de données du chat</string>
<string name="error_stopping_chat">Erreur lors de l\'arrêt du chat</string>
<string name="error_exporting_chat_database">Erreur lors de l\'exportation de la base de données du chat</string>
<string name="import_database_question">Importer la base de données du chat \?</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Votre base de données de chat actuelle sera SUPPRIMÉE et REMPLACÉE par celle qui a été importée.
\nCette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irrémédiablement perdus.</string>
<string name="enter_passphrase">Entrez la phrase secrète…</string>
<string name="incoming_audio_call">Appel audio entrant</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> veut se connecter à vous via</string>
<string name="your_calls">Vos appels</string>
<string name="connect_calls_via_relay">Se connecter via relais</string>
<string name="call_on_lock_screen">Appels en écran verrouillé :</string>
<string name="show_call_on_lock_screen">Montrer</string>
<string name="no_call_on_lock_screen">Désactiver</string>
<string name="your_ice_servers">Vos serveurs ICE</string>
<string name="webrtc_ice_servers">Serveurs WebRTC ICE</string>
<string name="open_simplex_chat_to_accept_call">Ouvrez <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour décrocher</string>
<string name="status_no_e2e_encryption">sans chiffrement de bout en bout</string>
<string name="status_contact_has_e2e_encryption">Ce contact a le chiffrement de bout en bout</string>
<string name="status_contact_has_no_e2e_encryption">Ce contact n\'a pas le chiffrement de bout en bout</string>
<string name="call_connection_peer_to_peer">pair-à-pair</string>
<string name="icon_descr_audio_off">Audio OFF</string>
<string name="icon_descr_audio_on">Audio ON</string>
<string name="icon_descr_speaker_off">Haut-parleur OFF</string>
<string name="icon_descr_flip_camera">Retourner la caméra</string>
<string name="icon_descr_call_pending_sent">Appel en suspend</string>
<string name="icon_descr_call_missed">Appel manqué</string>
<string name="icon_descr_call_connecting">Appel en connexion</string>
<string name="answer_call">Répondre à l\'appel</string>
<string name="integrity_msg_bad_hash">hash de message incorrect</string>
<string name="integrity_msg_duplicate">message dupliqué</string>
<string name="alert_title_skipped_messages">Messages manqués</string>
<string name="privacy_and_security">Vie privée et sécurité</string>
<string name="protect_app_screen">Protéger l\'écran de l\'app</string>
<string name="auto_accept_images">Images auto-acceptées</string>
<string name="full_backup">Sauvegarde des données de l\'app</string>
<string name="settings_section_title_you">VOUS</string>
<string name="settings_section_title_help">AIDE</string>
<string name="settings_section_title_support">SOUTENEZ SIMPLEX CHAT</string>
<string name="settings_section_title_incognito">Mode Incognito</string>
<string name="chat_is_running">Le chat est en cours d\'exécution</string>
<string name="chat_is_stopped">Le chat est arrêté</string>
<string name="chat_database_section">BASE DE DONNÉES DU CHAT</string>
<string name="database_passphrase">Phrase secrète de la base de données</string>
<string name="export_database">Exporter la base de données</string>
<string name="stop_chat_confirmation">Arrêter</string>
<string name="set_password_to_export">Définir la phrase secrète pour l\'export</string>
<string name="set_password_to_export_desc">La base de données est chiffrée à l\'aide d\'une phrase secrète aléatoire. Veuillez la changer avant d\'exporter.</string>
<string name="error_importing_database">Erreur lors de l\'importation de la base de données du chat</string>
<string name="chat_database_imported">Base de données du chat importée</string>
<string name="delete_chat_profile_question">Supprimer le profil du chat \?</string>
<string name="stop_chat_to_enable_database_actions">Arrêter le chat pour agir sur la base de données.</string>
<string name="delete_files_and_media_question">Supprimer les fichiers et médias \?</string>
<string name="delete_files_and_media_desc">Cette action ne peut être annulée - tous les fichiers et médias reçus et envoyés seront supprimés. Les photos à faible résolution seront conservées.</string>
<string name="no_received_app_files">Aucun fichier reçu ou envoyé</string>
<string name="chat_item_ttl_month">1 mois</string>
<string name="chat_item_ttl_seconds">%s seconde·s</string>
<string name="delete_messages_after">Supprimer les messages après</string>
<string name="enable_automatic_deletion_question">Activer la suppression automatique des messages \?</string>
<string name="error_changing_message_deletion">Erreur de changement de paramètre</string>
<string name="remove_passphrase_from_keychain">Retirer la phrase secrète du Keystore \?</string>
<string name="notifications_will_be_hidden">Les notifications seront délivrées jusqu\'à ce que l\'application s\'arrête !</string>
<string name="remove_passphrase">Supprimer</string>
<string name="current_passphrase">Phrase secrète actuelle…</string>
<string name="new_passphrase">Nouvelle phrase secrète…</string>
<string name="confirm_new_passphrase">Confirmer la nouvelle phrase secrète…</string>
<string name="update_database_passphrase">Mise à jour de la phrase secrète de la base de données</string>
<string name="keychain_is_storing_securely">Le Keystore d\'Android est utilisé pour stocker en toute sécurité la phrase secrète - elle permet au service de notification de fonctionner.</string>
<string name="you_have_to_enter_passphrase_every_time">Vous devez saisir la phrase secrète à chaque fois que l\'application démarre - elle n\'est pas stockée sur l\'appareil.</string>
<string name="encrypt_database_question">Chiffrer la base de données \?</string>
<string name="change_database_passphrase_question">Changer la phrase secrète de la base de données \?</string>
<string name="database_will_be_encrypted">La base de données sera chiffrée.</string>
<string name="keychain_error">Erreur de la keychain</string>
<string name="file_with_path">Fichier : %s</string>
<string name="database_passphrase_is_required">La phrase secrète de la base de données est nécessaire pour ouvrir le chat.</string>
<string name="save_passphrase_and_open_chat">Sauvegarder la phrase secrète et ouvrir le chat</string>
<string name="open_chat">Ouvrir le chat</string>
<string name="database_backup_can_be_restored">La tentative de modification de la phrase secrète de la base de données n\'a pas abouti.</string>
<string name="restore_database_alert_title">Restaurer la sauvegarde de la base de données \?</string>
<string name="restore_database_alert_confirm">Restaurer</string>
<string name="chat_is_stopped_indication">Le chat est arrêté</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Vous pouvez lancer le chat via les Paramètres / la Base de données de l\'app ou en la redémarrant.</string>
<string name="chat_archive_header">Archives du chat</string>
<string name="chat_archive_section">ARCHIVE DU CHAT</string>
<string name="save_archive">Sauvegarder l\'archive</string>
<string name="delete_archive">Supprimer l\'archive</string>
<string name="delete_chat_archive_question">Supprimer l\'archive du chat \?</string>
<string name="group_invitation_item_description">Invitation au groupe <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="join_group_question">Rejoindre le groupe \?</string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Vous êtes invité·e dans un groupe. Rejoignez le pour vous connecter avec ses membres.</string>
<string name="join_group_button">Rejoindre</string>
<string name="join_group_incognito_button">Rejoindre en incognito</string>
<string name="joining_group">Entrain de rejoindre le groupe</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Vous avez rejoint ce groupe. Connexion à l\'invitation d\'un membre du groupe.</string>
<string name="leave_group_button">Quitter</string>
<string name="you_are_invited_to_group">Vous êtes invité·e au groupe</string>
<string name="group_invitation_tap_to_join">Appuyez pour rejoindre</string>
<string name="group_invitation_tap_to_join_incognito">Appuyez pour rejoindre incognito</string>
<string name="group_invitation_expired">Invitation au groupe expirée</string>
<string name="rcv_group_event_member_added">a invité <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">est connecté·e</string>
<string name="rcv_group_event_changed_member_role">a modifié le rôle de %s pour %s</string>
<string name="rcv_group_event_changed_your_role">a modifié votre rôle pour %s</string>
<string name="rcv_group_event_user_deleted">vous a retiré</string>
<string name="rcv_group_event_group_deleted">a supprimé le groupe</string>
<string name="rcv_group_event_updated_group_profile">mise à jour du profil de groupe</string>
<string name="snd_group_event_changed_member_role">vous avez modifié le rôle de %s pour %s</string>
<string name="snd_group_event_changed_role_for_yourself">vous avez modifié votre rôle pour %s</string>
<string name="snd_group_event_user_left">vous avez quitté</string>
<string name="snd_group_event_group_profile_updated">mise à jour du profil de groupe</string>
<string name="rcv_conn_event_switch_queue_phase_completed">adresse modifiée pour vous</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">changement d\'adresse pour %s…</string>
<string name="snd_conn_event_switch_queue_phase_changing">changement d\'adresse…</string>
<string name="group_member_role_member">membre</string>
<string name="group_member_role_admin">admin</string>
<string name="group_member_role_owner">propriétaire</string>
<string name="group_member_status_removed">supprimé</string>
<string name="group_member_status_left">a quitté</string>
<string name="group_member_status_group_deleted">groupe supprimé</string>
<string name="group_member_status_invited">invité·e</string>
<string name="group_member_status_introduced">connexion (introduite)</string>
<string name="group_member_status_intro_invitation">connexion (introduite par invitation)</string>
<string name="group_member_status_accepted">connexion (acceptée)</string>
<string name="group_member_status_announced">connexion (annoncée)</string>
<string name="group_member_status_connected">connecté</string>
<string name="group_member_status_complete">complet</string>
<string name="group_member_status_creator">créateur</string>
<string name="group_member_status_connecting">connexion</string>
<string name="no_contacts_to_add">Aucun contact à ajouter</string>
<string name="new_member_role">Nouveau rôle</string>
<string name="delete_group_question">Supprimer le groupe\?</string>
<string name="group_link">Lien du groupe</string>
<string name="button_create_group_link">Créer un lien</string>
<string name="button_edit_group_profile">Modifier le profil du groupe</string>
<string name="remove_member_confirmation">Supprimer</string>
<string name="member_info_section_title_member">MEMBRE</string>
<string name="live_message">Message dynamique !</string>
<string name="send_live_message">Envoyer un message dynamique</string>
<string name="send_live_message_desc">Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez</string>
<string name="send_verb">Envoyer</string>
<string name="member_role_will_be_changed_with_invitation">Le rôle sera changé pour «%s». Le membre va recevoir une nouvelle invitation.</string>
<string name="live">LIVE</string>
<string name="button_add_members">Inviter des membres</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Vous pouvez partager un lien ou un code QR - n\'importe qui pourra rejoindre le groupe. Vous ne perdrez pas les membres du groupe si vous le supprimez par la suite.</string>
<string name="info_row_local_name">Nom local</string>
<string name="create_group_link">Créer un lien de groupe</string>
<string name="error_deleting_link_for_group">Erreur lors de la suppression du lien du groupe</string>
<string name="error_creating_link_for_group">Erreur lors de la création du lien du groupe</string>
<string name="only_group_owners_can_change_prefs">Seuls les propriétaires du groupe peuvent modifier les préférences du groupe.</string>
<string name="section_title_for_console">POUR TERMINAL</string>
<string name="change_member_role_question">Changer le rôle du groupe \?</string>
<string name="member_role_will_be_changed_with_notification">Le rôle sera changé pour «%s». Les membres du groupe seront notifiés.</string>
<string name="icon_descr_contact_checked">Contact vérifié⸱e</string>
<string name="clear_contacts_selection_button">Effacer</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contact·s sélectionné·e·s</string>
<string name="skip_inviting_button">Passer linvitation de membres</string>
<string name="select_contacts">Sélectionnez des contacts</string>
<string name="no_contacts_selected">Aucun contact sélectionné</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBRES</string>
<string name="group_info_member_you">vous : <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="button_delete_group">Supprimer le groupe</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Le groupe va être supprimé pour tout les membres - impossible de revenir en arrière!</string>
<string name="delete_group_for_self_cannot_undo_warning">Le groupe va être supprimé pour vous - impossible de revenir en arrière!</string>
<string name="button_leave_group">Quitter le groupe</string>
<string name="delete_link_question">Supprimer le lien\?</string>
<string name="delete_link">Supprimer le lien</string>
<string name="all_group_members_will_remain_connected">Tous les membres du groupe resteront connectés.</string>
<string name="icon_descr_expand_role">Étendre la sélection de rôle</string>
<string name="invite_to_group_button">Inviter au groupe</string>
<string name="invite_prohibited">Impossible d\'inviter le contact!</string>
<string name="invite_prohibited_description">Vous essayez d\'inviter un contact avec lequel vous avez partagé un profil incognito à rejoindre le groupe dans lequel vous utilisez votre profil principal</string>
<string name="info_row_database_id">ID de base de données</string>
<string name="button_remove_member">Retirer le membre</string>
<string name="button_send_direct_message">Envoi de message direct</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Ce membre sera retiré du groupe - impossible de revenir en arrière!</string>
<string name="role_in_group">Rôle</string>
<string name="change_role">Changer le rôle</string>
<string name="change_verb">Changer</string>
<string name="switch_verb">Échanger</string>
<string name="error_removing_member">Erreur lors de la suppression d\'un membre</string>
<string name="error_changing_role">Erreur lors du changement de rôle</string>
<string name="group_full_name_field">Nom complet du groupe :</string>
<string name="update_network_settings_confirmation">Mise à jour</string>
<string name="chat_preferences_on">on</string>
<string name="chat_preferences_off">off</string>
<string name="direct_messages">Messages dynamiques</string>
<string name="full_deletion">Supprimer pour tous</string>
<string name="only_you_can_delete_messages">Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé).</string>
<string name="conn_stats_section_title_servers">SERVEURS</string>
<string name="receiving_via">Réception via</string>
<string name="theme_system">Système</string>
<string name="allow_direct_messages">Autoriser l\'envoi de messages directs aux membres.</string>
<string name="prohibit_direct_messages">Interdire l\'envoi de messages directs aux membres.</string>
<string name="group_members_can_delete">Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés.</string>
<string name="message_deletion_prohibited_in_chat">La suppression irréversible de messages est interdite dans ce groupe.</string>
<string name="sending_via">Envoi via</string>
<string name="network_status">État du réseau</string>
<string name="switch_receiving_address">Changer d\'adresse de réception</string>
<string name="create_secret_group_title">Créer un groupe secret</string>
<string name="group_main_profile_sent">Votre profil de chat sera envoyé aux membres du groupe</string>
<string name="network_option_enable_tcp_keep_alive">Activer le TCP keep-alive</string>
<string name="network_options_save">Sauvegarder</string>
<string name="update_network_settings_question">Mettre à jour les paramètres réseau \?</string>
<string name="incognito">Incognito</string>
<string name="incognito_random_profile">Votre profil aléatoire</string>
<string name="incognito_random_profile_description">Un profil aléatoire sera envoyé à votre contact</string>
<string name="incognito_random_profile_from_contact_description">Un profil aléatoire sera envoyé au contact qui vous a envoyé ce lien</string>
<string name="incognito_info_allows">Cela permet d\'avoir plusieurs connections anonymes sans aucune données partagées entre elles sur un même profil.</string>
<string name="incognito_info_find">Pour trouver le profil utilisé lors d\'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat.</string>
<string name="theme_light">Clair</string>
<string name="theme_dark">Sombre</string>
<string name="theme">Thème</string>
<string name="save_color">Sauvegarder la couleur</string>
<string name="reset_color">Réinitialisation des couleurs</string>
<string name="color_primary">Principale</string>
<string name="chat_preferences_you_allow">Vous autorisez</string>
<string name="chat_preferences_contact_allows">Votre contact autorise</string>
<string name="chat_preferences_default">par défaut (%s)</string>
<string name="chat_preferences_no">non</string>
<string name="chat_preferences_always">toujours</string>
<string name="chat_preferences">Préférences de chat</string>
<string name="contact_preferences">Préférences de contact</string>
<string name="group_preferences">Préférences du groupe</string>
<string name="set_group_preferences">Définir les préférences du groupe</string>
<string name="your_preferences">Vos préférences</string>
<string name="allow_your_contacts_irreversibly_delete">Autorise votre contact à supprimer de façon définitive des messages envoyés.</string>
<string name="contacts_can_mark_messages_for_deletion">Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter.</string>
<string name="allow_your_contacts_to_send_voice_messages">Autorise vos contacts à envoyer des messages vocaux.</string>
<string name="allow_voice_messages_only_if">Autoriser les messages vocaux uniquement si votre contact les autorise.</string>
<string name="prohibit_sending_voice_messages">Interdire l\'envoi de messages vocaux.</string>
<string name="only_you_can_send_disappearing">Seulement vous pouvez envoyer des messages éphémères.</string>
<string name="only_you_can_send_voice">Vous seul pouvez envoyer des messages vocaux.</string>
<string name="allow_to_delete_messages">Autoriser la suppression irréversible de messages envoyés.</string>
<string name="disappearing_messages_are_prohibited">Les messages éphémères sont interdits dans ce groupe.</string>
<string name="group_members_can_send_voice">Les membres du groupe peuvent envoyer des messages vocaux.</string>
<string name="delete_after">Supprimer après</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_s">%ds</string>
<string name="ttl_min">%d min</string>
<string name="ttl_month">%d mois</string>
<string name="ttl_months">%d mois</string>
<string name="ttl_m">%dm</string>
<string name="ttl_mth">%dm</string>
<string name="ttl_hour">%d heure</string>
<string name="ttl_hours">%d heures</string>
<string name="ttl_h">%dh</string>
<string name="ttl_day">%d jour</string>
<string name="ttl_days">%d jours</string>
<string name="ttl_d">%dj</string>
<string name="ttl_week">%d semaine</string>
<string name="ttl_weeks">%d semaines</string>
<string name="ttl_w">%dsmn</string>
<string name="timed_messages">Messages éphémères</string>
<string name="voice_messages">Messages vocaux</string>
<string name="feature_enabled">activé</string>
<string name="feature_enabled_for_you">activé pour vous</string>
<string name="feature_enabled_for_contact">activé pour le contact</string>
<string name="feature_off">off</string>
<string name="feature_received_prohibited">reçu, non autorisé</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Autorise votre contact à envoyer des messages éphémères.</string>
<string name="conn_level_desc_direct">directe</string>
<string name="group_is_decentralized">Le groupe est entièrement décentralisé il n\'est visible que par ses membres.</string>
<string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages éphémères.</string>
<string name="network_options_revert">Revenir en arrière</string>
<string name="prohibit_sending_disappearing_messages">Interdit lenvoi de messages éphémères.</string>
<string name="incognito_info_protects">Le mode Incognito protège la confidentialité de votre profil principal — pour chaque nouveau contact un nouveau profil aléatoire est créé.</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</string>
<string name="incognito_info_share">Lorsque vous partagez un profil incognito avec quelqu\'un, ce profil sera utilisé pour les groupes auxquels il vous invite.</string>
<string name="chat_preferences_yes">oui</string>
<string name="allow_disappearing_messages_only_if">Autorise les messages éphémères seulement si votre contact les autorises.</string>
<string name="allow_irreversible_message_deletion_only_if">Autoriser la suppression irréversible des messages uniquement si votre contact vous l\'autorise.</string>
<string name="only_your_contact_can_delete">Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé).</string>
<string name="only_your_contact_can_send_disappearing">Seulement votre contact peut envoyer des messages éphémères.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Vous et votre contact êtes tous deux en mesure d\'envoyer des messages éphémères.</string>
<string name="voice_messages_are_prohibited">Les messages vocaux sont interdits dans ce groupe.</string>
<string name="group_display_name_field">Nom affiché du groupe :</string>
<string name="group_unsupported_incognito_main_profile_sent">Le mode Incognito n\'est pas supporté ici - votre profil principal sera envoyé aux membres du groupe</string>
<string name="conn_level_desc_indirect">indirecte (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="info_row_group">Groupe</string>
<string name="info_row_connection">Connexion</string>
<string name="network_option_seconds_label">sec</string>
<string name="network_option_tcp_connection_timeout">Délai de connexion TCP</string>
<string name="group_profile_is_stored_on_members_devices">Le profil du groupe est stocké sur les appareils des membres, pas sur les serveurs.</string>
<string name="save_group_profile">Sauvegarder le profil du groupe</string>
<string name="error_saving_group_profile">Erreur lors de la sauvegarde du profil de groupe</string>
<string name="network_options_reset_to_defaults">Réinitialisation des valeurs par défaut</string>
<string name="network_option_protocol_timeout">Délai du protocole</string>
<string name="network_option_ping_interval">Intervalle de PING</string>
<string name="both_you_and_your_contacts_can_delete">Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés.</string>
<string name="message_deletion_prohibited">La suppression irréversible de message est interdite dans ce chat.</string>
<string name="both_you_and_your_contact_can_send_voice">Vous et votre contact êtes tous deux en mesure d\'envoyer des messages vocaux.</string>
<string name="only_your_contact_can_send_voice">Seul votre contact peut envoyer des messages vocaux.</string>
<string name="voice_prohibited_in_this_chat">Les messages vocaux sont interdits dans ce chat.</string>
<string name="disappearing_prohibited_in_this_chat">Les messages éphémères sont interdits dans cette discussion.</string>
<string name="allow_to_send_voice">Autoriser l\'envoi de messages vocaux.</string>
<string name="prohibit_sending_voice">Interdire l\'envoi de messages vocaux.</string>
<string name="allow_to_send_disappearing">Autorise lenvoi de messages éphémères.</string>
<string name="prohibit_sending_disappearing">Interdit lenvoi de messages éphémères.</string>
<string name="prohibit_message_deletion">Interdire la suppression irréversible des messages.</string>
<string name="group_members_can_send_dms">Les membres du groupe peuvent envoyer des messages directs.</string>
<string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans ce groupe.</string>
<string name="v4_4_live_messages_desc">Les destinataires voient les mises à jour au fur et à mesure que vous les tapez.</string>
<string name="v4_4_verify_connection_security">Vérifier la sécurité de la connexion</string>
<string name="v4_4_verify_connection_security_desc">Comparez les codes de sécurité avec vos contacts.</string>
<string name="new_in_version">Nouveautés de la %s</string>
<string name="v4_2_security_assessment">Évaluation de sécurité</string>
<string name="v4_2_group_links">Liens de groupe</string>
<string name="v4_2_auto_accept_contact_requests_desc">Avec message de bienvenue facultatif.</string>
<string name="v4_3_voice_messages">Messages vocaux</string>
<string name="v4_3_voice_messages_desc">Max 40 secondes, réception immédiate.</string>
<string name="v4_3_irreversible_message_deletion">Suppression irréversible des messages</string>
<string name="v4_3_irreversible_message_deletion_desc">Vos contacts peuvent autoriser la suppression complète des messages.</string>
<string name="v4_3_improved_privacy_and_security">Une meilleure sécurité et protection de la vie privée</string>
<string name="v4_3_improved_privacy_and_security_desc">Masquer l\'écran de l\'app dans les apps récentes.</string>
<string name="v4_4_disappearing_messages">Messages éphémères</string>
<string name="v4_4_disappearing_messages_desc">Les messages envoyés seront supprimés après une durée déterminée.</string>
<string name="v4_4_live_messages">Messages dynamiques</string>
<string name="accept_feature">Accepter</string>
<string name="v4_2_auto_accept_contact_requests">Demandes de contact auto-acceptées</string>
<string name="whats_new">Quoi de neuf \?</string>
<string name="v4_2_group_links_desc">Les admins peuvent créer les liens qui permettent de rejoindre les groupes.</string>
<string name="accept_feature_set_1_day">Définir 1 jour</string>
<string name="v4_2_security_assessment_desc">La sécurité de SimpleX Chat a été auditée par Trail of Bits.</string>
<string name="v4_3_improved_server_configuration">Configuration de serveur améliorée</string>
<string name="v4_3_improved_server_configuration_desc">Ajoutez des serveurs en scannant des codes QR.</string>
<string name="invalid_data">données invalides</string>
<string name="invalid_chat">chat invalide</string>
<string name="icon_descr_cancel_live_message">Annuler le message dynamique</string>
<string name="feature_offered_item">offert %s</string>
<string name="feature_offered_item_with_param">offert %s: %2s</string>
<string name="feature_cancelled_item">annulé %s</string>
<string name="app_version_title">Version de l\'application</string>
<string name="core_simplexmq_version">simplexmq : v%s (%2s)</string>
<string name="app_version_code">Build de l\'app : %s</string>
<string name="app_version_name">Version de l\'app : v%s</string>
<string name="core_build_timestamp">Cœur compilé le : %s</string>
<string name="core_version">Version du cœur : v%s</string>
<string name="network_option_ping_count">Nombre de PING</string>
<string name="users_delete_all_chats_deleted">Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !</string>
<string name="delete_files_and_media_all">Effacer tous les fichiers</string>
<string name="users_delete_profile_for">Supprimer le profil de chat pour</string>
<string name="network_session_mode_user_description">Une connexion TCP distincte (et un identifiant SOCKS) sera utilisée <b>pour chaque profil de chat que vous avez dans l\'application</b>.</string>
<string name="users_delete_question">Supprimer le profil du chat \?</string>
<string name="files_and_media_section">Fichiers &amp; médias</string>
<string name="messages_section_title">Messages</string>
<string name="smp_servers_per_user">Les serveurs pour les nouvelles connexions de votre profil de chat actuel</string>
<string name="messages_section_description">Ce paramètre s\'applique aux messages de votre profil de chat actuel</string>
<string name="network_session_mode_entity">Connexion</string>
<string name="delete_files_and_media_for_all_users">Effacer les fichiers de tous les profils de chat</string>
<string name="users_delete_with_connections">Profil et connexions au serveur</string>
<string name="network_session_mode_transport_isolation">Isolement du transport</string>
<string name="update_network_session_mode_question">Mettre à jour le mode d\'isolation du transport \?</string>
<string name="your_chat_profiles_stored_locally">Vos profils de chat sont stockés localement, uniquement sur votre appareil</string>
<string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>.
\n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.</string>
<string name="network_session_mode_user">Profil de chat</string>
<string name="users_add">Ajouter un profil</string>
<string name="users_delete_data_only">Données de profil local uniquement</string>
<string name="error_deleting_user">Erreur lors de la suppression du profil utilisateur</string>
<string name="your_chat_profiles">Vos profils de chat</string>
<string name="failed_to_active_user_title">Erreur lors du changement de profil !</string>
<string name="failed_to_create_user_title">Erreur lors de la création du profil !</string>
<string name="failed_to_create_user_duplicate_desc">Vous avez déjà un profil de chat avec ce même nom affiché. Veuillez choisir un autre nom.</string>
<string name="failed_to_create_user_duplicate_title">Nom d\'affichage en double !</string>
<string name="v4_4_french_interface">Interface en français</string>
<string name="v4_5_transport_isolation_descr">Par profil de chat (par défaut) ou par connexion (BETA).</string>
<string name="v4_5_italian_interface">Interface en italien</string>
<string name="v4_5_message_draft">Brouillon de message</string>
<string name="v4_5_reduced_battery_usage_descr">Plus d\'améliorations à venir !</string>
<string name="v4_5_multiple_chat_profiles">Différents profils de chat</string>
<string name="v4_5_message_draft_descr">Conserver le brouillon du dernier message, avec les pièces jointes.</string>
<string name="v4_5_reduced_battery_usage">Réduction de la consommation de batterie</string>
<string name="v4_5_private_filenames">Noms de fichiers privés</string>
<string name="v4_5_italian_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="v4_4_french_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="v4_5_private_filenames_descr">Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC.</string>
<string name="v4_5_transport_isolation">Isolation du transport</string>
<string name="v4_5_multiple_chat_profiles_descr">Différents noms, avatars et mode d\'isolation de transport.</string>
</resources>
@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="share_image">चित्र साझा करें…</string>
<string name="chat_preferences_off">बंद</string>
<string name="accept_feature_set_1_day">1 दिन निर्धारित करें</string>
<string name="v4_3_improved_server_configuration_desc">क्यूआर संहिता स्कैन करके सर्वर जोड़ें।</string>
<string name="group_preview_you_are_invited">आपको समूह में आमंत्रित किया जाता है</string>
<string name="icon_descr_server_status_connected">जुड़े हुए</string>
<string name="use_camera_button">कैमरे का प्रयोग करें</string>
<string name="above_then_preposition_continuation">ऊपर,तब:</string>
<string name="accept_contact_button">स्वीकार करना</string>
<string name="connect_button">जुडिये</string>
<string name="your_contact_address">आपका संपर्क पता</string>
<string name="smp_servers_add_to_another_device">दूसरे उपकरण में जोड़ें</string>
<string name="bold">निडर</string>
<string name="answer_call">कॉल का उत्तर दें</string>
<string name="settings_section_title_you">तुम</string>
<string name="settings_section_title_settings">समायोजन</string>
<string name="chat_item_ttl_month">1 महीना</string>
<string name="rcv_group_event_member_connected">जुड़े हुए</string>
<string name="group_member_role_admin">व्यवस्थापक</string>
<string name="all_group_members_will_remain_connected">समूह के सभी सदस्य जुड़े रहेंगे।</string>
<string name="change_verb">परिवर्तन</string>
<string name="sending_via">माध्यम से भेजा जा रहा है</string>
<string name="feature_off">बंद</string>
<string name="whats_new">नया क्या है</string>
<string name="v4_2_group_links_desc">व्यवस्थापक समूहों में शामिल होने के लिए लिंक बना सकते हैं।</string>
<string name="chat_item_ttl_day">1 दिन</string>
<string name="chat_item_ttl_week">1 सप्ताह</string>
<string name="about_simplex">सिंपलएक्स के बारे में</string>
<string name="about_simplex_chat">बारे में <xliff:g id="appNameFull">सिंप्लेक्स चैट</xliff:g></string>
<string name="accept_call_on_lock_screen">स्वीकार करना</string>
<string name="accept">स्वीकार करना</string>
<string name="accept_feature">स्वीकार करना</string>
<string name="accept_connection_request__question">संबंध अनुरोध स्वीकार करें\?</string>
<string name="callstatus_accepted">स्वीकृत कॉल</string>
<string name="accept_contact_incognito_button">गुप्त स्वीकार करें</string>
<string name="accept_requests">निवेदन स्वीकार करो</string>
<string name="smp_servers_preset_add">पूर्वनिर्धारित सर्वर जोड़ें</string>
<string name="users_add">प्रोफ़ाइल जोड़ें</string>
<string name="smp_servers_add">सर्वर जोड़े…</string>
<string name="notifications_mode_service">हमेशा बने रहें</string>
<string name="attach">संलग्न करना</string>
<string name="network_settings">उन्नत संजाल समायोजन</string>
<string name="users_delete_all_chats_deleted">सभी बातचीत और संदेश हटा दिए जाएंगे - इसे पूर्ववत नहीं किया जा सकता!</string>
<string name="chat_preferences_always">हमेशा</string>
<string name="allow_verb">अनुमति देना</string>
<string name="appearance_settings">दिखावट</string>
<string name="cancel_verb">रद्द करना</string>
<string name="icon_descr_cancel_file_preview">फ़ाइल पूर्वावलोकन रद्द करें</string>
<string name="icon_descr_cancel_image_preview">छवि पूर्वावलोकन रद्द करें</string>
<string name="clear_verb">साफ़</string>
<string name="colored">रंगीन</string>
<string name="callstate_connected">जुड़े हुए</string>
<string name="smp_server_test_connect">जुडिये</string>
<string name="connect_via_link_verb">जुडिये</string>
<string name="server_connected">जुड़े हुए</string>
<string name="group_member_role_owner">स्वामी</string>
<string name="group_member_status_connected">जुड़े हुए</string>
<string name="notification_contact_connected">जुड़े हुए</string>
<string name="you_joined_this_group">आप इस समूह में शामिल हो गए</string>
<string name="group_info_member_you">तुम: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="you_are_invited_to_group">आपको समूह में आमंत्रित किया जाता है</string>
<string name="snd_conn_event_switch_queue_phase_completed">तुमने पता बदल लिया</string>
<string name="snd_group_event_user_left">आप चले गए</string>
<string name="unknown_error">अज्ञात त्रुटि</string>
<string name="chat_preferences_you_allow">आप आज्ञा दें</string>
<string name="welcome">स्वागत!</string>
<string name="la_notice_turn_on">चालू करो</string>
<string name="section_title_welcome_message">स्वागत संदेश</string>
<string name="unknown_message_format">अज्ञात संदेश प्रारूप</string>
<string name="personal_welcome">स्वागत <xliff:g>%1$s</xliff:g>!</string>
<string name="callstate_starting">शुरुआत</string>
<string name="send_verb">भेजना</string>
<string name="save_color">रंग बचाओ</string>
<string name="share_verb">साझा करना</string>
<string name="reject_contact_button">अस्वीकार</string>
<string name="network_use_onion_hosts_required">आवश्यक</string>
<string name="reject">अस्वीकार</string>
<string name="open_verb">खुला</string>
<string name="group_member_status_removed">निकाला गया</string>
<string name="rcv_group_event_member_deleted">निकाला गया <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="reply_verb">जवाब दे दो</string>
<string name="leave_group_button">छोड़ना</string>
<string name="mark_read">पढ़ा हुआ चिह्नित करें</string>
<string name="icon_descr_more_button">अधिक</string>
<string name="network_use_onion_hosts_no">नहीं</string>
<string name="chat_item_ttl_none">कभी नहीं</string>
<string name="group_member_status_invited">आमंत्रित</string>
<string name="delete_after">बाद मिटा दें</string>
<string name="display_name_invited_to_connect">जुड़ने के लिए आमंत्रित किया</string>
<string name="rcv_group_event_invited_via_your_group_link">आपके समूह लिंक के माध्यम से आमंत्रित किया गया</string>
<string name="rcv_group_event_member_added">आमंत्रित <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="icon_descr_add_members">सदस्यों को आमंत्रित करो</string>
<string name="v4_3_irreversible_message_deletion">अपरिवर्तनीय संदेश विलोपन</string>
<string name="button_add_members">सदस्यों को आमंत्रित करो</string>
<string name="invite_to_group_button">समूह में आमंत्रित करें</string>
<string name="message_deletion_prohibited_in_chat">इस समूह में अपरिवर्तनीय संदेश हटाना प्रतिबंधित है।</string>
<string name="italic">तिरछा</string>
<string name="join_group_button">जोड़ना</string>
<string name="join_group_incognito_button">गुप्त में शामिल हों</string>
<string name="joining_group">समूह में शामिल होना</string>
<string name="join_group_question">समूह में शामिल हों\?</string>
<string name="thousand_abbreviation"></string>
<string name="keychain_error">चाबी का गुच्छा त्रुटि</string>
<string name="button_leave_group">समूह छोड़ दें</string>
<string name="leave_group_question">समूह छोड़ दें</string>
<string name="rcv_group_event_member_left">बाएं</string>
<string name="group_member_status_left">बाएं</string>
<string name="theme_light">रोशनी</string>
<string name="info_row_local_name">स्थानीय नाम</string>
<string name="users_delete_data_only">केवल स्थानीय प्रोफ़ाइल डेटा</string>
<string name="auth_log_in_using_credential">अपने क्रेडेंशियल का उपयोग करके लॉग इन करें</string>
<string name="make_private_connection">एक निजी संबंध बनाओ</string>
<string name="marked_deleted_description">मिटाया हुआ चिह्नित किया गया</string>
<string name="mark_unread">अपठित को चिह्नित करें</string>
<string name="v4_3_voice_messages_desc">अधिकतम 40 सेकंड, तुरन्त प्राप्त हुआ।</string>
<string name="you_sent_group_invitation">आपने समूह आमंत्रण भेजा</string>
<string name="message_delivery_error_desc">सबसे अधिक संभावना है कि इस संपर्क ने आपके साथ संबंध हटा दिया है।</string>
<string name="mute_chat">मूक</string>
<string name="network_status">नेटवर्क की स्थिति</string>
<string name="notification_new_contact_request">नया संपर्क अनुरोध</string>
<string name="delete_files_and_media_all">सभी फाइलों को मिटा दें</string>
<string name="delete_archive">संग्रह हटाएं</string>
<string name="new_database_archive">नया डेटाबेस संग्रह</string>
<string name="new_member_role">नए सदस्य की भूमिका</string>
<string name="settings_notifications_mode_title">अधिसूचना सेवा</string>
<string name="notification_preview_new_message">नया सन्देश</string>
<string name="no_contacts_to_add">जोड़ने के लिए कोई संपर्क नहीं है</string>
<string name="chat_preferences_no">नहीं</string>
<string name="no_contacts_selected">कोई संपर्क नहीं चुना गया</string>
<string name="no_details">कोई विवरण नहीं</string>
<string name="settings_notification_preview_title">अधिसूचना पूर्वावलोकन</string>
<string name="notifications">सूचनाएं</string>
<string name="full_deletion">सभी के लिए हटाएं</string>
<string name="delete_chat_archive_question">चैट संग्रह मिटाएं\?</string>
<string name="delete_chat_profile_question">चैट प्रोफ़ाइल हटाएं\?</string>
<string name="users_delete_question">चैट प्रोफ़ाइल हटाएं\?</string>
<string name="users_delete_profile_for">के लिए चैट प्रोफ़ाइल हटाएं</string>
<string name="button_delete_contact">संपर्क मिटा दें</string>
<string name="deleted_description">हटाए गए</string>
<string name="delete_contact_question">संपर्क मिटा दें\?</string>
<string name="rcv_group_event_group_deleted">हटाए गए समूह</string>
<string name="delete_image">छवि हटाएं</string>
<string name="button_delete_group">समूह हटाएं</string>
<string name="for_me_only">मेरे लिए हटाएं</string>
</resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,969 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="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">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>
<string name="image_will_be_received_when_contact_is_online">L\'immagine verrà ricevuta quando il tuo contatto sarà in linea, aspetta o controlla più tardi!</string>
<string name="waiting_for_image">In attesa dell\'immagine</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="thousand_abbreviation">k</string>
<string name="connect_via_invitation_link">Connettere via link di invito\?</string>
<string name="connect_via_group_link">Connettere via link del gruppo\?</string>
<string name="profile_will_be_sent_to_contact_sending_link">Il tuo profilo verrà inviato al contatto da cui hai ricevuto questo link.</string>
<string name="connect_via_link_verb">Connetti</string>
<string name="server_connected">connesso</string>
<string name="server_error">errore</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>
<string name="marked_deleted_description">contrassegnato eliminato</string>
<string name="sending_files_not_yet_supported">l\'invio di file non è ancora supportato</string>
<string name="receiving_files_not_yet_supported">la ricezione di file non è ancora supportata</string>
<string name="sender_you_pronoun">tu</string>
<string name="unknown_message_format">formato messaggio sconosciuto</string>
<string name="invalid_message_format">formato messaggio non valido</string>
<string name="live">IN DIRETTA</string>
<string name="invalid_chat">conversazione non valida</string>
<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">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>
<string name="description_via_group_link_incognito">incognito via link di gruppo</string>
<string name="description_via_contact_address_link">via link indirizzo del contatto</string>
<string name="description_via_contact_address_link_incognito">incognito via link indirizzo del contatto</string>
<string name="description_via_one_time_link">via link una tantum</string>
<string name="description_via_one_time_link_incognito">incognito via link una tantum</string>
<string name="simplex_link_contact">Indirizzo del contatto SimpleX</string>
<string name="simplex_link_invitation">Invito SimpleX una tantum</string>
<string name="simplex_link_group">Link gruppo SimpleX</string>
<string name="simplex_link_mode_full">Link completo</string>
<string name="simplex_link_mode_browser">Via browser</string>
<string name="error_saving_smp_servers">Errore di salvataggio server SMP</string>
<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 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>
<string name="error_sending_message">Errore di invio del messaggio</string>
<string name="error_adding_members">Errore di aggiunta del/i membro/i</string>
<string name="error_joining_group">Errore di entrata nel gruppo</string>
<string name="cannot_receive_file">Impossibile ricevere il file</string>
<string name="sender_cancelled_file_transfer">Il mittente ha annullato il trasferimento del file.</string>
<string name="error_receiving_file">Errore di ricezione del file</string>
<string name="error_creating_address">Errore di creazione dell\'indirizzo</string>
<string name="contact_already_exists">Il contatto esiste già</string>
<string name="invalid_connection_link">Link di connessione non valido</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro.</string>
<string name="connection_error_auth">Errore di connessione (AUTH)</string>
<string name="error_accepting_contact_request">Errore di accettazione della richiesta del contatto</string>
<string name="sender_may_have_deleted_the_connection_request">Il mittente potrebbe aver eliminato la richiesta di connessione.</string>
<string name="error_deleting_contact">Errore di eliminazione del contatto</string>
<string name="error_deleting_group">Errore di eliminazione del gruppo</string>
<string name="error_deleting_contact_request">Errore di eliminazione della richiesta di contatto</string>
<string name="error_deleting_pending_contact_connection">Errore di eliminazione della connessione del contatto in attesa</string>
<string name="error_changing_address">Errore di modifica dell\'indirizzo</string>
<string name="error_smp_test_failed_at_step">Test fallito al passo %s.</string>
<string name="error_smp_test_server_auth">Il server richiede l\'autorizzazione di creare code, controlla la password</string>
<string name="smp_server_test_connect">Connetti</string>
<string name="smp_server_test_create_queue">Crea coda</string>
<string name="smp_server_test_secure_queue">Coda sicura</string>
<string name="smp_server_test_delete_queue">Elimina coda</string>
<string name="smp_server_test_disconnect">Disconnetti</string>
<string name="icon_descr_instant_notifications">Notifiche istantanee</string>
<string name="service_notifications">Notifiche istantanee!</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Può essere disattivato nelle impostazioni</b>; le notifiche verranno comunque mostrate mentre l\'app è in uso.</string>
<string name="turning_off_service_and_periodic">L\'ottimizzazione della batteria è attiva, spegnimento del servizio in secondo piano e delle richieste periodiche di messaggi nuovi. Puoi riattivarli nelle impostazioni.</string>
<string name="periodic_notifications">Notifiche periodiche</string>
<string name="periodic_notifications_disabled">Le notifiche periodiche sono disattivate!</string>
<string name="periodic_notifications_desc">L\'app cerca nuovi messaggi periodicamente, utilizza una piccola percentuale di batteria al giorno. L\'app non usa notifiche push, non vengono inviati dati dal tuo dispositivo ai server.</string>
<string name="enter_passphrase_notification_title">Password necessaria</string>
<string name="enter_passphrase_notification_desc">Per ricevere notifiche, inserisci la password del database</string>
<string name="database_initialization_error_title">Impossibile inizializzare il database</string>
<string name="database_initialization_error_desc">Il database non funziona bene. Tocca per maggiori informazioni</string>
<string name="simplex_service_notification_text">Ricezione messaggi…</string>
<string name="hide_notification">Nascondi</string>
<string name="ntf_channel_messages">Messaggi di SimpleX Chat</string>
<string name="ntf_channel_calls">Chiamate di SimpleX Chat</string>
<string name="settings_notifications_mode_title">Servizio di notifica</string>
<string name="settings_notification_preview_mode_title">Mostra anteprima</string>
<string name="settings_notification_preview_title">Anteprima notifica</string>
<string name="notifications_mode_off">Quando l\'app è aperta</string>
<string name="notifications_mode_periodic">Periodicamente</string>
<string name="notifications_mode_service">Sempre attivo</string>
<string name="notifications_mode_off_desc">L\'app può ricevere notifiche solo quando è attiva, non verrà avviato alcun servizio in secondo piano</string>
<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">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>
<string name="notification_preview_somebody">Contatto nascosto:</string>
<string name="notification_preview_new_message">messaggio nuovo</string>
<string name="notification_new_contact_request">Nuova richiesta di contatto</string>
<string name="notification_contact_connected">Connesso</string>
<string name="la_notice_turn_on">Attiva</string>
<string name="auth_unlock">Sblocca</string>
<string name="auth_log_in_using_credential">Accedi usando le tue credenziali</string>
<string name="auth_enable_simplex_lock">Attiva SimpleX Lock</string>
<string name="auth_disable_simplex_lock">Disattiva SimpleX Lock</string>
<string name="auth_confirm_credential">Conferma le tue credenziali</string>
<string name="auth_unavailable">Autenticazione non disponibile</string>
<string name="auth_device_authentication_is_disabled_turning_off">L\'autenticazione del dispositivo è disattivata. Disattivazione di SimpleX Lock.</string>
<string name="auth_stop_chat">Ferma la chat</string>
<string name="auth_open_chat_console">Apri la console della chat</string>
<string name="message_delivery_error_title">Errore di recapito del messaggio</string>
<string name="message_delivery_error_desc">Probabilmente questo contatto ha eliminato la connessione con te.</string>
<string name="reply_verb">Rispondi</string>
<string name="share_verb">Condividi</string>
<string name="copy_verb">Copia</string>
<string name="save_verb">Salva</string>
<string name="edit_verb">Modifica</string>
<string name="delete_verb">Elimina</string>
<string name="reveal_verb">Rivela</string>
<string name="hide_verb">Nascondi</string>
<string name="allow_verb">Consenti</string>
<string name="delete_message__question">Eliminare il messaggio\?</string>
<string name="delete_message_cannot_be_undone_warning">Il messaggio verrà eliminato, non è reversibile!</string>
<string name="for_me_only">Elimina per me</string>
<string name="for_everybody">Per tutti</string>
<string name="icon_descr_edited">modificato</string>
<string name="icon_descr_sent_msg_status_sent">inviato</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">invio non autorizzato</string>
<string name="icon_descr_sent_msg_status_send_failed">invio fallito</string>
<string name="icon_descr_received_msg_status_unread">non letto</string>
<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 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">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 chat</string>
<string name="share_image">Condividi immagine…</string>
<string name="share_file">Condividi file…</string>
<string name="icon_descr_context">Icona contestuale</string>
<string name="icon_descr_cancel_file_preview">Annulla anteprima file</string>
<string name="images_limit_title">Troppe immagini!</string>
<string name="image_decoding_exception_title">Errore di decodifica</string>
<string name="image_decoding_exception_desc">L\'immagine non può essere decodificata. Prova con un\'altra o contatta gli sviluppatori.</string>
<string name="image_descr">Immagine</string>
<string name="icon_descr_waiting_for_image">In attesa dell\'immagine</string>
<string name="icon_descr_asked_to_receive">Richiesta di ricezione immagine</string>
<string name="icon_descr_image_snd_complete">Immagine inviata</string>
<string name="image_saved">Immagine salvata nella Galleria</string>
<string name="icon_descr_file">File</string>
<string name="large_file">File grande!</string>
<string name="maximum_supported_file_size">Attualmente la dimensione massima supportata è di <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="waiting_for_file">In attesa del file</string>
<string name="file_will_be_received_when_contact_is_online">Il file verrà ricevuto quando il tuo contatto sarà in linea, aspetta o controlla più tardi!</string>
<string name="file_saved">File salvato</string>
<string name="file_not_found">File non trovato</string>
<string name="error_saving_file">Errore di salvataggio del file</string>
<string name="voice_message">Messaggio vocale</string>
<string name="voice_message_with_duration">Messaggio vocale (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="voice_message_send_text">Messaggio vocale…</string>
<string name="notifications">Notifiche</string>
<string name="delete_contact_question">Eliminare il contatto\?</string>
<string name="button_delete_contact">Elimina contatto</string>
<string name="text_field_set_contact_placeholder">Imposta nome del contatto…</string>
<string name="icon_descr_server_status_connected">Connesso</string>
<string name="icon_descr_server_status_disconnected">Disconnesso</string>
<string name="icon_descr_server_status_error">Errore</string>
<string name="icon_descr_server_status_pending">In attesa</string>
<string name="switch_receiving_address_question">Cambiare l\'indirizzo di ricezione\?</string>
<string name="view_security_code">Vedi codice di sicurezza</string>
<string name="verify_security_code">Verifica codice di sicurezza</string>
<string name="icon_descr_send_message">Invia messaggio</string>
<string name="icon_descr_record_voice_message">Registra messaggio vocale</string>
<string name="allow_voice_messages_question">Permettere i messaggi vocali\?</string>
<string name="you_need_to_allow_to_send_voice">Devi consentire al tuo contatto di inviare messaggi vocali per poterli inviare anche tu.</string>
<string name="voice_messages_prohibited">Messaggi vocali vietati!</string>
<string name="ask_your_contact_to_enable_voice">Chiedi al tuo contatto di attivare l\'invio dei messaggi vocali.</string>
<string name="send_live_message">Invia messaggio in diretta</string>
<string name="live_message">Messaggio in diretta!</string>
<string name="send_verb">Invia</string>
<string name="back">Indietro</string>
<string name="cancel_verb">Annulla</string>
<string name="confirm_verb">Conferma</string>
<string name="reset_verb">Ripristina</string>
<string name="ok">OK</string>
<string name="connect_via_contact_link">Connettere via link del contatto\?</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="you_will_join_group">Entrerai in un gruppo a cui si riferisce questo link e ti connetterai ai suoi membri.</string>
<string name="connection_local_display_name">connessione <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="simplex_link_mode_description">Descrizione</string>
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode_browser_warning">Aprire il link nel browser può ridurre la privacy e la sicurezza della connessione. I link SimpleX non fidati saranno in rosso.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sei già connesso a <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="connection_error_auth_desc">A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo.
\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.</string>
<string name="error_smp_test_certificate">Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Per rispettare la tua privacy, invece delle notifiche push l\'app ha un <b>servizio <xliff:g id="appName">SimpleX</xliff:g> in secondo piano</b>; usa una piccola percentuale di batteria al giorno.</string>
<string name="turn_off_battery_optimization">Per poterlo usare, <b>disattiva l\'ottimizzazione della batteria</b> per <xliff:g id="appName">SimpleX</xliff:g> nella prossima schermata. Altrimenti le notifiche saranno disattivate.</string>
<string name="simplex_service_notification_title">Servizio <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="notifications_mode_service_desc">Servizio in secondo piano sempre attivo. Le notifiche verranno mostrate appena i messaggi saranno disponibili.</string>
<string name="la_notice_title_simplex_lock">SimpleX Lock</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">Per proteggere le tue informazioni, attiva SimpleX Lock.
\nTi verrà chiesto di completare l\'autenticazione prima di attivare questa funzionalità.</string>
<string name="auth_simplex_lock_turned_on">SimpleX Lock attivo</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Dovrai autenticarti quando avvii o riapri l\'app dopo 30 secondi in secondo piano.</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">L\'autenticazione del dispositivo non è attiva. Potrai attivare SimpleX Lock nelle impostazioni, quando avrai attivato l\'autenticazione del dispositivo.</string>
<string name="delete_message_mark_deleted_warning">Il messaggio verrà contrassegnato per l\'eliminazione. I destinatari potranno rivelare questo messaggio.</string>
<string name="share_message">Condividi messaggio…</string>
<string name="contact_sent_large_file">Il tuo contatto ha inviato un file più grande della dimensione massima supportata (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Il contatto e tutti i messaggi verranno eliminati, non è reversibile!</string>
<string name="switch_receiving_address_desc">Questa funzionalità è sperimentale! Funzionerà solo se l\'altro client ha la versione 4.2 installata. Dovresti vedere il messaggio nella conversazione una volta completato il cambio di indirizzo. Controlla di potere ancora ricevere messaggi da questo contatto (o membro del gruppo).</string>
<string name="only_group_owners_can_enable_voice">Solo i proprietari del gruppo possono attivare i messaggi vocali.</string>
<string name="send_live_message_desc">Invia un messaggio in diretta: si aggiornerà per i destinatari mentre lo digiti</string>
<string name="chat_item_ttl_day">1 giorno</string>
<string name="a_plus_b">a + b</string>
<string name="about_simplex_chat">Riguardo <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="group_member_role_admin">amministratore</string>
<string name="chat_item_ttl_week">1 settimana</string>
<string name="smp_servers_add_to_another_device">Aggiungi ad un altro dispositivo</string>
<string name="accept">Accetta</string>
<string name="v4_2_group_links_desc">Gli amministratori possono creare i link per entrare nei gruppi.</string>
<string name="allow_disappearing_messages_only_if">Consenti i messaggi a tempo solo se il tuo contatto li consente.</string>
<string name="allow_to_delete_messages">Permetti di eliminare irreversibilmente i messaggi inviati.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Permetti ai tuoi contatti di inviare messaggi a tempo.</string>
<string name="accept_requests">Accetta le richieste</string>
<string name="network_enable_socks_info">Accedere ai server via proxy SOCKS sulla porta 9050\? Il proxy deve essere avviato prima di attivare questa opzione.</string>
<string name="v4_3_improved_server_configuration_desc">Aggiungi server scansionando codici QR.</string>
<string name="all_group_members_will_remain_connected">Tutti i membri del gruppo resteranno connessi.</string>
<string name="allow_irreversible_message_deletion_only_if">Consenti l\'eliminazione irreversibile dei messaggi solo se il contatto la consente a te.</string>
<string name="above_then_preposition_continuation">sopra, quindi:</string>
<string name="accept_contact_button">Accetta</string>
<string name="accept_connection_request__question">Accettare la richiesta di connessione\?</string>
<string name="accept_contact_incognito_button">Accetta in incognito</string>
<string name="clear_chat_warning">Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te.</string>
<string name="smp_servers_preset_add">Aggiungi server preimpostati</string>
<string name="smp_servers_add">Aggiungi server…</string>
<string name="network_settings">Impostazioni di rete avanzate</string>
<string name="about_simplex">Riguardo SimpleX</string>
<string name="callstatus_accepted">chiamata accettata</string>
<string name="accept_call_on_lock_screen">Accetta</string>
<string name="color_primary">Principale</string>
<string name="accept_feature">Accetta</string>
<string name="allow_voice_messages_only_if">Consenti i messaggi vocali solo se il tuo contatto li consente.</string>
<string name="allow_your_contacts_irreversibly_delete">Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati.</string>
<string name="allow_direct_messages">Permetti l\'invio di messaggi diretti ai membri.</string>
<string name="allow_to_send_disappearing">Permetti l\'invio di messaggi a tempo.</string>
<string name="allow_to_send_voice">Permetti l\'invio di messaggi vocali.</string>
<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, 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>
<string name="chat_database_deleted">Database della chat eliminato</string>
<string name="settings_section_title_icon">ICONA APP</string>
<string name="incognito_random_profile_from_contact_description">Verrà inviato un profilo casuale al contatto da cui hai ricevuto questo link</string>
<string name="incognito_random_profile_description">Verrà inviato un profilo casuale al tuo contatto</string>
<string name="onboarding_notifications_mode_off_desc"><b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione, il servizio in secondo piano NON verrà usato.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Consuma più batteria</b>! Il servizio in secondo piano è sempre attivo: le notifiche verranno mostrate non appena i messaggi saranno disponibili.</string>
<string name="callstatus_calling">chiamata…</string>
<string name="icon_descr_cancel_link_preview">annulla anteprima link</string>
<string name="cannot_access_keychain">Impossibile accedere al Keystore per salvare la password del database</string>
<string name="alert_title_cant_invite_contacts">Impossibile invitare i contatti!</string>
<string name="change_role">Cambia ruolo</string>
<string name="chat_archive_section">ARCHIVIO CHAT</string>
<string name="snd_conn_event_switch_queue_phase_changing">cambio indirizzo…</string>
<string name="chat_is_stopped">Chat fermata</string>
<string name="group_member_status_introduced">connessione (presentato)</string>
<string name="contact_requests">Richieste del contatto</string>
<string name="connection_request_sent">Richiesta di connessione inviata!</string>
<string name="delete_link_question">Eliminare il link\?</string>
<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="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>
<string name="delete_group_menu_action">Elimina</string>
<string name="direct_messages_are_prohibited_in_chat">I messaggi diretti tra i membri sono vietati in questo gruppo.</string>
<string name="display_name">Nome da mostrare</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Aggiungi un contatto</b>: per creare il tuo codice QR una tantum per il tuo contatto.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scansiona codice QR</b>: per connetterti al contatto che ti mostra il codice QR.</string>
<string name="choose_file">Scegli file</string>
<string name="clear_chat_button">Svuota chat</string>
<string name="clear_chat_question">Svuotare la chat\?</string>
<string name="clear_verb">Svuota</string>
<string name="connect_via_link_or_qr">Connetti via link / codice QR</string>
<string name="copied">Copiato negli appunti</string>
<string name="share_one_time_link">Crea link di invito una tantum</string>
<string name="create_group">Crea gruppo segreto</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scansiona dall\'app il codice QR mostrato, tramite <b>Scansiona codice QR</b>.</string>
<string name="from_gallery_button">Dalla Galleria</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Se scegli di rifiutare, il mittente NON verrà avvisato.</string>
<string name="clear_chat_menu_action">Svuota</string>
<string name="icon_descr_close_button">Pulsante di chiusura</string>
<string name="alert_title_contact_connection_pending">Il contatto non è ancora connesso!</string>
<string name="delete_contact_menu_action">Elimina</string>
<string name="delete_pending_connection__question">Eliminare la connessione in attesa\?</string>
<string name="icon_descr_email">Email</string>
<string name="icon_descr_help">aiuto</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Se non potete incontrarvi di persona, <b>mostra il codice QR nella videochiamata</b>, oppure condividi il link.</string>
<string name="chat_console">Console della chat</string>
<string name="clear_verification">Annulla la verifica</string>
<string name="connect_button">Connetti</string>
<string name="connect_via_link">Connetti via link</string>
<string name="create_one_time_link">Crea link di invito una tantum</string>
<string name="database_passphrase_and_export">Password del database ed esportazione</string>
<string name="smp_servers_enter_manually">Inserisci il server manualmente</string>
<string name="how_to_use_simplex_chat">Come si usa</string>
<string name="all_your_contacts_will_remain_connected">Tutti i tuoi contatti resteranno connessi.</string>
<string name="appearance_settings">Aspetto</string>
<string name="smp_servers_check_address">Controlla l\'indirizzo del server e riprova.</string>
<string name="configure_ICE_servers">Configura server ICE</string>
<string name="contribute">Contribuisci</string>
<string name="delete_address">Elimina indirizzo</string>
<string name="delete_address__question">Eliminare l\'indirizzo\?</string>
<string name="smp_servers_delete_server">Elimina server</string>
<string name="error_saving_ICE_servers">Errore nel salvataggio dei server ICE</string>
<string name="how_to">Come si fa</string>
<string name="how_to_use_your_servers">Come usare i tuoi server</string>
<string name="enter_one_ICE_server_per_line">Server ICE (uno per riga)</string>
<string name="accept_automatically">Automaticamente</string>
<string name="bold">grassetto</string>
<string name="callstatus_ended">chiamata terminata <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_error">errore di chiamata</string>
<string name="callstatus_in_progress">chiamata in corso</string>
<string name="colored">colorato</string>
<string name="callstate_connected">connesso</string>
<string name="callstate_connecting">connessione…</string>
<string name="callstatus_connecting">connessione chiamata…</string>
<string name="create_profile_button">Crea</string>
<string name="create_profile">Crea profilo</string>
<string name="delete_image">Elimina immagine</string>
<string name="display_name__field">Nome da mostrare:</string>
<string name="display_name_cannot_contain_whitespace">Il nome da mostrare non può contenere spazi.</string>
<string name="edit_image">Modifica immagine</string>
<string name="exit_without_saving">Esci senza salvare</string>
<string name="full_name__field">Nome completo:</string>
<string name="full_name_optional__prompt">Nome completo (facoltativo)</string>
<string name="how_to_use_markdown">Come usare il markdown</string>
<string name="icon_descr_audio_call">chiamata audio</string>
<string name="audio_call_no_encryption">chiamata audio (non crittografata e2e)</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Buono per la batteria</b>. Il servizio in secondo piano controlla nuovi messaggi ogni 10 minuti. Potresti perdere chiamate e messaggi urgenti.</string>
<string name="call_already_ended">Chiamata già terminata!</string>
<string name="create_your_profile">Crea il tuo profilo</string>
<string name="decentralized">Decentralizzato</string>
<string name="encrypted_audio_call">Chiamata crittografata e2e</string>
<string name="encrypted_video_call">Videochiamata crittografata e2e</string>
<string name="callstate_ended">terminata</string>
<string name="how_it_works">Come funziona</string>
<string name="how_simplex_works">Come funziona <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="answer_call">Rispondi alla chiamata</string>
<string name="icon_descr_audio_off">Audio spento</string>
<string name="icon_descr_audio_on">Audio acceso</string>
<string name="settings_audio_video_calls">Chiamate audio e video</string>
<string name="auto_accept_images">Auto-accetta immagini</string>
<string name="integrity_msg_bad_hash">hash del messaggio errato</string>
<string name="integrity_msg_bad_id">ID messaggio errato</string>
<string name="icon_descr_call_ended">Chiamata terminata</string>
<string name="icon_descr_call_progress">Chiamata in corso</string>
<string name="call_on_lock_screen">Chiamate sulla schermata di blocco:</string>
<string name="icon_descr_call_connecting">Connessione chiamata</string>
<string name="connect_calls_via_relay">Connetti via relay</string>
<string name="status_contact_has_e2e_encryption">il contatto ha la crittografia e2e</string>
<string name="status_contact_has_no_e2e_encryption">il contatto non ha la crittografia e2e</string>
<string name="no_call_on_lock_screen">Disattiva</string>
<string name="integrity_msg_duplicate">messaggio duplicato</string>
<string name="status_e2e_encrypted">crittografato e2e</string>
<string name="allow_accepting_calls_from_lock_screen">Attiva le chiamate dalla schermata di blocco tramite le impostazioni.</string>
<string name="icon_descr_flip_camera">Fotocamera frontale/posteriore</string>
<string name="icon_descr_hang_up">Riaggancia</string>
<string name="settings_section_title_calls">CHIAMATE</string>
<string name="chat_database_section">DATABASE DELLA CHAT</string>
<string name="chat_database_imported">Database della chat importato</string>
<string name="chat_is_running">Chat in esecuzione</string>
<string name="settings_section_title_chats">CHAT</string>
<string name="set_password_to_export_desc">Il database è crittografato con una password casuale. Cambiala prima di esportare.</string>
<string name="database_passphrase">Password del database</string>
<string name="delete_chat_profile_question">Eliminare il profilo di chat\?</string>
<string name="delete_database">Elimina database</string>
<string name="settings_section_title_develop">SVILUPPA</string>
<string name="settings_developer_tools">Strumenti di sviluppo</string>
<string name="settings_section_title_device">DISPOSITIVO</string>
<string name="error_deleting_database">Errore nell\'eliminazione del database della chat</string>
<string name="error_exporting_chat_database">Errore nell\'esportazione del database della chat</string>
<string name="error_starting_chat">Errore nell\'avvio della chat</string>
<string name="error_stopping_chat">Errore nell\'interruzione della chat</string>
<string name="settings_experimental_features">Funzionalità sperimentali</string>
<string name="export_database">Esporta database</string>
<string name="settings_section_title_help">AIUTO</string>
<string name="chat_archive_header">Archivio chat</string>
<string name="chat_is_stopped_indication">Chat fermata</string>
<string name="archive_created_on_ts">Creato il <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="database_error">Errore del database</string>
<string name="passphrase_is_different">La password del database è diversa da quella salvata nel Keystore.</string>
<string name="delete_archive">Elimina archivio</string>
<string name="delete_chat_archive_question">Eliminare l\'archivio della chat\?</string>
<string name="encrypted_database">Database crittografato</string>
<string name="enter_correct_passphrase">Inserisci la password giusta.</string>
<string name="enter_passphrase">Inserisci la password…</string>
<string name="error_with_info">Errore: %s</string>
<string name="file_with_path">File: %s</string>
<string name="icon_descr_group_inactive">Gruppo inattivo</string>
<string name="rcv_conn_event_switch_queue_phase_completed">indirizzo cambiato per te</string>
<string name="rcv_group_event_changed_member_role">cambiato il ruolo di %s in %s</string>
<string name="rcv_group_event_changed_your_role">cambiato il tuo ruolo in %s</string>
<string name="rcv_conn_event_switch_queue_phase_changing">cambio indirizzo…</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">cambio indirizzo per %s…</string>
<string name="rcv_group_event_member_connected">connesso</string>
<string name="group_member_status_connected">connesso</string>
<string name="group_member_status_accepted">connessione (accettato)</string>
<string name="group_member_status_announced">connessione (annunciato)</string>
<string name="group_member_status_intro_invitation">connessione (invito di presentazione)</string>
<string name="rcv_group_event_group_deleted">gruppo eliminato</string>
<string name="group_member_status_group_deleted">gruppo eliminato</string>
<string name="group_invitation_expired">Invito al gruppo scaduto</string>
<string name="alert_message_group_invitation_expired">L\'invito al gruppo non è più valido, è stato rimosso dal mittente.</string>
<string name="alert_title_no_group">Gruppo non trovato!</string>
<string name="snd_group_event_group_profile_updated">profilo del gruppo aggiornato</string>
<string name="invite_prohibited">Impossibile invitare il contatto!</string>
<string name="change_verb">Cambia</string>
<string name="change_member_role_question">Cambiare il ruolo del gruppo\?</string>
<string name="clear_contacts_selection_button">Svuota</string>
<string name="group_member_status_complete">completo</string>
<string name="group_member_status_connecting">connessione</string>
<string name="icon_descr_contact_checked">Contatto controllato</string>
<string name="create_group_link">Crea link del gruppo</string>
<string name="group_member_status_creator">creatore</string>
<string name="info_row_database_id">ID database</string>
<string name="button_delete_group">Elimina gruppo</string>
<string name="delete_group_question">Eliminare il gruppo\?</string>
<string name="button_edit_group_profile">Modifica il profilo del gruppo</string>
<string name="error_creating_link_for_group">Errore nella creazione del link del gruppo</string>
<string name="error_deleting_link_for_group">Errore nell\'eliminazione del link del gruppo</string>
<string name="icon_descr_expand_role">Espandi la selezione dei ruoli</string>
<string name="section_title_for_console">PER CONSOLE</string>
<string name="group_link">Link del gruppo</string>
<string name="delete_group_for_all_members_cannot_undo_warning">Il gruppo verrà eliminato per tutti i membri. Non è reversibile!</string>
<string name="delete_group_for_self_cannot_undo_warning">Il gruppo verrà eliminato per te. Non è reversibile!</string>
<string name="info_row_connection">Connessione</string>
<string name="create_secret_group_title">Crea gruppo segreto</string>
<string name="conn_level_desc_direct">diretta</string>
<string name="network_option_enable_tcp_keep_alive">Attiva il keep-alive TCP</string>
<string name="error_changing_role">Errore nel cambio di ruolo</string>
<string name="error_removing_member">Errore nella rimozione del membro</string>
<string name="error_saving_group_profile">Errore nel salvataggio del profilo del gruppo</string>
<string name="info_row_group">Gruppo</string>
<string name="group_display_name_field">Nome da mostrare del gruppo:</string>
<string name="group_profile_is_stored_on_members_devices">Il profilo del gruppo è memorizzato sui dispositivi dei membri, non sui server.</string>
<string name="chat_preferences_always">sempre</string>
<string name="both_you_and_your_contacts_can_delete">Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Sia tu che il tuo contatto potete inviare messaggi a tempo.</string>
<string name="both_you_and_your_contact_can_send_voice">Sia tu che il tuo contatto potete inviare messaggi vocali.</string>
<string name="chat_preferences">Preferenze della chat</string>
<string name="chat_preferences_contact_allows">Il contatto lo consente</string>
<string name="contact_preferences">Preferenze del contatto</string>
<string name="contacts_can_mark_messages_for_deletion">I contatti possono contrassegnare i messaggi per l\'eliminazione; potrai vederli.</string>
<string name="theme_dark">Scuro</string>
<string name="chat_preferences_default">predefinito (%s)</string>
<string name="full_deletion">Elimina per tutti</string>
<string name="direct_messages">Messaggi diretti</string>
<string name="timed_messages">Messaggi a tempo</string>
<string name="disappearing_prohibited_in_this_chat">I messaggi a tempo sono vietati in questa conversazione.</string>
<string name="feature_enabled">attivato</string>
<string name="feature_enabled_for_contact">attivato per il contatto</string>
<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">%dg</string>
<string name="ttl_day">%d giorno</string>
<string name="ttl_days">%d giorni</string>
<string name="delete_after">Elimina dopo</string>
<string name="ttl_h">%do</string>
<string name="ttl_hour">%d ora</string>
<string name="ttl_hours">%d ore</string>
<string name="disappearing_messages_are_prohibited">I messaggi a tempo sono vietati in questo gruppo.</string>
<string name="ttl_m">%dm</string>
<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">%dmese</string>
<string name="ttl_s">%ds</string>
<string name="ttl_sec">%d sec</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>
<string name="group_members_can_delete">I membri del gruppo possono eliminare irreversibilmente i messaggi inviati.</string>
<string name="group_members_can_send_dms">I membri del gruppo possono inviare messaggi diretti.</string>
<string name="group_members_can_send_disappearing">I membri del gruppo possono inviare messaggi a tempo.</string>
<string name="group_members_can_send_voice">I membri del gruppo possono inviare messaggi vocali.</string>
<string name="v4_4_verify_connection_security_desc">Confronta i codici di sicurezza con i tuoi contatti.</string>
<string name="v4_4_disappearing_messages">Messaggi a tempo</string>
<string name="v4_3_improved_privacy_and_security_desc">Nascondi la schermata dell\'app nelle app recenti.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore verrà usato per memorizzare in modo sicuro la password dopo il riavvio dell\'app o la modifica della password; consentirà di ricevere le notifiche.</string>
<string name="impossible_to_recover_passphrase"><b>Nota bene</b>: NON potrai recuperare o cambiare la password se la perdi.</string>
<string name="change_database_passphrase_question">Cambiare password del database\?</string>
<string name="confirm_new_passphrase">Conferma password nuova…</string>
<string name="current_passphrase">Password attuale…</string>
<string name="database_encrypted">Database crittografato!</string>
<string name="database_passphrase_will_be_updated">La password di crittografia del database verrà aggiornata.</string>
<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_messages">Elimina messaggi</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>
<string name="encrypt_database">Crittografare</string>
<string name="error_changing_message_deletion">Errore nella modifica dell\'impostazione</string>
<string name="error_encrypting_database">Errore nella crittografia del database</string>
<string name="your_settings">Le tue impostazioni</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Verrai connesso/a al gruppo quando il dispositivo dell\'host del gruppo sarà in linea, attendi o controlla più tardi!</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Se hai ricevuto il link di invito a <xliff:g id="appName">SimpleX Chat</xliff:g>, puoi aprirlo nel tuo browser:</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobile: tocca <b>Apri nell\'app mobile</b>, quindi <b>Connetti</b> nell\'app.</string>
<string name="no_details">nessun dettaglio</string>
<string name="add_contact">Link di invito una tantum</string>
<string name="only_stored_on_members_devices">(memorizzato solo dai membri del gruppo)</string>
<string name="toast_permission_denied">Autorizzazione negata!</string>
<string name="reject_contact_button">Rifiuta</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scansiona o incolla dagli appunti)</string>
<string name="scan_QR_code">Scansiona codice QR</string>
<string name="add_contact_or_create_group">Inizia una nuova conversazione</string>
<string name="chat_help_tap_button">Tocca il pulsante</string>
<string name="thank_you_for_installing_simplex">Grazie per aver installato <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="to_connect_via_link_title">Per connettersi via link</string>
<string name="to_share_with_your_contact">(da condividere con il tuo contatto)</string>
<string name="to_start_a_new_chat_help_header">Per iniziare una nuova chat</string>
<string name="use_camera_button">Usa la fotocamera</string>
<string name="you_can_connect_to_simplex_chat_founder">Puoi <font color="#0088ff">connetterti con gli sviluppatori di <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per porre domande e ricevere aggiornamenti</font>.</string>
<string name="invalid_contact_link">Link non valido!</string>
<string name="invalid_QR_code">Codice QR non valido</string>
<string name="image_descr_link_preview">immagine di anteprima link</string>
<string name="mark_read">Segna come già letto</string>
<string name="mark_unread">Segna come non letto</string>
<string name="icon_descr_more_button">Altro</string>
<string name="mute_chat">Silenzia</string>
<string name="image_descr_profile_image">immagine del profilo</string>
<string name="icon_descr_profile_image_placeholder">segnaposto immagine del profilo</string>
<string name="image_descr_qr_code">Codice QR</string>
<string name="set_contact_name">Imposta il nome del contatto</string>
<string name="icon_descr_settings">Impostazioni</string>
<string name="show_QR_code">Mostra codice QR</string>
<string name="connection_you_accepted_will_be_cancelled">La connessione che hai accettato verrà annullata!</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Il contatto con cui hai condiviso questo link NON sarà in grado di connettersi!</string>
<string name="this_link_is_not_a_valid_connection_link">Questo non è un link di connessione valido!</string>
<string name="this_QR_code_is_not_a_link">Questo codice QR non è un link!</string>
<string name="unmute_chat">Riattiva audio</string>
<string name="contact_wants_to_connect_with_you">vuole connettersi con te!</string>
<string name="image_descr_simplex_logo">Logo di <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_address">Indirizzo di <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_simplex_team">Squadra di <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="you_accepted_connection">Hai accettato la connessione</string>
<string name="you_invited_your_contact">Hai invitato il contatto</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Il tuo profilo di chat verrà inviato
\nal tuo contatto</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Il tuo contatto può scansionare il codice QR dall\'app.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Il tuo contatto deve essere in linea per completare la connessione.
\nPuoi annullare questa connessione e rimuovere il contatto (e riprovare più tardi con un link nuovo).</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Verrai connesso/a quando la tua richiesta di connessione verrà accettata, attendi o controlla più tardi!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Verrai connesso/a quando il dispositivo del tuo contatto sarà in linea, attendi o controlla più tardi!</string>
<string name="incorrect_code">Codice di sicurezza sbagliato!</string>
<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/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>
<string name="smp_servers_preset_server">Server preimpostato</string>
<string name="smp_servers_preset_address">Indirizzo server preimpostato</string>
<string name="smp_servers_save">Salva i server</string>
<string name="scan_code">Scansiona codice</string>
<string name="scan_code_from_contacts_app">Scansiona il codice di sicurezza dall\'app del tuo contatto.</string>
<string name="smp_servers_scan_qr">Scansiona codice QR del server</string>
<string name="security_code">Codice di sicurezza</string>
<string name="chat_with_the_founder">Invia domande e idee</string>
<string name="send_us_an_email">Inviaci un\'email</string>
<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/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>
<string name="smp_servers_test_servers">Testa i server</string>
<string name="this_string_is_not_a_connection_link">Questa stringa non è un link di connessione!</string>
<string name="to_verify_compare">Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi.</string>
<string name="smp_servers_use_server_for_new_conn">Usa per connessioni nuove</string>
<string name="smp_servers_use_server">Usa il server</string>
<string name="you_can_also_connect_by_clicking_the_link">Puoi anche connetterti cliccando il link. Se si apre nel browser, clicca il pulsante <b>Apri nell\'app mobile</b>.</string>
<string name="your_profile_will_be_sent">Il tuo profilo di chat verrà inviato al tuo contatto</string>
<string name="your_contact_address">Il tuo indirizzo di contatto</string>
<string name="smp_servers_your_server">Il tuo server</string>
<string name="smp_servers_your_server_address">L\'indirizzo del tuo server</string>
<string name="your_simplex_contact_address">Il tuo indirizzo di contatto di <xliff:g id="appName">SimpleX</xliff:g>.</string>
<string name="network_disable_socks_info">Se confermi, i server di messaggistica saranno in grado di vedere il tuo indirizzo IP e il tuo fornitore, a quali server ti stai connettendo.</string>
<string name="install_simplex_chat_for_terminal">Installa <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per terminale</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi.</string>
<string name="network_and_servers">Rete e server</string>
<string name="network_settings_title">Impostazioni di rete</string>
<string name="network_use_onion_hosts_no">No</string>
<string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Gli host Onion saranno necessari per la connessione.</string>
<string name="network_use_onion_hosts_prefer_desc">Gli host Onion verranno usati quando disponibili.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Gli host Onion verranno usati quando disponibili.</string>
<string name="network_use_onion_hosts_no_desc">Gli host Onion non verranno usati.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Gli host Onion non verranno usati.</string>
<string name="rate_the_app">Valuta l\'app</string>
<string name="network_use_onion_hosts_required">Obbligatorio</string>
<string name="save_servers_button">Salva</string>
<string name="saved_ICE_servers_will_be_removed">I server WebRTC ICE salvati verranno rimossi.</string>
<string name="share_link">Condividi link</string>
<string name="star_on_github">Stella su GitHub</string>
<string name="update_onion_hosts_settings_question">Aggiornare l\'impostazione degli host .onion\?</string>
<string name="network_disable_socks">Usare una connessione internet diretta\?</string>
<string name="network_use_onion_hosts">Usa gli host .onion</string>
<string name="network_enable_socks">Usare il proxy SOCKS\?</string>
<string name="network_socks_toggle">Usa il proxy SOCKS (porta 9050)</string>
<string name="use_simplex_chat_servers__question">Usare i server di <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\?</string>
<string name="using_simplex_chat_servers">Stai usando i server di <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
<string name="network_use_onion_hosts_prefer">Quando disponibili</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te. Non perderai i tuoi contatti se in seguito lo elimini.</string>
<string name="your_ICE_servers">I tuoi server ICE</string>
<string name="your_SMP_servers">I tuoi server SMP</string>
<string name="italic">corsivo</string>
<string name="callstatus_missed">chiamata persa</string>
<string name="callstate_received_answer">risposta ricevuta…</string>
<string name="callstate_received_confirmation">conferma ricevuta…</string>
<string name="callstatus_rejected">chiamata rifiutata</string>
<string name="save_and_notify_contact">Salva e avvisa il contatto</string>
<string name="save_and_notify_contacts">Salva e avvisa i contatti</string>
<string name="save_and_notify_group_members">Salva e avvisa i membri del gruppo</string>
<string name="save_preferences_question">Salvare le preferenze\?</string>
<string name="secret">segreto</string>
<string name="callstate_starting">avvio…</string>
<string name="strikethrough">barrato</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La piattaforma di messaggistica che protegge la tua privacy e sicurezza.</string>
<string name="profile_is_only_shared_with_your_contacts">Il profilo è condiviso solo con i tuoi contatti.</string>
<string name="callstate_waiting_for_answer">in attesa di risposta…</string>
<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 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_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
\nI server di <xliff:g id="appName">SimpleX</xliff:g> non possono vedere il tuo profilo.</string>
<string name="ignore">Ignora</string>
<string name="immune_to_spam_and_abuse">Immune a spam e abusi</string>
<string name="incoming_audio_call">Chiamata in arrivo</string>
<string name="incoming_video_call">Videochiamata in arrivo</string>
<string name="onboarding_notifications_mode_service">Istantaneo</string>
<string name="onboarding_notifications_mode_subtitle">Può essere cambiato in seguito via impostazioni.</string>
<string name="make_private_connection">Crea una connessione privata</string>
<string name="many_people_asked_how_can_it_deliver">Molte persone hanno chiesto: <i>se <xliff:g id="appName">SimpleX</xliff:g> non ha identificatori utente, come può recapitare i messaggi\?</i></string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Solo i dispositivi client memorizzano i profili utente, i contatti, i gruppi e i messaggi inviati con <b>crittografia end-to-end a 2 livelli</b>.</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocollo e codice open source: chiunque può gestire i server.</string>
<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">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>
<string name="reject">Rifiuta</string>
<string name="first_platform_without_user_ids">La prima piattaforma senza alcun identificatore utente privata by design.</string>
<string name="next_generation_of_private_messaging">La nuova generazione di messaggistica privata</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Per proteggere la privacy, invece degli ID utente usati da tutte le altre piattaforme, <xliff:g id="appName">SimpleX</xliff:g> dispone di identificatori per le code dei messaggi, separati per ciascuno dei tuoi contatti.</string>
<string name="use_chat">Usa la chat</string>
<string name="icon_descr_video_call">videochiamata</string>
<string name="video_call_no_encryption">videochiamata (non crittografata e2e)</string>
<string name="onboarding_notifications_mode_off">Quando l\'app è in esecuzione</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> vuole connettersi con te via</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Puoi controllare attraverso quale/i server <b>ricevere</b> i messaggi, i tuoi contatti i server che usi per inviare loro i messaggi.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Può accadere quando:
\n1. I messaggi scadono sul server se non sono stati ricevuti per 30 giorni,
\n2. Il server usato per ricevere i messaggi da questo contatto è stato aggiornato e riavviato.
\n3. La connessione è compromessa.
\nConnettiti agli sviluppatori tramite Impostazioni per ricevere aggiornamenti riguardo i server.
\nAggiungeremo la ridondanza del server per prevenire la perdita di messaggi.</string>
<string name="icon_descr_call_rejected">Chiamata rifiutata</string>
<string name="icon_descr_call_missed">Chiamata persa</string>
<string name="status_no_e2e_encryption">nessuna crittografia e2e</string>
<string name="open_verb">Apri</string>
<string name="open_simplex_chat_to_accept_call">Apri <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per accettare la chiamata</string>
<string name="call_connection_peer_to_peer">peer-to-peer</string>
<string name="icon_descr_call_pending_sent">Chiamata in sospeso</string>
<string name="privacy_and_security">Privacy e sicurezza</string>
<string name="protect_app_screen">Proteggi la schermata dell\'app</string>
<string name="show_call_on_lock_screen">Mostra</string>
<string name="alert_title_skipped_messages">Messaggi saltati</string>
<string name="icon_descr_speaker_off">Altoparlante spento</string>
<string name="icon_descr_speaker_on">Altoparlante acceso</string>
<string name="call_connection_via_relay">via relay</string>
<string name="icon_descr_video_off">Video off</string>
<string name="icon_descr_video_on">Video on</string>
<string name="webrtc_ice_servers">Server WebRTC ICE</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> messaggio/i saltato/i</string>
<string name="your_calls">Le tue chiamate</string>
<string name="your_ice_servers">I tuoi server ICE</string>
<string name="your_privacy">La tua privacy</string>
<string name="import_database_confirmation">Importa</string>
<string name="import_database_question">Importare il database della chat\?</string>
<string name="import_database">Importa database</string>
<string name="settings_section_title_incognito">Modalità incognito</string>
<string name="settings_section_title_messages">MESSAGGI</string>
<string name="new_database_archive">Nuovo archivio database</string>
<string name="old_database_archive">Vecchio archivio del database</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Riavvia l\'app per creare un profilo di chat nuovo.</string>
<string name="restart_the_app_to_use_imported_chat_database">Riavvia l\'app per usare il database della chat importato.</string>
<string name="run_chat_section">AVVIA CHAT</string>
<string name="send_link_previews">Invia anteprime dei link</string>
<string name="set_password_to_export">Imposta la password per esportare</string>
<string name="settings_section_title_settings">IMPOSTAZIONI</string>
<string name="settings_section_title_socks">PROXY SOCKS</string>
<string name="stop_chat_confirmation">Ferma</string>
<string name="stop_chat_question">Fermare la chat\?</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Ferma la chat per esportare, importare o eliminare il database della chat. Non potrai ricevere e inviare messaggi mentre la chat è ferma.</string>
<string name="settings_section_title_support">SUPPORTA SIMPLEX CHAT</string>
<string name="settings_section_title_themes">TEMI</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</string>
<string name="settings_section_title_you">TU</string>
<string name="your_chat_database">Il tuo database della chat</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Il tuo attuale database di chat verrà ELIMINATO e SOSTITUITO con quello importato.
\nQuesta azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</string>
<string name="alert_title_group_invitation_expired">Invito scaduto!</string>
<string name="group_invitation_item_description">invito al gruppo <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="icon_descr_add_members">Invita membri</string>
<string name="join_group_button">Entra</string>
<string name="join_group_question">Entrare nel gruppo\?</string>
<string name="join_group_incognito_button">Entra in incognito</string>
<string name="joining_group">Ingresso nel gruppo</string>
<string name="keychain_error">Errore del portachiavi</string>
<string name="leave_group_button">Esci</string>
<string name="leave_group_question">Uscire dal gruppo\?</string>
<string name="open_chat">Apri chat</string>
<string name="restore_passphrase_not_found_desc">Password non trovata nel Keystore, inseriscila a mano. Potrebbe essere successo se hai ripristinato i dati dell\'app usando uno strumento di backup. In caso contrario, contatta gli sviluppatori.</string>
<string name="restore_database_alert_desc">Inserisci la password precedente dopo aver ripristinato il backup del database. Questa azione non può essere annullata.</string>
<string name="store_passphrase_securely_without_recover">Conserva la password in modo sicuro, NON potrai accedere alla chat se la perdi.</string>
<string name="restore_database_alert_confirm">Ripristina</string>
<string name="restore_database">Ripristina backup del database</string>
<string name="restore_database_alert_title">Ripristinare il backup del database\?</string>
<string name="database_restore_error">Errore di ripristino del database</string>
<string name="save_archive">Salva archivio</string>
<string name="save_passphrase_and_open_chat">Salva la password e apri la chat</string>
<string name="database_backup_can_be_restored">Il tentativo di cambiare la password del database non è stato completato.</string>
<string name="unknown_database_error_with_info">Errore del database sconosciuto: %s</string>
<string name="unknown_error">Errore sconosciuto</string>
<string name="wrong_passphrase">Password del database sbagliata</string>
<string name="wrong_passphrase_title">Password sbagliata!</string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Sei stato/a invitato/a al gruppo. Entra per connetterti con i suoi membri.</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puoi avviare la chat tramite Impostazioni -&gt; Database o riavviando l\'app.</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sei entrato/a in questo gruppo. Connessione al membro del gruppo invitante.</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata.</string>
<string name="group_member_status_invited">invitato</string>
<string name="rcv_group_event_invited_via_your_group_link">invitato via link del tuo gruppo</string>
<string name="rcv_group_event_member_added">invitato <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_left">uscito/a</string>
<string name="group_member_status_left">uscito/a</string>
<string name="group_member_role_member">membro</string>
<string name="group_member_role_owner">proprietario</string>
<string name="group_member_status_removed">rimosso</string>
<string name="rcv_group_event_member_deleted">rimosso <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_user_deleted">sei stato/a rimosso/a</string>
<string name="group_invitation_tap_to_join">Tocca per entrare</string>
<string name="group_invitation_tap_to_join_incognito">Toccare per entrare in incognito</string>
<string name="alert_message_no_group">Questo gruppo non esiste più.</string>
<string name="rcv_group_event_updated_group_profile">profilo del gruppo aggiornato</string>
<string name="you_are_invited_to_group">Sei stato/a invitato/a al gruppo</string>
<string name="snd_conn_event_switch_queue_phase_completed">hai cambiato indirizzo</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">hai cambiato l\'indirizzo per %s</string>
<string name="snd_group_event_changed_role_for_yourself">hai cambiato il tuo ruolo in %s</string>
<string name="snd_group_event_changed_member_role">hai cambiato il ruolo di %s in %s</string>
<string name="you_joined_this_group">Sei entrato/a in questo gruppo</string>
<string name="snd_group_event_user_left">sei uscito/a</string>
<string name="you_rejected_group_invitation">Hai rifiutato l\'invito al gruppo</string>
<string name="snd_group_event_member_deleted">hai rimosso <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="alert_title_cant_invite_contacts_descr">Stai usando un profilo in incognito per questo gruppo: per impedire la condivisione del tuo profilo principale non è consentito invitare contatti</string>
<string name="you_sent_group_invitation">Hai inviato un invito al gruppo</string>
<string name="button_add_members">Invita membri</string>
<string name="invite_to_group_button">Invita al gruppo</string>
<string name="button_leave_group">Esci dal gruppo</string>
<string name="info_row_local_name">Nome locale</string>
<string name="member_info_section_title_member">MEMBRO</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Il membro verrà rimosso dal gruppo, non è reversibile!</string>
<string name="new_member_role">Nuovo ruolo del membro</string>
<string name="no_contacts_selected">Nessun contatto selezionato</string>
<string name="no_contacts_to_add">Nessun contatto da aggiungere</string>
<string name="only_group_owners_can_change_prefs">Solo i proprietari del gruppo possono modificarne le preferenze.</string>
<string name="remove_member_confirmation">Rimuovi</string>
<string name="button_remove_member">Rimuovi membro</string>
<string name="role_in_group">Ruolo</string>
<string name="select_contacts">Seleziona i contatti</string>
<string name="button_send_direct_message">Invia messaggio diretto</string>
<string name="skip_inviting_button">Salta l\'invito di membri</string>
<string name="switch_verb">Cambia</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contatto/i selezionato/i</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBRI</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini.</string>
<string name="invite_prohibited_description">Stai tentando di invitare un contatto con cui hai condiviso un profilo in incognito nel gruppo in cui stai usando il tuo profilo principale</string>
<string name="group_info_member_you">tu: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="incognito">Incognito</string>
<string name="group_unsupported_incognito_main_profile_sent">La modalità in incognito non è supportata qui: il tuo profilo principale verrà inviato ai membri del gruppo</string>
<string name="incognito_info_protects">La modalità in incognito protegge la privacy del nome e dell\'immagine del tuo profilo principale: per ogni nuovo contatto viene creato un nuovo profilo casuale.</string>
<string name="conn_level_desc_indirect">indiretta (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="incognito_info_allows">Permette di avere molte connessioni anonime senza dati condivisi tra di loro in un unico profilo di chat.</string>
<string name="theme_light">Chiaro</string>
<string name="network_status">Stato della rete</string>
<string name="network_option_ping_interval">Intervallo PING</string>
<string name="network_option_protocol_timeout">Scadenza del protocollo</string>
<string name="receiving_via">Ricezione via</string>
<string name="network_options_reset_to_defaults">Ripristina i predefiniti</string>
<string name="network_options_revert">Annulla</string>
<string name="network_options_save">Salva</string>
<string name="save_group_profile">Salva il profilo del gruppo</string>
<string name="network_option_seconds_label">sec</string>
<string name="sending_via">Invio tramite</string>
<string name="conn_stats_section_title_servers">SERVER</string>
<string name="switch_receiving_address">Cambia indirizzo di ricezione</string>
<string name="theme_system">Sistema</string>
<string name="network_option_tcp_connection_timeout">Scadenza connessione TCP</string>
<string name="group_is_decentralized">Il gruppo è completamente decentralizzato: è visibile solo ai membri.</string>
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica.</string>
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito.</string>
<string name="incognito_info_find">Per trovare il profilo usato per una connessione in incognito, tocca il nome del contatto o del gruppo in cima alla chat.</string>
<string name="update_network_settings_confirmation">Aggiorna</string>
<string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</string>
<string name="incognito_info_share">Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano.</string>
<string name="group_main_profile_sent">Il tuo profilo di chat verrà inviato ai membri del gruppo</string>
<string name="incognito_random_profile">Il tuo profilo casuale</string>
<string name="message_deletion_prohibited">L\'eliminazione irreversibile dei messaggi è vietata in questa chat.</string>
<string name="chat_preferences_no">no</string>
<string name="chat_preferences_off">off</string>
<string name="feature_off">off</string>
<string name="chat_preferences_on">on</string>
<string name="only_you_can_delete_messages">Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l\'eliminazione).</string>
<string name="only_you_can_send_disappearing">Solo tu puoi inviare messaggi a tempo.</string>
<string name="only_your_contact_can_delete">Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l\'eliminazione).</string>
<string name="only_your_contact_can_send_disappearing">Solo il tuo contatto può inviare messaggi a tempo.</string>
<string name="prohibit_sending_disappearing_messages">Proibisci l\'invio di messaggi a tempo.</string>
<string name="prohibit_sending_voice_messages">Proibisci l\'invio di messaggi vocali.</string>
<string name="feature_received_prohibited">ricevuto, vietato</string>
<string name="reset_color">Ripristina i colori</string>
<string name="save_color">Salva colore</string>
<string name="accept_feature_set_1_day">Imposta 1 giorno</string>
<string name="set_group_preferences">Imposta le preferenze del gruppo</string>
<string name="theme">Tema</string>
<string name="voice_messages">Messaggi vocali</string>
<string name="chat_preferences_yes"></string>
<string name="chat_preferences_you_allow">Lo consenti</string>
<string name="your_preferences">Le tue preferenze</string>
<string name="v4_3_improved_server_configuration">Configurazione del server migliorata</string>
<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à 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>
<string name="prohibit_direct_messages">Proibisci l\'invio di messaggi diretti ai membri.</string>
<string name="prohibit_sending_disappearing">Proibisci l\'invio di messaggi a tempo.</string>
<string name="prohibit_sending_voice">Proibisci l\'invio di messaggi vocali.</string>
<string name="v4_2_security_assessment">Valutazione della sicurezza</string>
<string name="v4_2_security_assessment_desc">La sicurezza di SimpleX Chat è stata verificata da Trail of Bits.</string>
<string name="v4_3_voice_messages">Messaggi vocali</string>
<string name="voice_prohibited_in_this_chat">I messaggi vocali sono vietati in questa chat.</string>
<string name="voice_messages_are_prohibited">I messaggi vocali sono vietati in questo gruppo.</string>
<string name="whats_new">Novità</string>
<string name="v4_2_auto_accept_contact_requests_desc">Con messaggio di benvenuto facoltativo.</string>
<string name="v4_3_irreversible_message_deletion_desc">I tuoi contatti possono consentire l\'eliminazione completa dei messaggi.</string>
<string name="v4_3_improved_privacy_and_security">Privacy e sicurezza migliorate</string>
<string name="v4_4_live_messages">Messaggi in diretta</string>
<string name="v4_4_live_messages_desc">I destinatari vedono gli aggiornamenti mentre li digiti.</string>
<string name="v4_4_disappearing_messages_desc">I messaggi inviati verranno eliminati dopo il tempo impostato.</string>
<string name="v4_4_verify_connection_security">Verifica la sicurezza della connessione</string>
<string name="chat_item_ttl_none">mai</string>
<string name="new_passphrase">Nuova password…</string>
<string name="no_received_app_files">Nessun file ricevuto o inviato</string>
<string name="notifications_will_be_hidden">Le notifiche verranno mostrate solo fino all\'arresto dell\'app!</string>
<string name="enter_correct_current_passphrase">Inserisci la password attuale corretta.</string>
<string name="store_passphrase_securely">Conserva la password in modo sicuro, NON potrai cambiarla se la perdi.</string>
<string name="remove_passphrase">Rimuovi</string>
<string name="remove_passphrase_from_keychain">Rimuovere la password dal Keystore\?</string>
<string name="save_passphrase_in_keychain">Salva la password nel Keystore</string>
<string name="chat_item_ttl_seconds">%s secondo/i</string>
<string name="stop_chat_to_enable_database_actions">Ferma la chat per attivare le azioni del database.</string>
<string name="delete_files_and_media_desc">Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione.</string>
<string name="enable_automatic_deletion_message">Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti.</string>
<string name="update_database">Aggiorna</string>
<string name="update_database_passphrase">Aggiorna la password del database</string>
<string name="you_have_to_enter_passphrase_every_time">Devi inserire la password ogni volta che si avvia l\'app: non viene memorizzata sul dispositivo.</string>
<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,86 @@
<?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>
<string name="allow_your_contacts_to_send_voice_messages">送信相手からの音声メッセージを許可する。</string>
<string name="both_you_and_your_contact_can_send_voice">あなたと連絡相手が音声メッセージを送信できます。</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>電池省エネに良い</b>:バックグラウンド機能で10分毎に新着メッセージを確認します。通話と緊急メッセージを見逃す可能性があります。</string>
<string name="icon_descr_audio_off">音声オフ</string>
<string name="attach">添付する</string>
<string name="app_version_code">アプリ・ビルド番号: %s</string>
<string name="all_your_contacts_will_remain_connected">あなたの連絡先が繋がったまま継続します。</string>
<string name="accept_requests">リクエストを承諾</string>
<string name="accept_automatically">自動的に</string>
<string name="icon_descr_audio_on">音声オン</string>
<string name="integrity_msg_bad_hash">メッセージのハッシュ値問題</string>
<string name="integrity_msg_bad_id">メッセージIDの問題</string>
<string name="allow_verb">許可</string>
<string name="allow_voice_messages_question">音声メッセージを許可しますか?</string>
<string name="back">戻る</string>
<string name="appearance_settings">見た目</string>
<string name="app_version_title">アプリのバージョン</string>
<string name="app_version_name">アプリのバージョン: v%s</string>
<string name="network_session_mode_user_description"><b>アプリ内の各チャットプロフィールに、</b>.連絡先毎にそれぞれのTCP接続(とSOCKS資格情報)が使われます。</string>
<string name="network_session_mode_entity_description"><b>各連絡先とグループに、</b>それぞれのTCP接続(とSOCKS資格情報)が使われます。
\n<b>※注意※</b> 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。</string>
<string name="bold">太文字</string>
<string name="icon_descr_audio_call">音声通話</string>
<string name="settings_audio_video_calls">音声とビデオ通話</string>
<string name="impossible_to_recover_passphrase"><b>※注意※</b>:喪失したら、パスフレーズの回復・変更ができません。</string>
<string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string>
<string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string>
<string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。</string>
<string name="allow_voice_messages_only_if">送信相手も音声メッセージを許可する時のみに許可する。</string>
<string name="both_you_and_your_contact_can_send_disappearing">あなたと連絡相手が消えるメッセージを送信できます。</string>
<string name="v4_2_auto_accept_contact_requests">連絡先を自動的に承諾</string>
<string name="chat_preferences_always">常に</string>
<string name="notifications_mode_service">常にオン</string>
<string name="clear_chat_warning">全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ</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="allow_to_send_disappearing">消えるメッセージの送信を許可</string>
<string name="allow_direct_messages">メンバーへのダイレクトメッセージを許可</string>
<string name="allow_to_send_voice">音声メッセージの送信を許可</string>
<string name="notifications_mode_off_desc">アクティブの時のみに通知が出ます。バックグラウンド通知サービスは起動されません。</string>
<string name="keychain_is_storing_securely">Androidキーストアはパスフレーズの保管に使われます。通知機能に必要です。</string>
<string name="keychain_allows_to_receive_ntfs">再起動時とパスフレーズ変更時にAndroidキーストアがパスフレーズの保管に使われます。通知機能に必要です。</string>
<string name="answer_call">通話に応答</string>
<string name="settings_section_title_icon">アプリのアイコン</string>
<string name="full_backup">アプリデータのバックアップ</string>
<string name="incognito_random_profile_from_contact_description">このリンクの送信元にランダムなプロフィール(ダミー)が送られます。</string>
<string name="incognito_random_profile_description">連絡先にランダムなプロフィール(ダミー)が送られます。</string>
<string name="audio_call_no_encryption">音声通話 (エンドツーエンド暗号化なし)</string>
<string name="icon_descr_asked_to_receive">画像受信を依頼しました。</string>
<string name="auth_unavailable">認証不可能</string>
<string name="auto_accept_images">画像を自動的に受信</string>
<string name="notifications_mode_service_desc">バックグラウンド機能が常にオンで、メッセージが到着次第に通知が出ます。</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>新しい連絡先を追加</b>:使い捨てのQRコードを発行</string>
<string name="turning_off_service_and_periodic">電池省エネをオンに、バックグラウンド機能と定期的な受信依頼をオフにします。設定メニューにて変更できます。</string>
<string name="onboarding_notifications_mode_off_desc"><b>電池消費が最少</b>:アプリがアクティブ時のみに通知が出ます。</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>設定メニューにてオフにできます。</b> アプリがアクティブ時に通知が出ます。</string>
<string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QRコードを読み込み</b>:連絡相手のQRコードをスキャンすると繋がります。</string>
</resources>
@@ -0,0 +1,617 @@
<?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>
<string name="invalid_connection_link">Ongeldige verbindingslink</string>
<string name="service_notifications_disabled">Onmiddelijke meldingen zijn uitgeschakeld!</string>
<string name="service_notifications">Onmiddellijke berichten!</string>
<string name="notification_preview_new_message">nieuw bericht</string>
<string name="icon_descr_image_snd_complete">Afbeelding verzonden</string>
<string name="live_message">Live bericht!</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Als je een uitnodigingslink voor <xliff:g id="appName">SimpleX Chat</xliff:g> hebt ontvangen, kun je deze in je browser openen:</string>
<string name="onboarding_notifications_mode_subtitle">Dit kan later worden gewijzigd via instellingen.</string>
<string name="join_group_question">Deelnemen aan groep\?</string>
<string name="icon_descr_add_members">Nodig leden uit</string>
<string name="no_contacts_selected">Geen contacten geselecteerd</string>
<string name="v4_4_live_messages">Live berichten</string>
<string name="icon_descr_instant_notifications">Onmiddellijke meldingen</string>
<string name="notification_new_contact_request">Nieuw contactverzoek</string>
<string name="auth_log_in_using_credential">Log in met uw inloggegevens</string>
<string name="message_delivery_error_desc">Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd.</string>
<string name="delete_message_cannot_be_undone_warning">Bericht wordt verwijderd - dit kan niet ongedaan worden gemaakt!</string>
<string name="large_file">Groot bestand!</string>
<string name="mark_read">Markeer gelezen</string>
<string name="mark_unread">Markeer als ongelezen</string>
<string name="mute_chat">Stom</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Als u elkaar niet persoonlijk kunt ontmoeten, kunt u <b> de QR-code scannen in het videogesprek </b>, of uw contactpersoon kan een uitnodigingslink delen.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Als je elkaar niet persoonlijk kunt ontmoeten, <b>toon je de QR-code in het videogesprek</b> of deel je de link.</string>
<string name="invalid_QR_code">Ongeldige QR-code</string>
<string name="icon_descr_more_button">Meer</string>
<string name="incorrect_code">Onjuiste beveiligingscode!</string>
<string name="mark_code_verified">Markeer geverifieerd</string>
<string name="how_to_use_simplex_chat">Hoe te gebruiken</string>
<string name="markdown_help">Markdown-hulp</string>
<string name="markdown_in_messages">Markdown in berichten</string>
<string name="network_settings_title">Netwerkinstellingen</string>
<string name="how_to_use_markdown">Markdown gebruiken</string>
<string name="italic">cursief</string>
<string name="how_it_works">Hoe het werkt</string>
<string name="callstatus_missed">gemiste oproep</string>
<string name="how_simplex_works">Hoe <xliff:g id="appName">SimpleX</xliff:g> werkt</string>
<string name="many_people_asked_how_can_it_deliver">Veel mensen vroegen: <i>als <xliff:g id="appName">SimpleX</xliff:g> geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?</i></string>
<string name="incoming_audio_call">Inkomende audio-oproep</string>
<string name="incoming_video_call">Inkomend videogesprek</string>
<string name="ignore">Negeren</string>
<string name="status_no_e2e_encryption">geen e2e-encryptie</string>
<string name="import_database_question">Chatdatabase importeren\?</string>
<string name="chat_item_ttl_none">nooit</string>
<string name="no_received_app_files">Geen ontvangen of verzonden bestanden</string>
<string name="alert_title_group_invitation_expired">Uitnodiging verlopen!</string>
<string name="rcv_group_event_member_left">Verlaten</string>
<string name="group_member_status_left">Verlaten</string>
<string name="group_member_status_invited">uitgenodigd</string>
<string name="button_leave_group">Groep verlaten</string>
<string name="info_row_local_name">Lokale naam</string>
<string name="group_unsupported_incognito_main_profile_sent">Incognitomodus wordt hier niet ondersteund - uw hoofdprofiel wordt naar groepsleden verzonden</string>
<string name="users_delete_data_only">Alleen lokale profielgegevens</string>
<string name="message_deletion_prohibited_in_chat">Het onomkeerbaar verwijderen van berichten is verboden in deze groep.</string>
<string name="v4_3_improved_privacy_and_security_desc">App-scherm verbergen in de recente apps.</string>
<string name="settings_section_title_incognito">Incognito modus</string>
<string name="messages_section_title">Berichten</string>
<string name="new_passphrase">Nieuwe wachtwoordzin…</string>
<string name="keychain_error">Keychain fout</string>
<string name="join_group_button">Word lid van</string>
<string name="leave_group_question">Groep verlaten\?</string>
<string name="new_member_role">Nieuwe ledenrol</string>
<string name="no_contacts_to_add">Geen contacten om toe te voegen</string>
<string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel.</string>
<string name="theme_light">Licht</string>
<string name="chat_preferences_no">Nee</string>
<string name="v4_5_multiple_chat_profiles">Meerdere chatprofielen</string>
<string name="v4_5_italian_interface">Italiaanse interface</string>
<string name="v4_5_message_draft">Concept bericht</string>
<string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string>
<string name="button_add_members">Nodig leden uit</string>
<string name="notification_display_mode_hidden_desc">Verberg contact en bericht</string>
<string name="turn_off_battery_optimization">Om het te gebruiken &lt;b xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\"&gt;batterijoptimalisatie uitschakelen&lt;/b&gt; voor &lt;xliff:g xmlns:xliff=\"urn:oasis: names:tc:xliff:document:1.2\" id=\"appName\"&gt;SimpleX&lt;/xliff:g&gt; in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld.</string>
<string name="onboarding_notifications_mode_service">Onmiddellijk</string>
<string name="rcv_group_event_member_added">uitgenodigd <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="group_invitation_item_description">uitnodiging voor groep <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="invite_to_group_button">Uitnodigen voor groep</string>
<string name="join_group_incognito_button">Doe incognito mee</string>
<string name="group_preview_join_as">lid worden als %s</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Het kan gebeuren wanneer:
\n1. De berichten op de server verlopen als ze 30 dagen niet zijn ontvangen,
\n2. De server die u gebruikt om de berichten van deze contactpersoon te ontvangen, is bijgewerkt en opnieuw opgestart.
\n3. De verbinding is verbroken.
\nMaak verbinding met de ontwikkelaars via Instellingen om de updates over de servers te ontvangen.
\nWe zullen serverredundantie toevoegen om verloren berichten te voorkomen.</string>
<string name="joining_group">Deel nemen aan groep</string>
<string name="leave_group_button">Verlaten</string>
<string name="group_member_role_member">lid</string>
<string name="image_descr_link_preview">link voorbeeldafbeelding</string>
<string name="member_info_section_title_member">LID</string>
<string name="settings_section_title_messages">BERICHTEN</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobiel: tik op <b>Openen in mobiele app</b> en tik vervolgens op <b>Verbinden</b> in de app.</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Lid wordt uit de groep verwijderd - dit kan niet ongedaan worden gemaakt!</string>
<string name="message_delivery_error_title">Fout bij bezorging van bericht</string>
<string name="delete_message_mark_deleted_warning">Bericht wordt gemarkeerd voor verwijdering. De ontvanger(s) kunnen dit bericht onthullen.</string>
<string name="network_status">Netwerk status</string>
<string name="image_descr">Afbeelding</string>
<string name="image_saved">Afbeelding opgeslagen in Galerij</string>
<string name="import_database_confirmation">Importeren</string>
<string name="import_database">Database importeren</string>
<string name="v4_3_improved_privacy_and_security">Verbeterde privacy en veiligheid</string>
<string name="image_will_be_received_when_contact_is_online">De afbeelding wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later!</string>
<string name="incognito_info_protects">De incognitomodus beschermt de privacy van uw hoofdprofielnaam en -afbeelding - voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt.</string>
<string name="new_database_archive">Nieuw database-archief</string>
<string name="no_details">geen details</string>
<string name="conn_level_desc_indirect">indirect (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="thousand_abbreviation">k</string>
<string name="notification_preview_mode_message">Bericht tekst</string>
<string name="settings_notification_preview_title">Meldingsvoorbeeld</string>
<string name="settings_notifications_mode_title">Meldingsservice</string>
<string name="notifications">Meldingen</string>
<string name="invalid_contact_link">Ongeldige link!</string>
<string name="smp_servers_invalid_address">Ongeldig serveradres!</string>
<string name="install_simplex_chat_for_terminal">Installeer <xliff:g id="appNameFull">SimpleX Chat</xliff:g> voor terminal</string>
<string name="how_to">Hoe</string>
<string name="how_to_use_your_servers">Hoe u uw servers gebruikt</string>
<string name="network_and_servers">Netwerk &amp; servers</string>
<string name="enter_one_ICE_server_per_line">ICE-servers (één per lijn)</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Zorg ervoor dat WebRTC ICE-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
<string name="network_disable_socks_info">Als u bevestigt, kunnen de berichtenservers uw IP-adres zien en uw provider - met welke servers u verbinding maakt.</string>
<string name="network_use_onion_hosts_no">Nee</string>
<string name="immune_to_spam_and_abuse">Immuun voor spam en misbruik</string>
<string name="make_private_connection">Maak een privéverbinding</string>
<string name="message_deletion_prohibited">Het onomkeerbaar verwijderen van berichten is verboden in deze chat.</string>
<string name="new_in_version">Nieuw in %s</string>
<string name="v4_3_voice_messages_desc">Max 40 seconden, direct ontvangen.</string>
<string name="v4_3_improved_server_configuration">Verbeterde serverconfiguratie</string>
<string name="v4_3_irreversible_message_deletion">Onomkeerbare berichtverwijdering</string>
<string name="rcv_group_event_invited_via_your_group_link">uitgenodigd via je groepslink</string>
<string name="incognito">Incognito</string>
<string name="icon_descr_call_missed">Gemiste oproep</string>
<string name="description_via_contact_address_link_incognito">incognito via link naar contactadres</string>
<string name="description_via_group_link_incognito">incognito via groepslink</string>
<string name="description_via_one_time_link_incognito">incognito via eenmalige link</string>
<string name="invalid_chat">ongeldige chat</string>
<string name="invalid_data">onjuiste data</string>
<string name="invalid_message_format">ongeldig berichtformaat</string>
<string name="display_name_invited_to_connect">uitgenodigd om te verbinden</string>
<string name="live">LIVE</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Zorg ervoor dat SMP-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
<string name="marked_deleted_description">gemarkeerd als verwijderd</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</string>
<string name="image_descr_profile_image">profielfoto</string>
<string name="privacy_redefined">Privacy opnieuw gedefinieerd</string>
<string name="privacy_and_security">Privacy en beveiliging</string>
<string name="network_error_desc">Controleer uw netwerkverbinding met <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> en probeer het opnieuw.</string>
<string name="error_smp_test_certificate">Mogelijk is de certificaatvingerafdruk in het serveradres onjuist</string>
<string name="periodic_notifications">Periodieke meldingen</string>
<string name="auth_open_chat_console">Chatconsole openen</string>
<string name="toast_permission_denied">Geen toestemming!</string>
<string name="icon_descr_profile_image_placeholder">profielafbeelding tijdelijke aanduiding</string>
<string name="one_time_link">Eenmalige uitnodigingslink</string>
<string name="paste_button">Plakken</string>
<string name="smp_servers_preset_address">Vooraf ingesteld serveradres</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts worden niet gebruikt.</string>
<string name="onboarding_notifications_mode_title">Privé meldingen</string>
<string name="paste_the_link_you_received">Plak de ontvangen link</string>
<string name="onboarding_notifications_mode_periodic">Periodiek</string>
<string name="open_verb">Open</string>
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> om de oproep te accepteren</string>
<string name="call_connection_peer_to_peer">peer-to-peer</string>
<string name="notifications_will_be_hidden">Meldingen worden alleen afgeleverd totdat de app stopt!</string>
<string name="restore_passphrase_not_found_desc">Wachtwoordzin niet gevonden in Keystore, voer deze handmatig in. Dit kan zijn gebeurd als u de gegevens van de app hebt hersteld met een back-uptool. Als dit niet het geval is, neem dan contact op met de ontwikkelaars.</string>
<string name="users_delete_with_connections">Profiel- en serververbindingen</string>
<string name="chat_preferences_off">uit</string>
<string name="chat_preferences_on">aan</string>
<string name="only_you_can_send_disappearing">Alleen jij kunt verdwijnende berichten verzenden.</string>
<string name="only_your_contact_can_send_disappearing">Alleen uw contactpersoon kan verdwijnende berichten verzenden.</string>
<string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</string>
<string name="feature_offered_item_with_param">aangeboden %s: %2s</string>
<string name="old_database_archive">Oud database-archief</string>
<string name="enter_correct_current_passphrase">Voer de juiste huidige wachtwoordzin in.</string>
<string name="group_member_role_owner">eigenaar</string>
<string name="network_option_ping_count">PING-telling</string>
<string name="network_option_ping_interval">PING-interval</string>
<string name="v4_5_message_draft_descr">Bewaar het laatste berichtconcept, met bijlagen.</string>
<string name="v4_5_private_filenames">Privé bestandsnamen</string>
<string name="images_limit_desc">Er kunnen slechts 10 afbeeldingen tegelijk worden verzonden</string>
<string name="enter_passphrase_notification_title">Wachtwoordzin is nodig</string>
<string name="feature_off">uit</string>
<string name="add_contact">Eenmalige uitnodigingslink</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts zijn vereist voor verbinding.</string>
<string name="only_group_owners_can_change_prefs">Alleen groepseigenaren kunnen groepsvoorkeuren wijzigen.</string>
<string name="only_stored_on_members_devices">(alleen opgeslagen door groepsleden)</string>
<string name="paste_connection_link_below_to_connect">Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contactpersoon.</string>
<string name="smp_servers_preset_server">Vooraf ingestelde server</string>
<string name="periodic_notifications_disabled">Periodieke meldingen zijn uitgeschakeld!</string>
<string name="icon_descr_server_status_pending">In behandeling</string>
<string name="only_group_owners_can_enable_voice">Alleen groepseigenaren kunnen spraakberichten inschakelen.</string>
<string name="ask_your_contact_to_enable_voice">Vraag uw contactpersoon om het verzenden van spraakberichten in te schakelen.</string>
<string name="ok">OK</string>
<string name="network_use_onion_hosts_required_desc">Onion hosts zijn vereist voor verbinding.</string>
<string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts worden gebruikt indien beschikbaar.</string>
<string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code iedereen kan de servers draaien.</string>
<string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string>
<string name="only_your_contact_can_delete">Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string>
<string name="only_you_can_send_voice">Alleen jij kunt spraakberichten verzenden.</string>
<string name="only_your_contact_can_send_voice">Alleen uw contactpersoon kan spraakberichten verzenden.</string>
<string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string>
<string name="feature_offered_item">aangeboden %s</string>
<string name="store_passphrase_securely_without_recover">Sla de wachtwoordzin veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chat.</string>
<string name="store_passphrase_securely">Bewaar de wachtwoordzin veilig, u kunt deze NIET wijzigen als u deze kwijtraakt.</string>
<string name="open_chat">Chat openen</string>
<string name="restore_database_alert_desc">Voer het vorige wachtwoord in na het herstellen van de databaseback-up. Deze actie kan niet ongedaan gemaakt worden.</string>
<string name="icon_descr_call_pending_sent">Oproep in behandeling</string>
<string name="simplex_link_mode_browser_warning">Het openen van de link in de browser kan de privacy en beveiliging van de verbinding verminderen. Niet-vertrouwde SimpleX-links worden rood weergegeven.</string>
<string name="contact_developers">Werk de app bij en neem contact op met de ontwikkelaars.</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Alleen client-apparaten slaan gebruikersprofielen, contacten, groepen en berichten op die zijn verzonden met <b>2-laags end-to-end-codering</b>.</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>
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="thousand_abbreviation">т</string>
<!-- Connect via Link - MainActivity.kt -->
<string name="connect_via_contact_link">Соединиться через ссылку-контакт?</string>
<string name="connect_via_invitation_link">Соединиться через ссылку-приглашение?</string>
@@ -10,7 +9,6 @@
<string name="profile_will_be_sent_to_contact_sending_link">Ваш профиль будет отправлен контакту, от которого вы получили эту ссылку.</string>
<string name="you_will_join_group">Вы вступите в группу, на которую ссылается эта ссылка.</string>
<string name="connect_via_link_verb">Соединиться</string>
<!-- Server info - ChatModel.kt -->
<string name="server_connected">соединено</string>
<string name="server_error">ошибка</string>
@@ -18,7 +16,6 @@
<string name="connected_to_server_to_receive_messages_from_contact">Установлено соединение с сервером, через который вы получаете сообщения от этого контакта.</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
<string name="trying_to_connect_to_server_to_receive_messages">Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта.</string>
<!-- Item Content - ChatModel.kt -->
<string name="deleted_description">удалено</string>
<string name="marked_deleted_description">помечено к удалению</string>
@@ -27,7 +24,6 @@
<string name="sender_you_pronoun">вы</string>
<string name="unknown_message_format">неизвестный формат сообщения</string>
<string name="invalid_message_format">неверный формат сообщения</string>
<!-- PendingContactConnection - ChatModel.kt -->
<string name="connection_local_display_name">connection <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="display_name_connection_established">соединение установлено</string>
@@ -41,7 +37,6 @@
<string name="description_via_contact_address_link_incognito">инкогнито через ссылку-контакт</string>
<string name="description_via_one_time_link">через одноразовую ссылку</string>
<string name="description_via_one_time_link_incognito">инкогнито через одноразовую ссылку</string>
<!-- FormattedText, SimpleX links - ChatModel.kt -->
<string name="simplex_link_contact">SimpleX ссылка-контакт</string>
<string name="simplex_link_invitation">SimpleX одноразовая ссылка</string>
@@ -52,12 +47,10 @@
<string name="simplex_link_mode_full">Полная ссылка</string>
<string name="simplex_link_mode_browser">В браузере</string>
<string name="simplex_link_mode_browser_warning">Использование ссылки в браузере может уменьшить конфиденциальность и безопасность соединения. Ссылки на неизвестные сайты будут красными.</string>
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Ошибка при сохранении SMP серверов</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Пожалуйста, проверьте, что адреса SMP серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется.</string>
<string name="error_setting_network_config">Ошибка при сохранении настроек сети</string>
<!-- API Error Responses - SimpleXAPI.kt -->
<string name="connection_timeout">Превышено время соединения</string>
<string name="connection_error">Ошибка соединения</string>
@@ -90,7 +83,6 @@
<string name="smp_server_test_secure_queue">Защита очереди</string>
<string name="smp_server_test_delete_queue">Удаление очереди</string>
<string name="smp_server_test_disconnect">Разрыв соединения</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Мгновенные уведомления</string>
<string name="service_notifications">Мгновенные уведомления!</string>
@@ -106,16 +98,13 @@
<string name="enter_passphrase_notification_desc">Для получения уведомлений, пожалуйста, введите пароль от базы данных</string>
<string name="database_initialization_error_title">Ошибка базы данных</string>
<string name="database_initialization_error_desc">Ошибка при инициализации базы данных. Нажмите чтобы узнать больше</string>
<!-- SimpleX Chat foreground Service -->
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> сервис</string>
<string name="simplex_service_notification_text">Приём сообщений…</string>
<string name="hide_notification">Скрыть</string>
<!-- Notification channels -->
<string name="ntf_channel_messages">SimpleX Chat сообщения</string>
<string name="ntf_channel_calls">SimpleX Chat звонки</string>
<!-- Notifications -->
<string name="settings_notifications_mode_title">Сервис уведомлений</string>
<string name="settings_notification_preview_mode_title">Показывать уведомления</string>
@@ -136,12 +125,10 @@
<string name="notification_preview_new_message">новое сообщение</string>
<string name="notification_new_contact_request">Новый запрос на соединение</string>
<string name="notification_contact_connected">Соединен(а)</string>
<!-- local authentication notice - SimpleXAPI.kt -->
<string name="la_notice_title_simplex_lock">Блокировка SimpleX</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">Чтобы защитить вашу информацию, включите блокировку <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.\nВам будет нужно пройти аутентификацию для включения блокировки.</string>
<string name="la_notice_turn_on">Включить</string>
<!-- LocalAuthentication.kt -->
<string name="auth_simplex_lock_turned_on">Блокировка SimpleX включена</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.</string>
@@ -155,11 +142,9 @@
<string name="auth_device_authentication_is_disabled_turning_off">Аутентификация устройства выключена. Отключение блокировки SimpleX Chat.</string>
<string name="auth_stop_chat">Остановить чат</string>
<string name="auth_open_chat_console">Открыть консоль</string>
<!-- Chat Alerts - ChatItemView.kt -->
<string name="message_delivery_error_title">Ошибка доставки сообщения</string>
<string name="message_delivery_error_desc">Скорее всего, этот контакт удалил соединение с вами.</string>
<!-- Chat Actions - ChatItemView.kt (and general) -->
<string name="reply_verb">Ответить</string>
<string name="share_verb">Поделиться</string>
@@ -175,14 +160,12 @@
<string name="delete_message_mark_deleted_warning">Сообщение будет помечено на удаление. Получатель(и) сможет(смогут) посмотреть это сообщение.</string>
<string name="for_me_only">Удалить для меня</string>
<string name="for_everybody">Для всех</string>
<!-- CIMetaView.kt -->
<string name="icon_descr_edited">отредактировано</string>
<string name="icon_descr_sent_msg_status_sent">отправлено</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">ошибка авторизации при отправке</string>
<string name="icon_descr_sent_msg_status_send_failed">ошибка при отправке</string>
<string name="icon_descr_received_msg_status_unread">не прочитано</string>
<!-- ChatListView.kt -->
<string name="personal_welcome">Здравствуйте <xliff:g>%1$s</xliff:g>!</string>
<string name="welcome">Здравствуйте!</string>
@@ -195,12 +178,10 @@
<string name="tap_to_start_new_chat">Нажмите, чтобы начать чат</string>
<string name="chat_with_developers">Соединиться с разработчиками</string>
<string name="you_have_no_chats">У вас нет чатов</string>
<!-- ShareListView.kt -->
<string name="share_message">Отправить сообщение…</string>
<string name="share_image">Отправить изображение…</string>
<string name="share_file">Отправить файл…</string>
<!-- ComposeView.kt, helpers -->
<string name="attach">Прикрепить</string>
<string name="icon_descr_context">Значок контекста</string>
@@ -210,7 +191,6 @@
<string name="images_limit_desc">Только 10 изображений могут быть отправлены одномоментно</string>
<string name="image_decoding_exception_title">Ошибка декодирования</string>
<string name="image_decoding_exception_desc">Не получается декодировать изображение. Пожалуйста, попробуйте другое изображение или свяжитесь с разработчиками.</string>
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Изображение</string>
<string name="icon_descr_waiting_for_image">Ожидается прием изображения</string>
@@ -219,7 +199,6 @@
<string name="waiting_for_image">Ожидается прием изображения</string>
<string name="image_will_be_received_when_contact_is_online">Изображение будет принято, когда ваш контакт будет в сети, подождите или проверьте позже!</string>
<string name="image_saved">Изображение сохранено в Галерею</string>
<!-- Files - CIFileView.kt -->
<string name="icon_descr_file">Файл</string>
<string name="large_file">Большой файл!</string>
@@ -230,15 +209,12 @@
<string name="file_saved">Файл сохранен</string>
<string name="file_not_found">Файл не найден</string>
<string name="error_saving_file">Ошибка сохранения файла</string>
<!-- Voice messages -->
<string name="voice_message">Голосовое сообщение</string>
<string name="voice_message_with_duration">Голосовое сообщение (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="voice_message_send_text">Голосовое сообщение…</string>
<!-- Chat Info Settings - ChatInfoView.kt -->
<string name="notifications">Уведомления</string>
<!-- Chat Info Actions - ChatInfoView.kt -->
<string name="delete_contact_question">Удалить контакт?</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Контакт и все сообщения будут удалены - это действие нельзя отменить!</string>
@@ -250,7 +226,6 @@
<string name="icon_descr_server_status_pending">Ожидается соединение с сервером</string>
<string name="switch_receiving_address_question">Переключить адрес получения?</string>
<string name="switch_receiving_address_desc">Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</string>
<!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Отправить сообщение</string>
<string name="icon_descr_record_voice_message">Записать голосовое сообщение</string>
@@ -259,7 +234,6 @@
<string name="voice_messages_prohibited">Голосовые сообщения запрещены!</string>
<string name="ask_your_contact_to_enable_voice">Попросите вашего контакта разрешить отправку голосовых сообщений.</string>
<string name="only_group_owners_can_enable_voice">Только владельцы группы могут разрешить голосовые сообщения.</string>
<!-- General Actions / Responses -->
<string name="back">Назад</string>
<string name="cancel_verb">Отменить</string>
@@ -269,7 +243,6 @@
<string name="no_details">нет описания</string>
<string name="add_contact">Одноразовая ссылка</string>
<string name="copied">Скопировано в буфер обмена</string>
<!-- NewChatSheet -->
<string name="add_contact_or_create_group">Начать новый разговор</string>
<string name="share_one_time_link">Создать ссылку-приглашение</string>
@@ -279,13 +252,11 @@
<string name="to_share_with_your_contact">(чтобы отправить вашему контакту)</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(сканировать или вставить из буфера)</string>
<string name="only_stored_on_members_devices">(хранится только у членов группы)</string>
<!-- GetImageView -->
<string name="toast_permission_denied">Разрешение не получено!</string>
<string name="use_camera_button">Камера</string>
<string name="from_gallery_button">Галерея</string>
<string name="choose_file">Файлы</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Спасибо, что установили <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="you_can_connect_to_simplex_chat_founder">Вы можете <font color="#0088ff">соединиться с разработчиками</font>, чтобы задать любые вопросы или получать уведомления о новых версиях.</string>
@@ -298,14 +269,12 @@
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Если вы получили ссылку с приглашением из <xliff:g id="appName">SimpleX Chat</xliff:g>, вы можете открыть ее в браузере:</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 на компьютере: сосканируйте показанный QR код из приложения через <b>Сканировать QR код</b>.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 на мобильном: намжите кнопку <b>Open in mobile app</b> на веб странице, затем нажмите <b>Соединиться</b> в приложении.</string>
<!-- Contact Request Alert Dialogue - CharListNavLinkView.kt -->
<string name="accept_connection_request__question">Принять запрос на соединение?</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Отправителю НЕ будет послано уведомление, если вы отклоните запрос на соединение.</string>
<string name="accept_contact_button">Принять</string>
<string name="accept_contact_incognito_button">Принять инкогнито</string>
<string name="reject_contact_button">Отклонить</string>
<!-- Clear Chat - ChatListNavLinkView.kt -->
<string name="clear_chat_question">Очистить чат?</string>
<string name="clear_chat_warning">Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для вас.</string>
@@ -317,29 +286,23 @@
<string name="mark_read">Прочитано</string>
<string name="mark_unread">Не прочитано</string>
<string name="set_contact_name">Имя контакта</string>
<!-- Actions - ChatListNavLinkView.kt -->
<string name="mute_chat">Без звука</string>
<string name="unmute_chat">Уведомлять</string>
<!-- Pending contact connection alert dialogues -->
<string name="you_invited_your_contact">Вы пригласили ваш контакт</string>
<string name="you_accepted_connection">Вы приняли приглашение соединиться</string>
<string name="delete_pending_connection__question">Удалить ожидаемое соединение?</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Контакт, которому вы отправили эту ссылку, не сможет соединиться!</string>
<string name="connection_you_accepted_will_be_cancelled">Подтвержденное соединение будет отменено!</string>
<!-- Connection Pending Alert Dialogue - ChatListNavLinkView.kt -->
<string name="alert_title_contact_connection_pending">Соединение еще не установлено!</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ваш контакт должен быть в сети чтобы установить соединение.\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой).</string>
<!-- Contact Request Information - ContactRequestView.kt -->
<string name="contact_wants_to_connect_with_you">хочет соединиться с вами!</string>
<!-- Image Placeholder - ChatInfoImage.kt -->
<string name="icon_descr_profile_image_placeholder">аватар не установлен</string>
<string name="image_descr_profile_image">аватар</string>
<!-- Content Descriptions -->
<string name="icon_descr_close_button">закрыть</string>
<string name="image_descr_link_preview">изображение превью ссылки</string>
@@ -352,10 +315,8 @@
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> логотип</string>
<string name="icon_descr_email">Email</string>
<string name="icon_descr_more_button">Больше</string>
<!-- Connection info - ContactConnectionInfoView.kt -->
<string name="show_QR_code">Показать QR код</string>
<!-- Add Contact - AddContactView.kt -->
<string name="invalid_QR_code">Неверный QR код</string>
<string name="this_QR_code_is_not_a_link">Этот QR код не является ссылкой!</string>
@@ -372,16 +333,13 @@
<string name="share_invitation_link">Поделиться ссылкой</string>
<string name="paste_connection_link_below_to_connect">Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта.</string>
<string name="your_profile_will_be_sent">Ваш профиль будет отправлен вашему контакту</string>
<!-- PasteToConnect.kt -->
<string name="connect_button">Соединиться</string>
<string name="paste_button">Вставить</string>
<!-- CreateLinkView.kt -->
<string name="create_one_time_link">Создать одноразовую ссылку</string>
<string name="one_time_link">Одноразовая ссылка</string>
<string name="your_contact_address">Ваш SimpleX адрес</string>
<!-- settings - SettingsView.kt -->
<string name="your_settings">Настройки</string>
<string name="your_simplex_contact_address">Ваш <xliff:g id="appName">SimpleX</xliff:g> адрес</string>
@@ -450,7 +408,6 @@
<string name="network_use_onion_hosts_no_desc_in_alert">Onion хосты не используются.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Подключаться только к onion хостам.</string>
<string name="appearance_settings">Интерфейс</string>
<!-- Address Items - UserAddressView.kt -->
<string name="create_address">Создать адрес</string>
<string name="delete_address__question">Удалить адрес?</string>
@@ -458,17 +415,15 @@
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
<string name="share_link">Поделиться\nссылкой</string>
<string name="delete_address">Удалить\nадрес</string>
<!-- AcceptRequestsView.kt -->
<string name="contact_requests">Запросы контактов</string>
<string name="accept_requests">Принимать запросы</string>
<string name="accept_automatically">Автоматически</string>
<string name="section_title_welcome_message">ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ</string>
<!-- 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>
@@ -477,7 +432,6 @@
<string name="save_and_notify_contacts">Сохранить и уведомить контакты</string>
<string name="save_and_notify_group_members">Сохранить и уведомить членов группы</string>
<string name="exit_without_saving">Выйти без сохранения</string>
<!-- Welcome Prompts - WelcomeView.kt -->
<string name="you_control_your_chat">Вы котролируете ваш чат!</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Платформа для сообщений и приложений, которая защищает вашу личную информацию и безопасность.</string>
@@ -490,7 +444,6 @@
<string name="full_name_optional__prompt">Полное имя (не обязательно)</string>
<string name="create_profile_button">Создать</string>
<string name="about_simplex">О SimpleX</string>
<!-- markdown demo - MarkdownHelpView.kt -->
<string name="how_to_use_markdown">Как форматировать</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Вы можете форматировать сообщения:</string>
@@ -503,7 +456,6 @@
<string name="connect_via_link">Соединиться через ссылку</string>
<string name="this_string_is_not_a_connection_link">Эта строка не является ссылкой-приглашением!</string>
<string name="you_can_also_connect_by_clicking_the_link">Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Открыть в приложении</b>.</string>
<!-- CICallStatus -->
<string name="callstatus_calling">входящий звонок…</string>
<string name="callstatus_missed">пропущенный звонок</string>
@@ -513,7 +465,6 @@
<string name="callstatus_in_progress">активный звонок</string>
<string name="callstatus_ended">звонок завершён <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_error">ошибка звонка</string>
<!-- CallState -->
<string name="callstate_starting">инициализация…</string>
<string name="callstate_waiting_for_answer">ожидается ответ…</string>
@@ -523,7 +474,6 @@
<string name="callstate_connecting">соединяется…</string>
<string name="callstate_connected">соединено</string>
<string name="callstate_ended">завершен</string>
<!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">Новое поколение приватных сообщений</string>
<string name="privacy_redefined">Более конфиденциальный</string>
@@ -535,7 +485,6 @@
<string name="create_your_profile">Создать профиль</string>
<string name="make_private_connection">Добавьте контакт</string>
<string name="how_it_works">Как это работает</string>
<!-- How SimpleX Works -->
<string name="how_simplex_works">Как <xliff:g id="appName">SimpleX</xliff:g> работает</string>
<string name="many_people_asked_how_can_it_deliver">Много пользователей спросили: <i>как <xliff:g id="appName">SimpleX</xliff:g> доставляет сообщения без идентификаторов пользователей?</i></string>
@@ -544,10 +493,10 @@
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются <b>с двухуровневым end-to-end шифрованием</b>.</string>
<string name="read_more_in_github">Узнайте больше из нашего GitHub репозитория.</string>
<string name="read_more_in_github_with_link">Узнайте больше из нашего <font color="#0088ff">GitHub репозитория</font>.</string>
<!-- SetNotificationsMode.kt -->
<string name="use_chat">Использовать чат</string>
<!-- MakeConnection -->
<string name="paste_the_link_you_received">Вставить полученную ссылку</string>
<!-- Call -->
<string name="incoming_video_call">Входящий видеозвонок</string>
<string name="incoming_audio_call">Входящий аудиозвонок</string>
@@ -562,7 +511,6 @@
<string name="call_already_ended">Звонок уже завершен!</string>
<string name="icon_descr_video_call">видеозвонок</string>
<string name="icon_descr_audio_call">аудиозвонок</string>
<!-- Call settings -->
<string name="settings_audio_video_calls">Аудио- и видеозвонки</string>
<string name="your_calls">Ваши звонки</string>
@@ -573,12 +521,10 @@
<string name="no_call_on_lock_screen">Выключить</string>
<string name="your_ice_servers">Ваши ICE серверы</string>
<string name="webrtc_ice_servers">WebRTC ICE серверы</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Откройте <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\nчтобы принять звонок</string>
<string name="allow_accepting_calls_from_lock_screen">Вы можете разрешить принимать звонки на экране блокировки через Настройки.</string>
<string name="open_verb">Открыть</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e зашифровано</string>
<string name="status_no_e2e_encryption">нет e2e шифрования</string>
@@ -594,7 +540,6 @@
<string name="icon_descr_speaker_off">Выключить спикер</string>
<string name="icon_descr_speaker_on">Включить спикер</string>
<string name="icon_descr_flip_camera">Перевернуть камеру</string>
<!-- Call items -->
<string name="icon_descr_call_pending_sent">Входящий звонок</string>
<string name="icon_descr_call_missed">Пропущенный звонок</string>
@@ -603,7 +548,6 @@
<string name="icon_descr_call_progress">Текущий звонок</string>
<string name="icon_descr_call_ended">Звонок завершен</string>
<string name="answer_call">Принять звонок</string>
<!-- Message integrity -->
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> пропущенных сообщений</string>
<string name="integrity_msg_bad_hash">ошибка хэш сообщения</string>
@@ -611,16 +555,13 @@
<string name="integrity_msg_duplicate">повторное сообщение</string>
<string name="alert_title_skipped_messages">Пропущенные сообщения</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Это может случится, когда:\n1. Сервер удалил сообщения, если они не были доставлены в течение 30 дней.\n2. Сервер, через который вы получаете сообщения от контакта, был обновлён и перезапущен.\n3. Соединение компроментировано.\nПожалуйста, соединитесь с девелоперами через Настройки, чтобы получать уведомления о серверах.\nМы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Конфиденциальность</string>
<string name="your_privacy">Конфиденциальность</string>
<string name="protect_app_screen">Защитить экран приложения</string>
<string name="auto_accept_images">Автоприем изображений</string>
<string name="transfer_images_faster">Передавать изображения быстрее</string>
<string name="send_link_previews">Отправлять картинки ссылок</string>
<string name="full_backup">Резервная копия данных</string>
<!-- Settings sections -->
<string name="settings_section_title_you">ВЫ</string>
<string name="settings_section_title_settings">НАСТРОЙКИ</string>
@@ -637,7 +578,6 @@
<string name="settings_section_title_messages">СООБЩЕНИЯ</string>
<string name="settings_section_title_calls">ЗВОНКИ</string>
<string name="settings_section_title_incognito">Режим Инкогнито</string>
<!-- DatabaseView.kt -->
<string name="your_chat_database">База данных</string>
<string name="run_chat_section">ЗАПУСТИТЬ ЧАТ</string>
@@ -671,8 +611,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>
@@ -687,7 +627,6 @@
<string name="enable_automatic_deletion_message">Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут.</string>
<string name="delete_messages">Удалить сообщения</string>
<string name="error_changing_message_deletion">Ошибка при изменении настройки</string>
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Сохранить пароль в Keystore</string>
<string name="database_encrypted">База данных зашифрована!</string>
@@ -716,7 +655,6 @@
<string name="database_passphrase_will_be_updated">Пароль базы данных будет изменен.</string>
<string name="store_passphrase_securely">Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете.</string>
<string name="store_passphrase_securely_without_recover">Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его.</string>
<!-- DatabaseErrorView.kt -->
<string name="wrong_passphrase">Неправильный пароль базы данных</string>
<string name="encrypted_database">База данных зашифрована</string>
@@ -741,11 +679,9 @@
<string name="restore_database_alert_confirm">Восстановить</string>
<string name="database_restore_error">Ошибка при восстановлении базы данных</string>
<string name="restore_passphrase_not_found_desc">Пароль не найден в Keystore, пожалуйста, введите его вручную. Это могло произойти, если вы восстановили данные приложения с помощью инструмента резервного копирования. Если это не так, пожалуйста, свяжитесь с разработчиками.</string>
<!-- ChatModel.chatRunning interactions -->
<string name="chat_is_stopped_indication">Чат остановлен</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Вы можете запустить чат через Настройки приложения или перезапустив приложение.</string>
<!-- ChatArchiveView.kt -->
<string name="chat_archive_header">Архив чата</string>
<string name="chat_archive_section">АРХИВ ЧАТА</string>
@@ -753,7 +689,6 @@
<string name="delete_archive">Удалить архив</string>
<string name="archive_created_on_ts">Дата создания <xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="delete_chat_archive_question">Удалить архив чата?</string>
<!-- Groups -->
<string name="group_invitation_item_description">приглашение в группу <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="join_group_question">Вступить в группу?</string>
@@ -773,7 +708,6 @@
<string name="alert_message_no_group">Эта группа больше не существует.</string>
<string name="alert_title_cant_invite_contacts">Нельзя пригласить контакты!</string>
<string name="alert_title_cant_invite_contacts_descr">Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено</string>
<!-- CIGroupInvitationView.kt -->
<string name="you_sent_group_invitation">Вы отправили приглашение в группу</string>
<string name="you_are_invited_to_group">Вы приглашены в группу</string>
@@ -782,7 +716,6 @@
<string name="you_joined_this_group">Вы вступили в эту группу</string>
<string name="you_rejected_group_invitation">Вы отклонили приглашение в группу</string>
<string name="group_invitation_expired">Приглашение в группу истекло</string>
<!-- Group event chat items -->
<string name="rcv_group_event_member_added">пригласил(а) <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">соединен(а)</string>
@@ -799,7 +732,6 @@
<string name="snd_group_event_member_deleted">вы удалили <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_user_left">вы покинули группу</string>
<string name="snd_group_event_group_profile_updated">профиль группы обновлен</string>
<!-- Conn event chat items -->
<string name="rcv_conn_event_switch_queue_phase_completed">поменял(а) адрес для вас</string>
<string name="rcv_conn_event_switch_queue_phase_changing">смена адреса…</string>
@@ -807,12 +739,10 @@
<string name="snd_conn_event_switch_queue_phase_changing_for_member">смена адреса для %s…</string>
<string name="snd_conn_event_switch_queue_phase_completed">вы поменяли адрес</string>
<string name="snd_conn_event_switch_queue_phase_changing">смена адреса…</string>
<!-- GroupMemberRole -->
<string name="group_member_role_member">член группы</string>
<string name="group_member_role_admin">админ</string>
<string name="group_member_role_owner">владелец</string>
<!-- GroupMemberStatus -->
<string name="group_member_status_removed">удален(а)</string>
<string name="group_member_status_left">покинул(а)</string>
@@ -825,9 +755,7 @@
<string name="group_member_status_connected">соединен(а)</string>
<string name="group_member_status_complete">соединение завершено</string>
<string name="group_member_status_creator">создатель</string>
<string name="group_member_status_connecting">соединяется</string>
<!-- AddGroupMembersView.kt -->
<string name="no_contacts_to_add">Нет контактов для добавления</string>
<string name="new_member_role">Роль члена группы</string>
@@ -841,7 +769,6 @@
<string name="no_contacts_selected">Контакты не выбраны</string>
<string name="invite_prohibited">Нельзя пригласить контакт!</string>
<string name="invite_prohibited_description">Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль</string>
<!-- GroupChatInfoView.kt -->
<string name="button_add_members">Пригласить членов группы</string>
<string name="group_info_section_title_num_members">ЧЛЕНОВ ГРУППЫ: <xliff:g id="num_members">%1$s</xliff:g></string>
@@ -861,12 +788,10 @@
<string name="error_creating_link_for_group">Ошибка при создании ссылки группы</string>
<string name="error_deleting_link_for_group">Ошибка при удалении ссылки группы</string>
<string name="only_group_owners_can_change_prefs">Только владельцы группы могут изменять предпочтения группы.</string>
<!-- For Console chat info section -->
<string name="section_title_for_console">ДЛЯ КОНСОЛИ</string>
<string name="info_row_local_name">Локальное имя</string>
<string name="info_row_database_id">ID базы данных</string>
<!-- GroupMemberInfoView.kt -->
<string name="button_remove_member">Удалить члена группы</string>
<string name="button_send_direct_message">Отправить сообщение</string>
@@ -886,14 +811,12 @@
<string name="info_row_connection">Соединение</string>
<string name="conn_level_desc_direct">прямое</string>
<string name="conn_level_desc_indirect">непрямое (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<!-- ConnectionStats -->
<string name="conn_stats_section_title_servers">СЕРВЕРЫ</string>
<string name="receiving_via">Получение через</string>
<string name="sending_via">Отправка через</string>
<string name="network_status">Состояние сети</string>
<string name="switch_receiving_address">Переключить адрес получения</string>
<!-- AddGroupView.kt -->
<string name="create_secret_group_title">Создать скрытую группу</string>
<string name="group_is_decentralized">Группа полностью децентрализована — она видна только членам.</string>
@@ -901,12 +824,10 @@
<string name="group_full_name_field">Полное имя:</string>
<string name="group_unsupported_incognito_main_profile_sent">Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы</string>
<string name="group_main_profile_sent">Ваш профиль чата будет отправлен членам группы</string>
<!-- GroupProfileView.kt -->
<string name="group_profile_is_stored_on_members_devices">Профиль группы хранится на устройствах членов, а не на серверах.</string>
<string name="save_group_profile">Сохранить профиль группы</string>
<string name="error_saving_group_profile">Ошибка при сохранении профиля группы</string>
<!-- AdvancedNetworkSettings.kt -->
<string name="network_options_reset_to_defaults">Сбросить настройки</string>
<string name="network_option_seconds_label">сек</string>
@@ -919,29 +840,24 @@
<string name="update_network_settings_question">Обновить настройки сети?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Обновление настроек приведет к переподключению клиента ко всем серверам.</string>
<string name="update_network_settings_confirmation">Обновить</string>
<!-- Incognito mode -->
<string name="incognito">Инкогнито</string>
<string name="incognito_random_profile">Случайный профиль</string>
<string name="incognito_random_profile_description">Вашему контакту будет отправлен случайный профиль</string>
<string name="incognito_random_profile_from_contact_description">Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль</string>
<string name="incognito_info_protects">Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль.</string>
<string name="incognito_info_allows">Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя.</string>
<string name="incognito_info_share">Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.</string>
<string name="incognito_info_find">Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата.</string>
<!-- Default themes -->
<string name="theme_system">Системная</string>
<string name="theme_light">Светлая</string>
<string name="theme_dark">Темная</string>
<!-- Appearance.kt -->
<string name="theme">Тема</string>
<string name="save_color">Сохранить цвет</string>
<string name="reset_color">Сбросить цвета</string>
<string name="color_primary">Акцент</string>
<!-- Preferences.kt -->
<string name="chat_preferences_you_allow">Вы разрешаете</string>
<string name="chat_preferences_contact_allows">Контакт разрешает</string>
@@ -990,5 +906,136 @@
<string name="message_deletion_prohibited_in_chat">Необратимое удаление сообщений запрещено в этой группе.</string>
<string name="group_members_can_send_voice">Члены группы могут отправлять голосовые сообщения.</string>
<string name="voice_messages_are_prohibited">Голосовые сообщения запрещены в этой группе.</string>
</resources>
<string name="onboarding_notifications_mode_off_desc"><b>Минимальный расход батареи</b>. Вы получите уведомления только когда приложение запущено, без фонового сервиса.</string>
<string name="onboarding_notifications_mode_title">Уведомления</string>
<string name="onboarding_notifications_mode_off">Когда приложение запущено</string>
<string name="onboarding_notifications_mode_periodic">Периодически</string>
<string name="onboarding_notifications_mode_service">Мгновенно</string>
<string name="onboarding_notifications_mode_service_desc"><b>Больше расход батареи</b>! Фоновый сервис постоянно запущен - уведомления будут показаны как только есть новые сообщения.</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Меньше расход батареи</b>. Фоновый сервис проверяет новые сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.</string>
<string name="onboarding_notifications_mode_subtitle">Можно изменить позже в настройках.</string>
<string name="live">LIVE</string>
<string name="send_live_message">Отправить живое сообщение</string>
<string name="live_message">Живое сообщение!</string>
<string name="send_verb">Отправить</string>
<string name="scan_code_from_contacts_app">Сканируйте код безопасности из приложения контакта.</string>
<string name="delete_after">Удалять через</string>
<string name="ttl_sec">%d сек</string>
<string name="ttl_s">%dс</string>
<string name="ttl_min">%d мин</string>
<string name="ttl_month">%d мес.</string>
<string name="ttl_months">%d мес.</string>
<string name="ttl_m">%dм</string>
<string name="ttl_mth">%dмес</string>
<string name="ttl_hour">%d час</string>
<string name="ttl_hours">%d ч.</string>
<string name="ttl_h">%dч</string>
<string name="ttl_day">%d день</string>
<string name="ttl_week">%d нед.</string>
<string name="timed_messages">Исчезающие сообщения</string>
<string name="view_security_code">Показать код безопасности</string>
<string name="verify_security_code">Подтвердить код безопасности</string>
<string name="both_you_and_your_contact_can_send_disappearing">Вы и ваш контакт можете отправлять исчезающие сообщения.</string>
<string name="only_you_can_send_disappearing">Только вы можете отправлять исчезающие сообщения.</string>
<string name="only_your_contact_can_send_disappearing">Только ваш контакт может отправлять исчезающие сообщения.</string>
<string name="disappearing_prohibited_in_this_chat">Исчезающие сообщения запрещены в этом чате.</string>
<string name="allow_to_send_disappearing">Разрешить посылать исчезающие сообщения.</string>
<string name="contact_developers">Пожалуйста, обновите приложение и свяжитесь с разработчиками.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Разрешить вашим контактам отправлять исчезающие сообщения.</string>
<string name="failed_to_parse_chat_title">Не удалось открыть чат</string>
<string name="failed_to_parse_chats_title">Не удалось открыть чаты</string>
<string name="incorrect_code">Неправильный код безопасности!</string>
<string name="scan_code">Сканировать код</string>
<string name="send_live_message_desc">Отправить живое сообщение — оно будет обновляться для получателей по мере того, как вы его вводите</string>
<string name="create_group_link">Создать ссылку группы</string>
<string name="prohibit_sending_disappearing_messages">Запретить отправлять исчезающие сообщения.</string>
<string name="disappearing_messages_are_prohibited">Исчезающие сообщения запрещены в этой группе.</string>
<string name="ttl_w">%dнед</string>
<string name="ttl_d">%dд</string>
<string name="ttl_weeks">%d нед.</string>
<string name="ttl_days">%d дней</string>
<string name="to_verify_compare">Чтобы подтвердить безопасность end-to-end шифрования с вашим контактом сравните (или сканируйте) код на ваших устройствах.</string>
<string name="is_verified">%s подтверждён</string>
<string name="is_not_verified">%s не подтверждён</string>
<string name="security_code">Код безопасности</string>
<string name="mark_code_verified">Подтвердить</string>
<string name="clear_verification">Сбросить подтверждение</string>
<string name="allow_disappearing_messages_only_if">Разрешить исчезающие сообщения, только если ваш контакт разрешает их вам.</string>
<string name="prohibit_sending_disappearing">Запретить посылать исчезающие сообщения.</string>
<string name="group_members_can_send_disappearing">Члены группы могут посылать исчезающие сообщения.</string>
<string name="whats_new">Новые функции</string>
<string name="new_in_version">Новое в %s</string>
<string name="v4_2_security_assessment">Аудит безопасности</string>
<string name="v4_2_security_assessment_desc">Безопасность SimpleX Chat была проверена Trail of Bits.</string>
<string name="v4_3_voice_messages">Голосовые сообщения</string>
<string name="v4_3_voice_messages_desc">Макс. 40 секунд, доставляются мгновенно.</string>
<string name="v4_3_irreversible_message_deletion">Окончательное удаление сообщений</string>
<string name="v4_3_irreversible_message_deletion_desc">Ваши контакты могут разрешить окончательное удаление сообщений.</string>
<string name="v4_3_improved_server_configuration_desc">Добавить серверы через QR код.</string>
<string name="v4_3_improved_privacy_and_security">Улучшенная безопасность</string>
<string name="v4_3_improved_privacy_and_security_desc">Скрыть экран приложения.</string>
<string name="v4_4_disappearing_messages">Исчезающие сообщения</string>
<string name="v4_4_disappearing_messages_desc">Отправленные сообщения будут удалены через заданное время.</string>
<string name="v4_3_improved_server_configuration">Улучшенная конфигурация серверов</string>
<string name="v4_4_live_messages">\"Живые\" сообщения</string>
<string name="v4_4_live_messages_desc">Получатели видят их в то время как вы их набираете.</string>
<string name="v4_4_verify_connection_security">Проверить безопасность соединения</string>
<string name="v4_4_verify_connection_security_desc">Сравните код безопасности с вашими контактами.</string>
<string name="invalid_chat">ошибка чата</string>
<string name="accept_feature">Принять</string>
<string name="accept_feature_set_1_day">Установить 1 день</string>
<string name="invalid_data">неверные данные</string>
<string name="v4_2_group_links">Ссылки групп</string>
<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,259 @@
<?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>
<string name="confirm_new_passphrase">确认新口令…</string>
<string name="group_member_status_complete">完整的</string>
<string name="group_member_status_connected">连接的</string>
<string name="chat_preferences">聊天偏好</string>
<string name="settings_section_title_chats">聊天</string>
<string name="clear_verb">清空</string>
<string name="clear_chat_menu_action">清空</string>
<string name="clear_chat_button">清除聊天记录</string>
<string name="clear_chat_question">清除聊天记录?</string>
<string name="chat_with_developers">与开发者聊天</string>
<string name="clear_contacts_selection_button">清除</string>
<string name="colored">有色</string>
<string name="callstate_connected">连接的</string>
<string name="connect_button">连接</string>
<string name="connect_via_link_verb">连接</string>
<string name="choose_file">选择文件</string>
<string name="network_session_mode_user">聊天资料</string>
<string name="v4_5_transport_isolation_descr">按聊天资料(默认)或按连接(BETA)。</string>
<string name="smp_servers_check_address">检查服务器地址并再试一次。</string>
<string name="clear_verification">清晰的验证</string>
<string name="icon_descr_close_button">关闭键</string>
<string name="configure_ICE_servers">配置ICE服务器</string>
<string name="confirm_verb">确认</string>
<string name="auth_confirm_credential">确认您的证书</string>
<string name="server_connected">连接的</string>
<string name="icon_descr_server_status_connected">连接的</string>
<string name="smp_server_test_connect">连接</string>
<string name="notification_contact_connected">连接的</string>
<string name="server_connecting">连接的</string>
<string name="group_member_status_connecting">连接的</string>
<string name="notifications_mode_periodic_desc">每10分钟检查一次新消息,最长1分钟</string>
<string name="rcv_group_event_member_connected">连接的</string>
<string name="v4_4_verify_connection_security_desc">与你的联系人比较安全码</string>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="chat_item_ttl_week">1 個星期</string>
<string name="accept_requests">接受請求</string>
<string name="a_plus_b">a + b</string>
<string name="about_simplex">關於 SimpleX</string>
<string name="accept_call_on_lock_screen">接受</string>
<string name="accept_feature">接受</string>
<string name="chat_item_ttl_day">1 天</string>
<string name="chat_item_ttl_month">1 個月</string>
<string name="accept_contact_button">接受</string>
<string name="about_simplex_chat">關於<xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="accept_connection_request__question">接受新連線要求\?</string>
<string name="callstatus_accepted">已接受電話</string>
<string name="network_enable_socks_info">要在端口 9050 啟動 SOCKS 代理伺服器嗎\? 在啟用這個選項之前,必須先啟動代理伺服器。</string>
<string name="group_member_role_admin">管理員</string>
<string name="above_then_preposition_continuation">以上,然後:</string>
<string name="smp_servers_preset_add">加入預設伺服器</string>
<string name="smp_servers_add">新增伺服器…</string>
<string name="accept">接受</string>
</resources>
@@ -27,6 +27,10 @@
<string name="sender_you_pronoun">you</string>
<string name="unknown_message_format">unknown message format</string>
<string name="invalid_message_format">invalid message format</string>
<string name="live">LIVE</string>
<string name="moderated_description">moderated</string>
<string name="invalid_chat">invalid chat</string>
<string name="invalid_data">invalid data</string>
<!-- PendingContactConnection - ChatModel.kt -->
<string name="connection_local_display_name">connection <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
@@ -57,6 +61,13 @@
<string name="error_saving_smp_servers">Error saving SMP servers</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Make sure SMP server addresses are in correct format, line separated and are not duplicated.</string>
<string name="error_setting_network_config">Error updating network configuration</string>
<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>
@@ -90,6 +101,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>
@@ -250,6 +262,8 @@
<string name="icon_descr_server_status_pending">Pending</string>
<string name="switch_receiving_address_question">Switch receiving address?</string>
<string name="switch_receiving_address_desc">This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed please check that you can still receive messages from this contact (or group member).</string>
<string name="view_security_code">View security code</string>
<string name="verify_security_code">Verify security code</string>
<!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Send Message</string>
@@ -259,6 +273,11 @@
<string name="voice_messages_prohibited">Voice messages prohibited!</string>
<string name="ask_your_contact_to_enable_voice">Please ask your contact to enable sending voice messages.</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="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
<string name="send_verb">Send</string>
<string name="icon_descr_cancel_live_message">Cancel live message</string>
<!-- General Actions / Responses -->
<string name="back">Back</string>
@@ -385,9 +404,23 @@
<string name="one_time_link">One-time invitation link</string>
<string name="your_contact_address">Your contact address</string>
<!-- ScanCodeView.kt -->
<string name="scan_code">Scan code</string>
<string name="incorrect_code">Incorrect security code!</string>
<string name="scan_code_from_contacts_app">Scan security code from your contact\'s app.</string>
<!-- VerifyCodeView.kt -->
<string name="security_code">Security code</string>
<string name="mark_code_verified">Mark verified</string>
<string name="clear_verification">Clear verification</string>
<string name="to_verify_compare">To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</string>
<string name="is_verified">%s is verified</string>
<string name="is_not_verified">%s is not verified</string>
<!-- 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>
@@ -417,6 +450,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>
@@ -452,7 +486,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>
@@ -471,7 +517,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>
@@ -545,6 +591,17 @@
<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>
<!-- SetNotificationsMode.kt -->
<string name="use_chat">Use chat</string>
<string name="onboarding_notifications_mode_title">Private notifications</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_periodic">Periodic</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>Uses more battery</b>! Background service is always running notifications will be shown as soon as the messages are available.</string>
<!-- MakeConnection -->
<string name="paste_the_link_you_received">Paste received link</string>
@@ -617,7 +674,6 @@
<string name="your_privacy">Your privacy</string>
<string name="protect_app_screen">Protect app screen</string>
<string name="auto_accept_images">Auto-accept images</string>
<string name="transfer_images_faster">Transfer images faster</string>
<string name="send_link_previews">Send link previews</string>
<string name="full_backup">App data backup</string>
@@ -671,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>
@@ -682,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>
@@ -853,6 +912,7 @@
<string name="button_leave_group">Leave group</string>
<string name="button_edit_group_profile">Edit group profile</string>
<string name="group_link">Group link</string>
<string name="create_group_link">Create group link</string>
<string name="button_create_group_link">Create link</string>
<string name="delete_link_question">Delete link?</string>
<string name="delete_link">Delete link</string>
@@ -914,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>
@@ -921,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>
@@ -957,6 +1027,7 @@
<string name="group_preferences">Group preferences</string>
<string name="set_group_preferences">Set group preferences</string>
<string name="your_preferences">Your preferences</string>
<string name="timed_messages">Disappearing messages</string>
<string name="direct_messages">Direct messages</string>
<string name="full_deletion">Delete for everyone</string>
<string name="voice_messages">Voice messages</string>
@@ -965,12 +1036,21 @@
<string name="feature_enabled_for_contact">enabled for contact</string>
<string name="feature_off">off</string>
<string name="feature_received_prohibited">received, prohibited</string>
<string name="accept_feature">Accept</string>
<string name="accept_feature_set_1_day">Set 1 day</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Allow your contacts to send disappearing messages.</string>
<string name="allow_disappearing_messages_only_if">Allow disappearing messages only if your contact allows them.</string>
<string name="prohibit_sending_disappearing_messages">Prohibit sending disappearing messages.</string>
<string name="allow_your_contacts_irreversibly_delete">Allow your contacts to irreversibly delete sent messages.</string>
<string name="allow_irreversible_message_deletion_only_if">Allow irreversible message deletion only if your contact allows it to you.</string>
<string name="contacts_can_mark_messages_for_deletion">Contacts can mark messages for deletion; you will be able to view them.</string>
<string name="allow_your_contacts_to_send_voice_messages">Allow your contacts to send voice messages.</string>
<string name="allow_voice_messages_only_if">Allow voice messages only if your contact allows them.</string>
<string name="prohibit_sending_voice_messages">Prohibit sending voice messages.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Both you and your contact can send disappearing messages.</string>
<string name="only_you_can_send_disappearing">Only you can send disappearing messages.</string>
<string name="only_your_contact_can_send_disappearing">Only your contact can send disappearing messages.</string>
<string name="disappearing_prohibited_in_this_chat">Disappearing messages are prohibited in this chat.</string>
<string name="both_you_and_your_contacts_can_delete">Both you and your contact can irreversibly delete sent messages.</string>
<string name="only_you_can_delete_messages">Only you can irreversibly delete messages (your contact can mark them for deletion).</string>
<string name="only_your_contact_can_delete">Only your contact can irreversibly delete messages (you can mark them for deletion).</string>
@@ -979,17 +1059,78 @@
<string name="only_you_can_send_voice">Only you can send voice messages.</string>
<string name="only_your_contact_can_send_voice">Only your contact can send voice messages.</string>
<string name="voice_prohibited_in_this_chat">Voice messages are prohibited in this chat.</string>
<string name="allow_to_send_disappearing">Allow to send disappearing messages.</string>
<string name="prohibit_sending_disappearing">Prohibit sending disappearing messages.</string>
<string name="allow_direct_messages">Allow sending direct messages to members.</string>
<string name="prohibit_direct_messages">Prohibit sending direct messages to members.</string>
<string name="allow_to_delete_messages">Allow to irreversibly delete sent messages.</string>
<string name="prohibit_message_deletion">Prohibit irreversible message deletion.</string>
<string name="allow_to_send_voice">Allow to send voice messages.</string>
<string name="prohibit_sending_voice">Prohibit sending voice messages.</string>
<string name="group_members_can_send_disappearing">Group members can send disappearing messages.</string>
<string name="disappearing_messages_are_prohibited">Disappearing messages are prohibited in this group.</string>
<string name="group_members_can_send_dms">Group members can send direct messages.</string>
<string name="direct_messages_are_prohibited_in_chat">Direct messages between members are prohibited in this group.</string>
<string name="group_members_can_delete">Group members can irreversibly delete sent messages.</string>
<string name="message_deletion_prohibited_in_chat">Irreversible message deletion is prohibited in this group.</string>
<string name="group_members_can_send_voice">Group members can send voice messages.</string>
<string name="voice_messages_are_prohibited">Voice messages are prohibited in this group.</string>
<string name="delete_after">Delete after</string>
<string name="ttl_sec">%d sec</string>
<string name="ttl_s">%ds</string>
<string name="ttl_min">%d min</string>
<string name="ttl_month">%d month</string>
<string name="ttl_months">%d months</string>
<string name="ttl_m">%dm</string>
<string name="ttl_mth">%dmth</string>
<string name="ttl_hour">%d hour</string>
<string name="ttl_hours">%d hours</string>
<string name="ttl_h">%dh</string>
<string name="ttl_day">%d day</string>
<string name="ttl_days">%d days</string>
<string name="ttl_d">%dd</string>
<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>
<string name="new_in_version">New in %s</string>
<string name="v4_2_security_assessment">Security assessment</string>
<string name="v4_2_security_assessment_desc">SimpleX Chat security was audited by Trail of Bits.</string>
<string name="v4_2_group_links">Group links</string>
<string name="v4_2_group_links_desc">Admins can create the links to join groups.</string>
<string name="v4_2_auto_accept_contact_requests">Auto-accept contact requests</string>
<string name="v4_2_auto_accept_contact_requests_desc">With optional welcome message.</string>
<string name="v4_3_voice_messages">Voice messages</string>
<string name="v4_3_voice_messages_desc">Max 40 seconds, received instantly.</string>
<string name="v4_3_irreversible_message_deletion">Irreversible message deletion</string>
<string name="v4_3_irreversible_message_deletion_desc">Your contacts can allow full message deletion.</string>
<string name="v4_3_improved_server_configuration">Improved server configuration</string>
<string name="v4_3_improved_server_configuration_desc">Add servers by scanning QR codes.</string>
<string name="v4_3_improved_privacy_and_security">Improved privacy and security</string>
<string name="v4_3_improved_privacy_and_security_desc">Hide app screen in the recent apps.</string>
<string name="v4_4_disappearing_messages">Disappearing messages</string>
<string name="v4_4_disappearing_messages_desc">Sent messages will be deleted after set time.</string>
<string name="v4_4_live_messages">Live messages</string>
<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>
@@ -1,3 +1,4 @@
<paths>
<files-path name="my_files" path="/"/>
<files-path name="app_temp" path="../app_temp"/>
</paths>
+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()
}

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