Compare commits

...

169 Commits

Author SHA1 Message Date
spaced4ndy c8721e8000 5.0: Android 117, iOS 144 2023-04-20 20:26:12 +04:00
Evgeny Poberezkin 03882367da core: 5.0.0.2 2023-04-20 14:19:09 +01:00
spaced4ndy 4d700d113d core, ios: mark files to receive from NSE, receive marked files on chat start (#2218) 2023-04-20 16:52:55 +04:00
Stanislav Dmitrenko 17bdd2a1d2 android: different icons for attachments (#2219)
* android: different icons for attachments

* icon

* change color

* changes

* strings

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-20 13:27:27 +01:00
Evgeny Poberezkin 80a68012a2 website: translations (#2217)
* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Translated using Weblate (Polish)

Currently translated at 33.1% (70 of 211 strings)

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

---------

Co-authored-by: Display Name <ptrumtine@proton.me>
2023-04-20 11:56:53 +01:00
Evgeny Poberezkin 3742906f75 mobile: translations (#2216)
* Translated using Weblate (Spanish)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1123 of 1125 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% (1043 of 1043 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1123 of 1123 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% (1042 of 1042 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% (1123 of 1123 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% (1042 of 1042 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 100.0% (1125 of 1125 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 100.0% (1125 of 1125 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 98.7% (1029 of 1042 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 100.0% (1125 of 1125 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 100.0% (1042 of 1042 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1123 of 1125 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% (1043 of 1043 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1123 of 1123 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% (1042 of 1042 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% (1123 of 1123 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% (1042 of 1042 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 100.0% (1125 of 1125 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 100.0% (1125 of 1125 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 98.7% (1029 of 1042 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 100.0% (1125 of 1125 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 100.0% (1042 of 1042 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1125 of 1125 strings)

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

* ios: import/export localizations

---------

Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
2023-04-20 11:55:04 +01:00
Stanislav Dmitrenko ae90edcdb5 android: moved to BasicTextField in some places (#2215)
* adnroid: moved to BasicTextField in some places

* field height

* field height 2

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-20 11:29:50 +01:00
Evgeny Poberezkin 9e76aadb0f Merge branch 'stable' 2023-04-19 18:09:57 +01:00
Evgeny Poberezkin 043544d7ec readme: new users group 2023-04-19 18:09:05 +01:00
Evgeny Poberezkin 2bf7d1dddc android: share video as video (#2214) 2023-04-19 17:39:31 +01:00
Evgeny Poberezkin e1741118ce android: fix profile string (#2213) 2023-04-19 15:52:33 +01:00
spaced4ndy 58fb3f7f2d android: fix onboarding fonts, paddings (#2212) 2023-04-19 18:48:00 +04:00
spaced4ndy 48e92a7e9b android: don't show button to hide user if authentication is not configured (#2211) 2023-04-19 18:16:26 +04:00
spaced4ndy 5bf16da09d core, mobile: prohibit to change chat item expiration when chat is stopped (#2210) 2023-04-19 15:21:28 +04:00
Evgeny Poberezkin 23ca3dd665 5.0.0-beta.2 2023-04-19 11:59:39 +01:00
Evgeny Poberezkin 2caff25fa2 android: revert to using gallery for images and videos (#2209)
* core: revert to using gallery for images and videos

* remove comment
2023-04-19 11:07:14 +01:00
Evgeny Poberezkin 37f835be8c mobile: remove XFTP toggle (#2208)
* mobile: remove XFTP toggle

* ios: remove unused string

* android: remove unused strings
2023-04-19 09:41:01 +01:00
Evgeny Poberezkin a6c1f2f776 5.0.0-beta.1: Android 115, iOS 142 2023-04-19 00:47:34 +01:00
Stanislav Dmitrenko acf0dfc38c android: alerts + dropdown menus (#2199)
* android: alerts + dropdown menus

* colors

* align

* paddings

* colors

* complete alerts

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-19 00:17:28 +01:00
Evgeny Poberezkin 67f13831ce mobile: translations (#2207)
* Translated using Weblate (German)

Currently translated at 97.8% (1101 of 1125 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% (1125 of 1125 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 97.6% (1018 of 1043 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% (1043 of 1043 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.2% (1035 of 1043 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 99.2% (1035 of 1043 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 97.6% (1018 of 1043 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 97.6% (1018 of 1043 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (Czech)

Currently translated at 97.6% (1018 of 1043 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1125 of 1125 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1043 of 1043 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1043 of 1043 strings)

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

* ios: import/export localizations

---------

Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
2023-04-18 21:30:18 +01:00
Evgeny Poberezkin d50e65af20 core: 5.0.0.1 2023-04-18 21:04:34 +01:00
Evgeny Poberezkin b8d74b74a6 website: translations (#2206)
* Translated using Weblate (Arabic)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 3.3% (7 of 211 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/
2023-04-18 18:52:48 +01:00
Evgeny Poberezkin 802a28d759 mobile: translations (#2204)
* Translated using Weblate (Italian)

Currently translated at 100.0% (1092 of 1092 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1092 of 1092 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1014 of 1014 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1092 of 1092 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1014 of 1014 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1092 of 1092 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1089 of 1091 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1091 of 1091 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1013 of 1013 strings)

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

* Translated using Weblate (German)

Currently translated at 95.6% (1044 of 1091 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1091 of 1091 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1013 of 1013 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1091 of 1091 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1013 of 1013 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (1091 of 1091 strings)

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

* Translated using Weblate (Korean)

Currently translated at 78.5% (857 of 1091 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 98.8% (1090 of 1103 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1013 of 1013 strings)

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

* Translated using Weblate (German)

Currently translated at 98.0% (1082 of 1103 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1103 of 1103 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 98.8% (1090 of 1103 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1103 of 1103 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1101 of 1101 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1013 of 1013 strings)

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

* Translated using Weblate (Czech)

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

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

* Translated using Weblate (Hindi)

Currently translated at 16.2% (179 of 1101 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 100.0% (1101 of 1101 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1102 of 1102 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1102 of 1102 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 16.3% (180 of 1102 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 100.0% (1119 of 1119 strings)

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

* Translated using Weblate (French)

Currently translated at 99.6% (1115 of 1119 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% (1013 of 1013 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% (1119 of 1119 strings)

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

* Translated using Weblate (Polish)

Currently translated at 99.2% (1111 of 1119 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1119 of 1119 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1119 of 1119 strings)

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

* ios: import localizations

* ios: re-export localizations

---------

Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: mf <work.j6nnu@slmail.me>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ram <airavatam@tutanota.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
2023-04-18 18:41:49 +01:00
Evgeny Poberezkin c85d43b349 ios: export localizations 2023-04-18 16:59:51 +01:00
spaced4ndy 64ad491197 mobile: what's new in 5.0 (#2200)
* mobile: what's new in 5.0

* update texts

* update

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-18 16:54:56 +01:00
spaced4ndy ddd8e719ef core, ios: don't send/receive XFTP files in NSE (#2202) 2023-04-18 19:43:16 +04:00
spaced4ndy e51f7a51cc core: prohibit SMP file handshake for XFTP files on sender side, except for images and voice messages (#2201) 2023-04-18 18:51:14 +04:00
Stanislav Dmitrenko e66f5d488b android: more enhancements to layouts (#2196)
* android: more enhancements to layouts

* changes

* unused code

* unused code
2023-04-18 11:11:00 +01:00
spaced4ndy b386cc83a7 core, mobile: enable xftp by default (#2198) 2023-04-18 13:49:09 +04:00
spaced4ndy 09481e09b6 core, mobile: file error statuses, cancel sent file (#2193) 2023-04-18 12:48:36 +04:00
Evgeny Poberezkin 6913bf1a46 mobile: Calls chat preference (#2195)
* ios: add calls to chat preferences

* android: types and strings for Calls preference

* android: UI for Calls preference
2023-04-18 09:29:49 +01:00
Stanislav Dmitrenko e6c87ff00b android: profiles UI (#2194)
* android: profiles UI

* reverted color change

* changes

* update color, padding

* colors changes and error check

* focused field color

* refactor

* focus

* layout

* profile editor

* centered layout

* bottom paddings

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-17 22:10:42 +01:00
Evgeny Poberezkin 5f41cf3c52 core: change default for Disappearing Messages to "allow", mobile: support disabling without prohibiting (#2192)
* core: change default for Disappearing Messages to "allow", mobile: support disabling without prohibiting

* fix tests

* disable tests back in CI

* fixed tests 2

* remove enable

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-04-17 18:13:10 +01:00
spaced4ndy 9edbe2e589 core: fix sending file to empty group (#2191) 2023-04-17 15:33:15 +04:00
Evgeny Poberezkin b6876712f0 core: chat preference for audio/video calls (#2188)
* core: chat preference for audio/video calls

* correction

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* clean up

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-04-17 10:18:04 +01:00
Evgeny Poberezkin 5b4c183466 core: do not create decryption error chat items for earlier messages (#2189)
* core: do not create decryption error chat items for earlier messages

* do not report earlier error, mobile items, fix tests
2023-04-16 18:30:25 +01:00
Evgeny Poberezkin 393238f47c android: change UInt type 2023-04-16 15:56:12 +01:00
Evgeny Poberezkin aea526f69d core: add chat items to indicate decryption failures due to ratchets being out of sync (#2175)
* core: add chat items to indicate decryption failures due to ratchets being out of sync

* show ratchet errors in chat items, show all integrity errors

* show decryption errors, tests

* ios: chat items, remove item for duplicate messages

* android: decryption errors chat items

* eol
2023-04-16 11:35:45 +01:00
Evgeny Poberezkin 13cf1cc004 ios: mute groups instantly when notifications are disabled (#2187) 2023-04-14 22:54:05 +01:00
Evgeny Poberezkin 1766a8bb2c 5.0.0-beta.0: Android 114 2023-04-14 20:01:08 +01:00
spaced4ndy 9a5732ab70 ios: 5.0 (141) 2023-04-14 20:43:45 +04:00
Evgeny Poberezkin 2923ca1356 ios: fix profile deletion on iOS 15 (#2185) 2023-04-14 16:51:55 +01:00
spaced4ndy 96f0083384 core: 5.0.0.0 2023-04-14 18:05:16 +04:00
Evgeny Poberezkin febfc396e3 ios: UI to cancel receiving file (#2123)
* ios: UI to cancel receiving file

* different alert
2023-04-14 13:57:17 +01:00
spaced4ndy 29735a807b core: don't delete XFTP file when temporary agent error is reported in RFERR/SFERR (#2184) 2023-04-14 15:52:39 +04:00
spaced4ndy eb36f64676 core: update simplexmq (digest entity id); integrate xftp snd delete (#2183) 2023-04-14 15:32:12 +04:00
Evgeny Poberezkin 4e01970d69 core: remove build timestamp from the version info (reproducible builds) (#2182)
* core: remove build timestamp from the version info (reproducible builds)

* remove strings
2023-04-14 12:03:41 +01:00
Evgeny Poberezkin e5713087e3 ios: export Polish localizations 2023-04-14 11:26:13 +01:00
Evgeny Poberezkin b40fc7ff18 mobile: add Polish language (#2181)
* mobile: add Polish language

* update readme
2023-04-14 10:20:58 +01:00
Evgeny Poberezkin 06ad2b7972 website: translations (#2180)
* ios: UI to cancel receiving file

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 1.8% (4 of 211 strings)

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

* Revert "ios: UI to cancel receiving file"

This reverts commit 0fcae6e8d5.

---------

Co-authored-by: Pedro Licio <amaralrj@gmail.com>
2023-04-13 23:15:18 +01:00
Evgeny Poberezkin 14eeb4451c mobile: translations (#2179)
* Translated using Weblate (French)

Currently translated at 100.0% (1056 of 1056 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% (968 of 968 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% (1062 of 1062 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% (968 of 968 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 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Korean)

Currently translated at 71.7% (762 of 1062 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1056 of 1056 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% (968 of 968 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% (1062 of 1062 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% (968 of 968 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 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Korean)

Currently translated at 71.7% (762 of 1062 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1062 of 1062 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1014 of 1014 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1014 of 1014 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 16.8% (179 of 1062 strings)

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

* Translated using Weblate (Korean)

Currently translated at 80.1% (851 of 1062 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1014 of 1014 strings)

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

* ios: import/export localizations

---------

Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Ram <airavatam@tutanota.com>
2023-04-13 23:11:36 +01:00
Stanislav Dmitrenko ead67adeb8 android: passcode implementation (#2177)
* android: passcode implementation

* layout

* passcode view

* unused param

* text for auth

* small changes

* fix

* use preference instead of toggle

* removed useless code and changed title of auth screen

* removed unneeded function

* EOLs

* changed local variable logic to global variable

* formatting

* different alert

* changed code placement

* alert behaviour

* button size

* tint of buttons

* error instead of failed status

* do not show auth alerts on failures, only on final errors

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-13 22:09:41 +01:00
spaced4ndy c2d70a5107 core: update simplexmq (recoverable xftp send) (#2178) 2023-04-13 20:46:07 +04:00
spaced4ndy 90dffc975a core: request and save extra recipient file descriptions (#2170) 2023-04-12 14:47:54 +04:00
Evgeny Poberezkin e5ba7caddc ios: export localizations 2023-04-12 11:39:45 +01:00
Evgeny Poberezkin ec6cee1389 ios: digital password (instead of device auth) (#2169)
* ios: digital password (instead of device auth)

* set, ask, change password

* kind of working, sometimes

* ZSTack

* fix cancel

* update title

* fix password showing after settings dismissed

* disable button when 16 digits entered

* fixes

* layout on larger screens

* do not disable auth when switching to system if system auth failed, refactor

* fix enabling auth via the initial alert

* support landscape orientation
2023-04-12 11:22:55 +01:00
Float-hu 1d16a19373 readme: translation contributors (#2172)
* Update README.md

* change

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-11 20:17:41 +01:00
Evgeny Poberezkin 04c90b4f07 android: SOCKS proxy settings (#2158)
* android: draft UI for SOCKS proxy settings

* footer comment

* UI for proxy host-port selection

* keyboard type in port text field

* footer

* better text validation logic

* use italic in footer

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-04-10 20:01:14 +01:00
Stanislav Dmitrenko 747cbb8e09 android: media attachments from one button (#2156)
* android: media attachments from one button

* refactor durationText

* use option to enable video

* icon overlay

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-10 16:28:21 +01:00
Evgeny Poberezkin 834487e548 4.6.1: Android 112, iOS 140 2023-04-08 21:17:47 +01:00
Evgeny Poberezkin e1b6b54209 website: translations (#2160)
* ios: UI to cancel receiving file

* Added translation using Weblate (Portuguese (Brazil))

* ios: UI to cancel receiving file

* Added translation using Weblate (Portuguese (Brazil))

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (211 of 211 strings)

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

* Revert "ios: UI to cancel receiving file"

This reverts commit 397788cb0d.

---------

Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: Float <float.hu+@gmail.com>
2023-04-08 20:58:22 +01:00
Evgeny Poberezkin 6366d60e4c mobile: translations (#2159)
* ios: UI to cancel receiving file

* Added translation using Weblate (Polish)

* Added translation using Weblate (Polish)

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 80.8% (844 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 64.1% (670 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 80.9% (845 of 1044 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.4% (996 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 16.1% (156 of 968 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.8% (1011 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 59.4% (575 of 968 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (1042 of 1044 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 59.5% (576 of 968 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 80.2% (777 of 968 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Lithuanian)

Currently translated at 6.8% (71 of 1044 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (968 of 968 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 38.5% (373 of 968 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 23.7% (248 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 0.1% (1 of 968 strings)

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

* Translated using Weblate (Polish)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 28.7% (300 of 1044 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 34.2% (358 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 69.7% (728 of 1044 strings)

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

* revert code change

* import/export localizations

---------

Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: mf <work.j6nnu@slmail.me>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: B.O.S.S <BxOxSxS@protonmail.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Moo <hazap@hotmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Twoomatch <twoomatch0@xmail.re>
2023-04-08 20:55:15 +01:00
Evgeny Poberezkin fabea36682 ios: update library 2023-04-08 19:53:33 +01:00
Stanislav Dmitrenko 991332a809 android: servers UI/API (#2155)
* android: servers UI/API

* non-optional server protocol in parsed address

* make another enum for ServerProtocol

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-07 18:19:44 +01:00
Evgeny Poberezkin 85537a99e8 core: 4.6.1.2 2023-04-07 18:18:21 +01:00
spaced4ndy 717c90c58b mobile: remove cancelled files (#2154) 2023-04-07 17:41:07 +04:00
Evgeny Poberezkin ccb52e0acd ios: validate server protocol 2023-04-06 23:25:40 +01:00
Evgeny Poberezkin d84b30c071 core: update simplemq (preset xftp servers) 2023-04-06 23:16:16 +01:00
Evgeny Poberezkin 5ae0afe1fe ios: update servers API/UI (#2149)
* ios: update servers API/UI

* fix UI

* fix
2023-04-06 22:48:32 +01:00
Evgeny Poberezkin d250e503b0 core: update simplexmq (fix file reception on 32 bit CPUs) 2023-04-06 21:06:46 +01:00
Stanislav Dmitrenko afb0ae3d03 ios: video support (#2115)
* ios: video support

* made video experience prettier

* line reordering

* fix warning

* remove playback speed

* fullscreen player

* removed unused code

* fix conflict

* setting playing status better

* thumbnail dimensions and loading indicator

* fill under video

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-06 18:26:48 +01:00
Evgeny Poberezkin 1a3f0bed47 core: update servers API to include XFTP servers, ios: generalize UI to manage servers (#2140)
* core: update servers API to include XFTP servers, ios: generalize UI to manage servers

* add test

* update migrations to pass tests

* fix readme

* update simplexmq
2023-04-05 21:59:12 +01:00
Stanislav Dmitrenko 1e280fb7e1 android: prevent possible race in chat items (#2148)
* android: prevent possible race in chat items

* change
2023-04-05 18:53:35 +01:00
Evgeny Poberezkin 6feac55380 core: update http2 library 2023-04-05 10:23:35 +01:00
Evgeny Poberezkin 93d8eac037 4.6.1-beta.2: Android 111, iOS 138 2023-04-04 23:48:11 +01:00
Evgeny Poberezkin a11f99be3d mobile: ignore spaces around password (#2144) 2023-04-04 21:53:25 +01:00
Evgeny Poberezkin da17639309 core: 4.6.1.1 2023-04-04 17:31:30 +01:00
Evgeny Poberezkin 10301aa742 terminal: autocomplete contacts, groups and commands (#2125)
* terminal: autocomplete contacts, groups and commands

* autocomplete for commands and member names

* update commands

* show variants

* improve

* improve

* do not show user in contacts, better state machine for tab states

* update CI runners
2023-04-04 14:58:26 +01:00
Evgeny Poberezkin 2148d50393 core: use Int64 in time calculations (#2143)
* core: use Int64 in time calculations

* remove import

* make interval Int64
2023-04-04 14:26:31 +01:00
Evgeny Poberezkin 12fb2a4ec5 ci: move to ubuntu 20/22, disable 2 tests in CI (#2142)
* ci: move to ubuntu 20/22

* skip test on mac

* skip some tests on mac CI

* skip test on CI

* skip test unconditionally

* skip on CI only
2023-04-04 13:09:07 +01:00
Stanislav Dmitrenko 8085e5b85c android: change active user after chat started (#2141) 2023-04-03 20:11:27 +01:00
Stanislav Dmitrenko 4ba310ec16 android: open direct chat simplified (#2139) 2023-04-03 19:59:50 +01:00
Stanislav Dmitrenko 865c56f400 scripts: adapted compress-and-sign-apk script to case-insensitive file systems (#2138)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-03 19:41:42 +01:00
Stanislav Dmitrenko c510e73256 android: disallow to reply on service messages (#2136) 2023-04-03 18:53:21 +01:00
spaced4ndy 73638129bc core: cancel file transfer when chat item is marked deleted (#2137) 2023-04-03 18:49:22 +04:00
spaced4ndy 1a7a79d504 core: allow repeat receive after cancel for XFTP files (#2134) 2023-04-03 16:31:18 +04:00
spaced4ndy d3268e4a72 mobile: delete XFTP files after uploading (#2133) 2023-04-03 16:31:09 +04:00
Evgeny Poberezkin 15a93014a5 core: update http2 2023-04-01 17:27:11 +01:00
Evgeny Poberezkin e7735329bc 4.6.1-beta.1: Android 110, iOS 137, update library 2023-04-01 16:04:44 +01:00
ishi_sama 3e222c68eb docs: FR Update (#2063)
* docs: FR update

* fix android.md

* fix rev dates

fix revision dates ; ANDROID.md & TRANSLATION.md rev dates added

* fix links

* update

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-04-01 14:55:25 +01:00
M Sarmad Qadeer a596bd9011 website: nav scrolling & direction issue (#2120)
* website: add dir to blog template

* website: remove scroll in mobile dropdown menu
2023-04-01 14:31:15 +01:00
Evgeny Poberezkin 21a49710a8 ios: scripts to import/export localizations 2023-04-01 14:28:12 +01:00
Evgeny Poberezkin ce6fdb2558 mobile: translations (#2121)
* Translated using Weblate (Russian)

Currently translated at 100.0% (1044 of 1044 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% (968 of 968 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 99.3% (962 of 968 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 99.3% (962 of 968 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 99.3% (962 of 968 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 99.2% (961 of 968 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.3% (962 of 968 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 99.3% (962 of 968 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.3% (962 of 968 strings)

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

* ios: export/import localizations
2023-04-01 14:27:24 +01:00
Evgeny Poberezkin 0baee848a6 mobile: translations (#2114)
* Added translation using Weblate (Korean)

* Added translation using Weblate (Korean)

* Translated using Weblate (French)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1008 of 1008 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Czech)

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

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

* Translated using Weblate (Korean)

Currently translated at 20.8% (210 of 1008 strings)

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

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

* Translated using Weblate (Korean)

Currently translated at 21.1% (213 of 1008 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1008 of 1008 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% (940 of 940 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 100.0% (1008 of 1008 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% (1009 of 1009 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% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.9% (1008 of 1009 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Korean)

Currently translated at 27.4% (277 of 1009 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1020 of 1020 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Korean)

Currently translated at 31.3% (320 of 1020 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1026 of 1026 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.9% (1025 of 1026 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% (1028 of 1028 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% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (1017 of 1028 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 99.1% (1019 of 1028 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% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.3% (13 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.5% (1023 of 1028 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 4.1% (39 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Korean)

Currently translated at 36.2% (373 of 1028 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 4.5% (47 of 1033 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1033 of 1035 strings)

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

* Translated using Weblate (Korean)

Currently translated at 45.0% (466 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Korean)

Currently translated at 50.9% (532 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 51.0% (533 of 1044 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 53.6% (560 of 1044 strings)

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

* Added translation using Weblate (Korean)

* Added translation using Weblate (Korean)

* Translated using Weblate (French)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1008 of 1008 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Czech)

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

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

* Translated using Weblate (Korean)

Currently translated at 20.8% (210 of 1008 strings)

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

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

* Translated using Weblate (Korean)

Currently translated at 21.1% (213 of 1008 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (1008 of 1008 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% (940 of 940 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 100.0% (1008 of 1008 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% (1009 of 1009 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% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 99.9% (1008 of 1009 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1009 of 1009 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Korean)

Currently translated at 27.4% (277 of 1009 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1020 of 1020 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (940 of 940 strings)

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

* Translated using Weblate (Korean)

Currently translated at 31.3% (320 of 1020 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (1026 of 1026 strings)

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

* Translated using Weblate (Italian)

Currently translated at 99.9% (1025 of 1026 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% (1028 of 1028 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% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (1017 of 1028 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 99.1% (1019 of 1028 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% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.3% (13 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.5% (1023 of 1028 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 4.1% (39 of 940 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1028 of 1028 strings)

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

* Translated using Weblate (Korean)

Currently translated at 36.2% (373 of 1028 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 4.5% (47 of 1033 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (1035 of 1035 strings)

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

* Translated using Weblate (Czech)

Currently translated at 99.8% (1033 of 1035 strings)

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

* Translated using Weblate (Korean)

Currently translated at 45.0% (466 of 1035 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Korean)

Currently translated at 50.9% (532 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 51.0% (533 of 1044 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 53.6% (560 of 1044 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (1044 of 1044 strings)

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

* Translated using Weblate (Korean)

Currently translated at 56.4% (589 of 1044 strings)

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

* ios: import/export localizations

---------

Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: 5olivetree <5olivetree+github.com@mailbox.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: 橙子 <legiorange@163.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Doocter <Undocked1040@proton.me>
2023-04-01 09:00:15 +01:00
Evgeny Poberezkin 6f304bc9e6 Merge branch 'stable' 2023-04-01 08:12:58 +01:00
Evgeny Poberezkin 1ca0dfffa0 docs: update the process to move profile 2023-04-01 08:12:48 +01:00
Evgeny Poberezkin 1420084f5e website: simplex icon in footer 2023-03-31 22:58:57 +01:00
Evgeny Poberezkin 3e03474437 readme: update translation contributors 2023-03-31 22:19:27 +01:00
Evgeny Poberezkin 95366e4d1b readme: update translations 2023-03-31 19:25:59 +01:00
Evgeny Poberezkin df1775a1e6 website: enable Arabic, Chinese, Spanish 2023-03-31 19:14:39 +01:00
Evgeny Poberezkin 30ccea18ab website: translations (#2113)
* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 46.4% (98 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 58.2% (123 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 100.0% (211 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 97.1% (205 of 211 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 99.0% (209 of 211 strings)

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

* fix strong tag in ar.json

---------

Co-authored-by: 4 Bi 5aYzVk 93FCVjWLWxh44XH3984teVSfjwFYmUGUrbvnHwGirk9 <userfifteen.seventeen@mailfence.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: jonnysemon <johndevand@tutanota.com>
2023-03-31 19:13:19 +01:00
M Sarmad Qadeer 4cd90d74ad website: add RTL languages compatibility (#2056)
* website: add RTL languages compatibility

* website: add few changes

- update tailwindcss version
- add few stylings
- move to rtl true false approach

* website: set lang:en to rtl:true for testing

* website: add arabic key values & textual flag

* website: fix strong tag issues in ar translation.

* website: flip navbar for rtl languages

* disable Arabic

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-31 17:14:20 +01:00
spaced4ndy 7f1214688a core: 4.6.1.0 2023-03-31 19:19:51 +04:00
spaced4ndy aa89d0d156 android: video progress, video & image cancelled indicators; ios: image cancelled indicator (#2111) 2023-03-31 19:15:37 +04:00
spaced4ndy 787cd94362 core: support fallback to SMP file transfer for backwards compatibility (#2110) 2023-03-31 17:33:52 +04:00
Evgeny Poberezkin ec61a7fc51 android: reduce video player opacity in the gallery 2023-03-31 13:59:40 +01:00
Evgeny Poberezkin 9b627534f5 android: update video item layout, add video behind experimental toggle (#2109)
* android: update video item layout, add video behind experimental toggle

* video duration / size design

* refactor, fix duration box size

* more readable

* reuse box modifier

* Revert "reuse box modifier"

This reverts commit d0d2d3e402.
2023-03-31 13:40:25 +01:00
Stanislav Dmitrenko 400a3707b2 android: video support (#2102)
* android: video support

* better landscape videos, UI styling

* removed volume control

* quote for video

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-31 12:25:13 +01:00
Evgeny Poberezkin 38a5676b37 4.6.1-beta.0: Android 109, iOS 136 2023-03-30 20:12:48 +01:00
spaced4ndy f00cfa9108 core, mobile: CRSndFileCompleteXFTP event (#2107) 2023-03-30 19:45:18 +04:00
Evgeny Poberezkin afa24722b2 Merge branch 'stable' 2023-03-30 15:45:45 +01:00
Evgeny Poberezkin ea5cec53bc update readme (#2106)
* update readme

* corrections

* update roadmap

* link to guide

* remove duplication
2023-03-30 15:44:57 +01:00
Evgeny Poberezkin 61dc649c70 guide: initial readme (#2006)
* guide: initial readme

* update guide

* guide:  initial documentation for audio and video calls (#2104)

* Added documentation for audio and video calls.

* Minor update

* corrections

---------

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

* remove trailing spaces and typos

* docs: update audio-video calls guide

* add app settings

* edit guide, link images and posts

* fix image links

* update

* update 2

* remove link

* remove spaces

* move images

* bold

* add image

---------

Co-authored-by: Silent-Ninja <128339587+silent-ninja-1@users.noreply.github.com>
2023-03-30 15:39:35 +01:00
spaced4ndy b20824e16c core: notify about xftp errors (#2105) 2023-03-30 18:36:39 +04:00
Evgeny Poberezkin 39330fdce3 guide: initial readme (#2006)
* guide: initial readme

* update guide

* guide:  initial documentation for audio and video calls (#2104)

* Added documentation for audio and video calls.

* Minor update

* corrections

---------

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

* remove trailing spaces and typos

* docs: update audio-video calls guide

* add app settings

* edit guide, link images and posts

* fix image links

* update

* update 2

* remove link

* remove spaces

* move images

* bold

* add image

---------

Co-authored-by: Silent-Ninja <128339587+silent-ninja-1@users.noreply.github.com>
2023-03-30 15:17:13 +01:00
spaced4ndy 6b725a8ef7 ios, android: cancel file UI; core: cancel file fixes (#2100)
backend fixes:
- check file is not complete on CancelFile,
- check file is not cancelled when processing XFTP events,
- mark SMP file cancelled if recipient cancelled in direct chat.
2023-03-30 14:10:13 +04:00
Evgeny Poberezkin cbcdeb2b43 ios: 4.6 (135), remove bluetooth-central from background modes (#2086) 2023-03-30 09:07:28 +01:00
Evgeny Poberezkin 4351610eca android: confirm password when deleting/unhiding inactive hidden user profile (#2103) 2023-03-30 09:02:57 +01:00
Evgeny Poberezkin 935d826a21 core, ios: unhiding user profiles always requires password (#2101) 2023-03-29 19:28:06 +01:00
Evgeny Poberezkin a8c8137ade core: fix current user becoming incorrect after hiding or (un)muting inactive user profile (#2098)
* core: fix current user becoming incorrect after hiding or (un)muting inactive user profile

* refactor test
2023-03-29 17:39:04 +01:00
spaced4ndy 7b33e1fba8 core: update cancel file api (#2097) 2023-03-29 17:18:44 +04:00
Evgeny Poberezkin ade7bba97b ios: confirm password when deleting active hidden user (#2095) 2023-03-29 14:01:24 +01:00
spaced4ndy 08dd321311 android: rcv & snd files progress, distinguish XFTP and SMP; ios: files UI improvements (#2096) 2023-03-29 15:48:00 +04:00
Evgeny Poberezkin 67961180c9 android: developer tools page (#2094) 2023-03-29 09:34:55 +01:00
Evgeny Poberezkin 1093892ede ios: update developer options (#2091)
* ios: update developer options

* move XFTP option, make chat console usable

* update footer

* typo

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-29 08:41:13 +01:00
spaced4ndy ef05fa4905 core: file protocol field; ios: distinguish behavior and look of XFTP and SMP files (#2090)
* core: file protocol field; ios: distinguish behavior and look of XFTP and SMP files

* remove unused method

* count style

* corrections

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-28 19:20:06 +01:00
Evgeny Poberezkin 6a99a4f1ae Merge pull request #2087 from simplex-chat/ep/android-down-migrations
android: support down migrations
2023-03-28 16:53:57 +01:00
Evgeny Poberezkin 4895f396a2 corrections 2023-03-28 16:00:18 +01:00
Evgeny Poberezkin c3dffc5909 fix 2023-03-28 14:14:09 +01:00
spaced4ndy af73e5993d core: test xftp group file transfer (#2088) 2023-03-28 15:22:42 +04:00
spaced4ndy dfec1cbb02 core: add protocol field to files table (#2089) 2023-03-28 14:38:36 +04:00
Evgeny Poberezkin 8d8f7b2524 migrations UI 2023-03-28 11:26:45 +01:00
Evgeny Poberezkin db7b81587f Merge branch 'master' into ep/android-down-migrations 2023-03-27 23:21:41 +01:00
Evgeny Poberezkin 31bb744ba7 android: support down migrations 2023-03-27 23:20:47 +01:00
Evgeny Poberezkin f4b349162f Merge pull request #1998 from simplex-chat/xftp
core: transfer files via XFTP
2023-03-27 23:18:34 +01:00
Evgeny Poberezkin 7f8adf8f03 Merge branch 'master' into xftp 2023-03-27 20:49:47 +01:00
Evgeny Poberezkin 1f4bb8a224 ios: fix picker heights 2023-03-27 20:43:39 +01:00
Evgeny Poberezkin 0c3dc8a6e9 core: add down migrations and fix test 2023-03-27 19:39:22 +01:00
Evgeny Poberezkin 1f15cf54af Merge branch 'master' into xftp 2023-03-27 18:57:14 +01:00
Evgeny Poberezkin c96ba30018 core: support down migrations to allow reverting to the previous version (#2072)
* core: support down migrations to allow reverting to the previous version

* update schema

* update simplexmq

* rename errors

* remove unused functions

* migration UI, test migration

* update migration UI

* return current migrations in CRVersionInfo

* update simplexmq

* test down migrations

* cleanup ios

* show migrations in log
2023-03-27 18:34:48 +01:00
spaced4ndy ffea61917d ios: display rcv & snd files progress (#2085)
* ios: display rcv & snd files progress

* remove animation
2023-03-27 18:02:54 +01:00
Stanislav Dmitrenko f5c11b8faf android: ability to change profile from share dialog, mobile: do not show profile dropdown when there is only one visible profile (#2084)
* android: ability to change profile from share dialog

* icons swap
2023-03-27 17:58:14 +01:00
Stanislav Dmitrenko 48b4b23204 android: make lint happy (#2081) 2023-03-27 15:35:35 +01:00
Evgeny Poberezkin 9df78c8ac8 core: fix video message JSON encoding (#2082) 2023-03-27 15:35:01 +01:00
Stanislav Dmitrenko 450bfe2e17 android: small layout change in moderated item (#2083) 2023-03-27 15:11:17 +01:00
Evgeny Poberezkin c79eb36a7a core: update file status on XFTP progress events (#2079)
* core: update file status on XFTP progress events

* update simplexmq
2023-03-27 12:37:22 +01:00
Evgeny Poberezkin a58b3a42db Merge branch 'stable' 2023-03-27 12:23:42 +01:00
Evgeny Poberezkin e344958224 blog: v4.6 announcement (#2078)
* blog: v4.6 announcement

* update post

* corrections

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-27 12:23:23 +01:00
spaced4ndy 1ba210fe77 android: support XFTP files (#2070) 2023-03-24 19:06:36 +04:00
spaced4ndy 7898395359 Merge branch 'master' into xftp 2023-03-24 16:27:23 +04:00
spaced4ndy aeb732c2f6 ios: support XFTP files (#2064) 2023-03-24 15:20:15 +04:00
spaced4ndy 8d6fe2be99 core: restore stateTVar imports 2023-03-23 17:29:04 +04:00
spaced4ndy babbca48f8 Merge branch 'master' into xftp 2023-03-23 13:58:23 +04:00
spaced4ndy 2a9c138a23 xftp: set xftp config (#2059) 2023-03-22 22:20:12 +04:00
spaced4ndy 47c6daf0cc xftp: set app tmp directory (#2054) 2023-03-22 18:48:38 +04:00
spaced4ndy 60d6a47bdb xftp: delete agent rcv files on completion, error, item delete (#2040) 2023-03-21 15:21:14 +04:00
Evgeny Poberezkin cfc323862f update simplexmq 2023-03-18 16:28:07 +00:00
Evgeny Poberezkin b0c9ba05f3 Merge branch 'master' into xftp 2023-03-18 11:00:30 +00:00
Evgeny Poberezkin 00d5f3b769 Merge branch 'master' into xftp 2023-03-18 09:59:25 +00:00
Evgeny Poberezkin 858f0f2650 Merge branch 'master' into xftp 2023-03-18 08:38:27 +00:00
Evgeny Poberezkin a8fa9b5e58 Merge branch 'master' into xftp 2023-03-17 09:58:36 +00:00
Evgeny Poberezkin f379fd0f8c xftp: sending file completion status (#2016)
* xftp: sending file completion status

* fix type

* fix type 2

* fix
2023-03-16 13:58:01 +00:00
spaced4ndy 34a3387830 core: xftp servers option; use local xftp server in tests (#2015) 2023-03-16 14:12:19 +04:00
spaced4ndy 12200a74ff core: XFTP file transfer test (#2009) 2023-03-16 10:49:57 +04:00
spaced4ndy fda41817e9 core: XFTP accept; provide save path to agent (#2005) 2023-03-14 21:51:35 +04:00
spaced4ndy 9b7fbfd513 core: rcv file events (#2002) 2023-03-14 15:26:40 +04:00
Evgeny Poberezkin e21b4d4236 xftp: send file descriptions when ready (#1999)
* xftp: send file descriptions when ready

* remove comments, update progress on completion

* update simplexmq

* fix error condition

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>

* fix conflict

* saveMemberFD

* more efficient list merging

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-14 13:28:54 +04:00
spaced4ndy bfc178faf3 core: process rcv file description (#1997)
* core: process rcv file description

* refactor, groups

* view

* refactor

* update simplexmq

* refactor

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-14 11:42:44 +04:00
Evgeny Poberezkin d7f9e17bcb core: use XFTP to send and receive files (#1993)
* core: use XFTP to send and receive files

* xftp files progress

* xftp reception stubs, migration

* update simplexmq

* xftp sequence diagram

* additional chat events

* send file via XFTP

* send XFTP file description inline when file is uploaded
2023-03-13 10:30:32 +00:00
297 changed files with 37512 additions and 4928 deletions
+5 -5
View File
@@ -52,9 +52,9 @@ jobs:
- os: ubuntu-20.04
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-20_04-x86-64
- os: ubuntu-18.04
- os: ubuntu-22.04
cache_path: ~/.cabal/store
asset_name: simplex-chat-ubuntu-18_04-x86-64
asset_name: simplex-chat-ubuntu-22_04-x86-64
- os: macos-latest
cache_path: ~/.cabal/store
asset_name: simplex-chat-macos-x86-64
@@ -96,7 +96,7 @@ jobs:
run: brew install pkg-config
- name: Unix prepare cabal.project.local for Ubuntu
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-18.04'
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
shell: bash
run: |
echo "ignore-project: False" >> cabal.project.local
@@ -112,8 +112,8 @@ jobs:
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
- name: Unix test
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
timeout-minutes: 20
if: matrix.os != 'windows-latest'
timeout-minutes: 30
shell: bash
run: cabal test --test-show-details=direct
+28 -14
View File
@@ -4,7 +4,7 @@
[![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)
| 19/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
@@ -48,7 +48,7 @@
## Join user groups
You can join an English-speaking users 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 join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-3](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FeDEgekVhh0zIBYvUupGWZ96kiBEMbXwK%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAS7Z3zQyOhZ9o1qzI9OTZRySpkFTagdLMa6Rc7opuNh4%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22cG3W1qxZGzyrvHTugx16bg%3D%3D%22%7D)
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
@@ -66,6 +66,10 @@ The channel through which you share the link does not have to be secure - it is
After you connect, you can [verify connection security code](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification).
## User guide (NEW)
Read about the app features and settings in the new [User guide](./docs/guide/README.md).
## Help translating SimpleX Chat
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages.
@@ -75,16 +79,18 @@ Join our translators to help SimpleX grow!
|locale|language |contributor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[website](https://simplex.chat)|Github docs|
|:----:|:-------:|:---------:|:---------:|:---------:|:---------:|
|🇬🇧 en|English | |✓|✓|✓|✓|
|ar|العربية |[jermanuts](https://github.com/jermanuts)||[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/cs/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/cs/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/cs/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)|
|🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/de/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/de/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/de/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/de/)||
|🇪🇸 es|Español ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi_sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)|||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
Languages in progress: Arabic, Hindi, Japanese, Spanish 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!
Languages in progress: Arabic, Japanese, Korean, Portuguese and [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
@@ -175,13 +181,17 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent updates:
[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).
[Mar 28, 2023. v4.6 released - with Android 8+ and ARMv7a support, hidden profiles, community moderation, improved audio/video calls and reduced battery usage](./blog/20230328-simplex-chat-v4-6-hidden-profiles.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).
[Mar 1, 2023. SimpleX File Transfer Protocol send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.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).
[Feb 4, 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 3, 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).
[Dec 6, 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).
[Nov 8, 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).
@@ -281,17 +291,21 @@ If you are considering developing with SimpleX platform please get in touch for
- ✅ 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.
- File server to optimize for efficient and private sending of large files.
- Improved audio & video calls.
- ✅ Support older Android OS and 32-bit CPUs.
- ✅ Hidden chat profiles.
- 🏗 Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md).
- 🏗 Video messages.
- 🏗 SMP queue redundancy and rotation (manual is supported).
- 🏗 Reduced battery and traffic usage in large groups.
- 🏗 Support older Android OS and 32-bit CPUs.
- Include optional message into connection request sent via contact address.
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Access password/pin (with optional alternative access password).
- Local app files encryption.
- Video messages.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- Privately share your location.
- Feeds/broadcasts.
- Web widgets for custom interactivity in the chats.
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
+24 -10
View File
@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 26
targetSdk 32
versionCode 107
versionName "4.6"
versionCode 117
versionName "5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -77,7 +77,18 @@ android {
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
// if (isRelease) {
// Comma separated list of languages that will be included in the apk
android.defaultConfig.resConfigs("en", "cs", "de", "es", "fr", "it", "nl", "ru", "zh-rCN")
android.defaultConfig.resConfigs(
"en",
"cs",
"de",
"es",
"fr",
"it",
"nl",
"pl",
"ru",
"zh-rCN"
)
// }
if (isBundle) {
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'
@@ -143,6 +154,9 @@ dependencies {
implementation "io.coil-kt:coil-compose:2.1.0"
implementation "io.coil-kt:coil-gif:2.1.0"
// Video support
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
@@ -150,19 +164,12 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}
def buildType = "unknown"
// Don't do anything if no compression is needed
if (compression_level != "0") {
tasks.whenTaskAdded { task ->
if (task.name == 'packageDebug') {
task.doLast {
buildType = "debug"
}
task.finalizedBy compressApk
} else if (task.name == 'packageRelease') {
task.doLast {
buildType = "release"
}
task.finalizedBy compressApk
}
}
@@ -170,6 +177,13 @@ if (compression_level != "0") {
tasks.register("compressApk") {
doLast {
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
def buildType
if (isRelease) {
buildType = "release"
} else {
buildType = "debug"
}
def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome()
def sdkDir = android.getSdkDirectory().getAbsolutePath()
def keyAlias = ""
+5 -3
View File
@@ -35,7 +35,7 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass
// from simplex-chat
typedef long* chat_ctrl;
extern char *chat_migrate_init(const char *path, const char *key, chat_ctrl *ctrl);
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
@@ -44,13 +44,15 @@ extern char *chat_parse_server(const char *str);
extern char *chat_password_hash(const char *pwd, const char *salt);
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
jlong _ctrl = (jlong) 0;
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, &_ctrl));
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm);
// Creating array of Object's (boxed values can be passed, eg. Long instead of long)
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
@@ -12,8 +12,7 @@ import androidx.activity.viewModels
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.runtime.*
@@ -25,8 +24,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.NtfManager
import chat.simplex.app.MainActivity.Companion.enteredBackground
import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.SimpleXTheme
@@ -37,8 +36,11 @@ import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chatlist.*
import chat.simplex.app.views.database.DatabaseErrorView
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import chat.simplex.app.views.usersettings.LAMode
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -93,7 +95,7 @@ class MainActivity: FragmentActivity() {
laFailed,
::runAuthenticate,
::setPerformLA,
showLANotice = { m.controller.showLANotice(this) }
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
)
}
}
@@ -111,7 +113,8 @@ class MainActivity: FragmentActivity() {
override fun onResume() {
super.onResume()
val enteredBackgroundVal = enteredBackground.value
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
runAuthenticate()
}
}
@@ -128,6 +131,7 @@ class MainActivity: FragmentActivity() {
override fun onStop() {
super.onStop()
VideoPlayer.stopAll()
enteredBackground.value = elapsedRealtime()
}
@@ -164,16 +168,27 @@ class MainActivity: FragmentActivity() {
delay(50)
withContext(Dispatchers.Main) {
authenticate(
generalGetString(R.string.auth_unlock),
generalGetString(R.string.auth_log_in_using_credential),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_unlock)
else
generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_log_in_using_credential)
else
generalGetString(R.string.auth_unlock),
this@MainActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
userAuthorized.value = true
is LAResult.Error, LAResult.Failed ->
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
laFailed.value = true
LAResult.Unavailable -> {
if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) {
laFailedAlert()
}
}
is LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.appPrefs.performLA.set(false)
@@ -187,21 +202,116 @@ class MainActivity: FragmentActivity() {
}
}
private fun setPerformLA(on: Boolean) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
if (on) {
enableLA()
} else {
disableLA()
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!laNoticeShown.get()) {
laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title_simplex_lock),
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
confirmText = generalGetString(R.string.la_notice_turn_on),
onConfirm = {
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
showChooseLAMode(laNoticeShown, activity)
}
}
)
}
}
private fun enableLA() {
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
laNoticeShown.set(true)
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.la_lock_mode),
text = null,
confirmText = generalGetString(R.string.la_lock_mode_passcode),
dismissText = generalGetString(R.string.la_lock_mode_system),
onConfirm = {
AlertManager.shared.hideAlert()
setPasscode()
},
onDismiss = {
AlertManager.shared.hideAlert()
initialEnableLA(activity)
}
)
}
private fun initialEnableLA(activity: FragmentActivity) {
val m = vm.chatModel
val appPrefs = m.controller.appPrefs
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
authenticate(
generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
this@MainActivity,
activity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = false
appPrefs.performLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.performLA.value = false
appPrefs.performLA.set(false)
m.showAdvertiseLAUnavailableAlert.value = true
}
}
}
)
}
private fun setPasscode() {
val chatModel = vm.chatModel
val appPrefs = chatModel.controller.appPrefs
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
chatModel.performLA.value = true
appPrefs.performLA.set(true)
appPrefs.laMode.set(LAMode.PASSCODE)
laTurnedOnAlert()
},
cancel = {
chatModel.performLA.value = false
appPrefs.performLA.set(false)
laPasscodeNotSetAlert()
},
close)
}
}
}
private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
if (on) {
enableLA(activity)
} else {
disableLA(activity)
}
}
private fun enableLA(activity: FragmentActivity) {
val m = vm.chatModel
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_enable_simplex_lock)
else
generalGetString(R.string.new_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_confirm_credential)
else
"",
activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
@@ -210,11 +320,13 @@ class MainActivity: FragmentActivity() {
prefPerformLA.set(true)
laTurnedOnAlert()
}
is LAResult.Error, LAResult.Failed -> {
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = false
prefPerformLA.set(false)
laFailedAlert()
}
LAResult.Unavailable -> {
is LAResult.Unavailable -> {
m.performLA.value = false
prefPerformLA.set(false)
laUnavailableInstructionAlert()
@@ -224,24 +336,33 @@ class MainActivity: FragmentActivity() {
)
}
private fun disableLA() {
private fun disableLA(activity: FragmentActivity) {
val m = vm.chatModel
authenticate(
generalGetString(R.string.auth_disable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
this@MainActivity,
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_disable_simplex_lock)
else
generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_confirm_credential)
else
generalGetString(R.string.auth_disable_simplex_lock),
activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
LAResult.Success -> {
m.performLA.value = false
prefPerformLA.set(false)
ksAppPassword.remove()
}
is LAResult.Error, LAResult.Failed -> {
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = true
prefPerformLA.set(true)
laFailedAlert()
}
LAResult.Unavailable -> {
is LAResult.Unavailable -> {
m.performLA.value = false
prefPerformLA.set(false)
laUnavailableTurningOffAlert()
@@ -263,7 +384,7 @@ fun MainPage(
userAuthorized: MutableState<Boolean?>,
laFailed: MutableState<Boolean>,
runAuthenticate: () -> Unit,
setPerformLA: (Boolean) -> Unit,
setPerformLA: (Boolean, FragmentActivity) -> Unit,
showLANotice: () -> Unit
) {
var showChatDatabaseError by rememberSaveable {
@@ -391,6 +512,14 @@ fun MainPage(
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
AlertManager.shared.showInView()
}
DisposableEffectOnRotate {
// When using lock delay = 0 and screen rotates, the app will be locked which is not useful.
// Let's prolong the unlocked period to 3 sec for screen rotation to take place
if (chatModel.controller.appPrefs.laLockDelay.get() == 0) {
enteredBackground.value = elapsedRealtime() + 3000
}
}
}
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
@@ -401,7 +530,8 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId")
if (chatId != null) {
withBGApi {
if (userId != null && userId != chatModel.currentUser.value?.userId) {
awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
chatModel.controller.changeActiveUser(userId, null)
}
val cInfo = chatModel.getChat(chatId)?.chatInfo
@@ -413,7 +543,8 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
NtfManager.ShowChatsAction -> {
Log.d(TAG, "processNotificationIntent: ShowChatsAction")
withBGApi {
if (userId != null && userId != chatModel.currentUser.value?.userId) {
awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
chatModel.controller.changeActiveUser(userId, null)
}
chatModel.chatId.value = null
@@ -451,14 +582,23 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
when {
"text/plain" == intent.type -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
chatModel.sharedContent.value = SharedContent.Text(it)
intent.type == "text/plain" -> {
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text != null) {
chatModel.sharedContent.value = SharedContent.Text(text)
}
}
intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(it))
} // All other mime types
else -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
isMediaIntent(intent) -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) {
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri))
} // All other mime types
}
else -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) {
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri)
}
}
}
}
@@ -466,16 +606,23 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
// Close active chat and show a list of chats
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
Log.e(TAG, "ACTION_SEND_MULTIPLE ${intent.type}")
when {
intent.type?.startsWith("image/") == true -> (intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>)?.let {
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
} // All other mime types
isMediaIntent(intent) -> {
val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>
if (uris != null) {
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris)
} // All other mime types
}
else -> {}
}
}
}
}
fun isMediaIntent(intent: Intent): Boolean =
intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) {
@@ -504,6 +651,20 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
}
}
}
suspend fun awaitChatStartedIfNeeded(chatModel: ChatModel, timeout: Long = 30_000) {
// Still decrypting database
if (chatModel.chatRunning.value == null) {
val step = 50L
for (i in 0..(timeout / step)) {
if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) {
break
}
delay(step)
}
}
}
//fun testJson() {
// val str: String = """
// """.trimIndent()
@@ -26,7 +26,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int
// SimpleX API
typealias ChatCtrl = Long
external fun chatMigrateInit(dbPath: String, dbKey: String): Array<Any>
external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
external fun chatRecvMsg(ctrl: ChatCtrl): String
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
@@ -41,10 +41,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
val defaultLocale: Locale = Locale.getDefault()
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
val res: DBMigrationResult = kotlin.runCatching {
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
@@ -326,4 +326,4 @@ class SimplexService: Service() {
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
}
}
}
@@ -54,10 +54,8 @@ class ChatModel(val controller: ChatController) {
val terminalItems = mutableStateListOf<TerminalItem>()
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
val userSMPServers = mutableStateOf<(List<ServerCfg>)?>(null)
// Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
val presetSMPServers = mutableStateOf<(List<String>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent
@@ -192,10 +190,13 @@ class ChatModel(val controller: ChatController) {
// add to current chat
if (chatId.value == cInfo.id) {
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)
// Prevent situation when chat item already in the list received from backend
if (chatItems.none { it.id == cItem.id }) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
} else {
chatItems.add(cItem)
}
}
}
}
@@ -222,19 +223,19 @@ class ChatModel(val controller: ChatController) {
res = true
}
// update current chat
if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
return false
} else {
withContext(Dispatchers.Main) {
return if (chatId.value == cInfo.id) {
withContext(Dispatchers.Main) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
false
} else {
chatItems.add(cItem)
true
}
return true
}
} else {
return res
res
}
}
@@ -728,6 +729,7 @@ data class Contact(
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
ChatFeature.Calls -> mergedPreferences.calls.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 }
@@ -746,12 +748,14 @@ data class Contact(
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Calls -> mergedPreferences.calls.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
ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO
}
companion object {
@@ -881,6 +885,7 @@ data class GroupInfo (
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
ChatFeature.Voice -> fullGroupPreferences.voice.on
ChatFeature.Calls -> false
}
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
override val displayName get() = groupProfile.displayName
@@ -1307,6 +1312,7 @@ data class ChatItem (
is CIContent.SndCall -> showNtfDir
is CIContent.RcvCall -> false // notification is shown on CallInvitation instead
is CIContent.RcvIntegrityError -> showNtfDir
is CIContent.RcvDecryptionError -> showNtfDir
is CIContent.RcvGroupInvitation -> showNtfDir
is CIContent.SndGroupInvitation -> showNtfDir
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
@@ -1419,7 +1425,7 @@ data class ChatItem (
file = null
)
}
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
const val TEMP_LIVE_CHAT_ITEM_ID = -2L
@@ -1464,10 +1470,10 @@ data class ChatItem (
file = null
)
fun invalidJSON(json: String): ChatItem =
fun invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String): ChatItem =
ChatItem(
chatDir = CIDirection.DirectSnd(),
meta = CIMeta.invalidJSON(),
chatDir = chatDir ?: CIDirection.DirectSnd(),
meta = meta ?: CIMeta.invalidJSON(),
content = CIContent.InvalidJSON(json),
quotedItem = null,
file = null
@@ -1610,6 +1616,7 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@@ -1636,6 +1643,7 @@ sealed class CIContent: ItemContent {
is SndCall -> status.text(duration)
is RcvCall -> status.text(duration)
is RcvIntegrityError -> msgError.text
is RcvDecryptionError -> msgDecryptError.text
is RcvGroupInvitation -> groupInvitation.text
is SndGroupInvitation -> groupInvitation.text
is RcvGroupEventContent -> rcvGroupEvent.text
@@ -1674,6 +1682,17 @@ sealed class CIContent: ItemContent {
}
}
@Serializable
enum class MsgDecryptError {
@SerialName("ratchetHeader") RatchetHeader,
@SerialName("tooManySkipped") TooManySkipped;
val text: String get() = when (this) {
RatchetHeader -> generalGetString(R.string.decryption_error)
TooManySkipped -> generalGetString(R.string.decryption_error)
}
}
@Serializable
class CIQuote (
val chatDir: CIDirection? = null,
@@ -1711,18 +1730,40 @@ class CIFile(
val fileName: String,
val fileSize: Long,
val filePath: String? = null,
val fileStatus: CIFileStatus
val fileStatus: CIFileStatus,
val fileProtocol: FileProtocol
) {
val loaded: Boolean = when (fileStatus) {
CIFileStatus.SndStored -> true
CIFileStatus.SndTransfer -> true
CIFileStatus.SndComplete -> true
CIFileStatus.SndCancelled -> true
CIFileStatus.RcvInvitation -> false
CIFileStatus.RcvAccepted -> false
CIFileStatus.RcvTransfer -> false
CIFileStatus.RcvCancelled -> false
CIFileStatus.RcvComplete -> true
is CIFileStatus.SndStored -> true
is CIFileStatus.SndTransfer -> true
is CIFileStatus.SndComplete -> true
is CIFileStatus.SndCancelled -> true
is CIFileStatus.SndError -> true
is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> false
is CIFileStatus.RcvTransfer -> false
is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true
is CIFileStatus.RcvError -> false
}
val cancelAction: CancelAction? = when (fileStatus) {
is CIFileStatus.SndStored -> sndCancelAction
is CIFileStatus.SndTransfer -> sndCancelAction
is CIFileStatus.SndComplete ->
if (fileProtocol == FileProtocol.XFTP) {
revokeCancelAction
} else {
null
}
is CIFileStatus.SndCancelled -> null
is CIFileStatus.SndError -> null
is CIFileStatus.RcvInvitation -> null
is CIFileStatus.RcvAccepted -> rcvCancelAction
is CIFileStatus.RcvTransfer -> rcvCancelAction
is CIFileStatus.RcvCancelled -> null
is CIFileStatus.RcvComplete -> null
is CIFileStatus.RcvError -> null
}
companion object {
@@ -1733,21 +1774,67 @@ class CIFile(
filePath: String? = "test.txt",
fileStatus: CIFileStatus = CIFileStatus.RcvComplete
): CIFile =
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus)
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP)
}
}
@Serializable
enum class CIFileStatus {
@SerialName("snd_stored") SndStored,
@SerialName("snd_transfer") SndTransfer,
@SerialName("snd_complete") SndComplete,
@SerialName("snd_cancelled") SndCancelled,
@SerialName("rcv_invitation") RcvInvitation,
@SerialName("rcv_accepted") RcvAccepted,
@SerialName("rcv_transfer") RcvTransfer,
@SerialName("rcv_complete") RcvComplete,
@SerialName("rcv_cancelled") RcvCancelled;
class CancelAction(
val uiActionId: Int,
val alert: AlertInfo
)
@Serializable
class AlertInfo(
val titleId: Int,
val messageId: Int,
val confirmId: Int
)
private val sndCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_snd_file__title,
messageId = R.string.stop_snd_file__message,
confirmId = R.string.stop_file__confirm
)
)
private val revokeCancelAction: CancelAction = CancelAction(
uiActionId = R.string.revoke_file__action,
alert = AlertInfo(
titleId = R.string.revoke_file__title,
messageId = R.string.revoke_file__message,
confirmId = R.string.revoke_file__confirm
)
)
private val rcvCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_rcv_file__title,
messageId = R.string.stop_rcv_file__message,
confirmId = R.string.stop_file__confirm
)
)
@Serializable
enum class FileProtocol {
@SerialName("smp") SMP,
@SerialName("xftp") XFTP;
}
@Serializable
sealed class CIFileStatus {
@Serializable @SerialName("sndStored") object SndStored: CIFileStatus()
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@@ -1758,6 +1845,7 @@ sealed class MsgContent {
@Serializable(with = MsgContentSerializer::class) class MCText(override val text: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCLink(override val text: String, val preview: LinkPreview): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCImage(override val text: String, val image: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCVideo(override val text: String, val image: String, val duration: Int): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): 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()
@@ -1811,6 +1899,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
element<String>("text")
element<String>("image")
})
element("MCVideo", buildClassSerialDescriptor("MCVideo") {
element<String>("text")
element<String>("image")
element<Int>("duration")
})
element("MCFile", buildClassSerialDescriptor("MCFile") {
element<String>("text")
})
@@ -1834,6 +1927,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format"
MsgContent.MCImage(text, image)
}
"video" -> {
val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format"
val duration = json["duration"]?.jsonPrimitive?.intOrNull ?: 0
MsgContent.MCVideo(text, image, duration)
}
"voice" -> {
val duration = json["duration"]?.jsonPrimitive?.intOrNull ?: 0
MsgContent.MCVoice(text, duration)
@@ -1869,6 +1967,13 @@ object MsgContentSerializer : KSerializer<MsgContent> {
put("text", value.text)
put("image", value.image)
}
is MsgContent.MCVideo ->
buildJsonObject {
put("type", "video")
put("text", value.text)
put("image", value.image)
put("duration", value.duration)
}
is MsgContent.MCVoice ->
buildJsonObject {
put("type", "voice")
@@ -2004,7 +2109,11 @@ enum class CICallStatus {
}
}
fun durationText(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60)
fun durationText(sec: Int): String {
val s = sec % 60
val m = sec / 60
return if (m < 60) "%02d:%02d".format(m, s) else "%02d:%02d:%02d".format(m / 60, m % 60, s)
}
@Serializable
sealed class MsgErrorType() {
File diff suppressed because it is too large Load Diff
@@ -27,3 +27,4 @@ val WarningOrange = Color(255, 127, 0, 255)
val WarningYellow = Color(255, 192, 0, 255)
val FileLight = Color(183, 190, 199, 255)
val FileDark = Color(101, 101, 106, 255)
val MenuTextColorDark = Color.White.copy(alpha = 0.8f)
@@ -8,4 +8,4 @@ val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
)
@@ -18,6 +18,7 @@ val DEFAULT_PADDING = 16.dp
val DEFAULT_SPACE_AFTER_ICON = 4.dp
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
val DEFAULT_BOTTOM_PADDING = 48.dp
val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
val DarkColorPalette = darkColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
@@ -27,7 +28,7 @@ val DarkColorPalette = darkColors(
// surface = Color.Black,
// background = Color(0xFF121212),
// surface = Color(0xFF121212),
// error = Color(0xFFCF6679),
error = Color.Red,
onBackground = Color(0xFFFFFBFA),
onSurface = Color(0xFFFFFBFA),
// onError: Color = Color.Black,
@@ -36,6 +37,7 @@ val LightColorPalette = lightColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
primaryVariant = SimplexGreen,
secondary = LightGray,
error = Color.Red,
// background = Color.White,
// surface = Color.White
// onPrimary = Color.White,
@@ -74,4 +76,4 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
shapes = Shapes,
content = content
)
}
}
@@ -6,19 +6,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBackIosNew
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
@@ -26,61 +29,72 @@ import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.AppBarTitle
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
fun isValidDisplayName(name: String) : Boolean {
return (name.firstOrNull { it.isWhitespace() }) == null
return (name.firstOrNull { it.isWhitespace() }) == null && !name.startsWith("@") && !name.startsWith("#")
}
@Composable
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val displayName = rememberSaveable { mutableStateOf("") }
val fullName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {
AppBarTitle(stringResource(R.string.create_profile), false)
ReadableText(R.string.your_profile_is_stored_on_your_device)
ReadableText(R.string.profile_is_only_shared_with_your_contacts)
Spacer(Modifier.height(10.dp))
Text(
stringResource(R.string.display_name),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 3.dp)
)
ProfileNameField(displayName, focusRequester)
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.full_name_optional__prompt),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 5.dp)
)
ProfileNameField(fullName)
/*CloseSheetBar(close = {
if (chatModel.users.isEmpty()) {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
close()
}
})*/
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
AppBarTitleCentered(stringResource(R.string.create_profile))
ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues())
ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center)
Spacer(Modifier.height(DEFAULT_PADDING * 1.5f))
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.display_name),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Text(
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.full_name_optional__prompt),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName, "", ::isValidDisplayName)
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
if (chatModel.users.isEmpty()) {
SimpleButton(
SimpleButtonDecorated(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew
icon = Icons.Outlined.ArrowBackIosNew,
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
}
Spacer(Modifier.fillMaxWidth().weight(1f))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier
val createColor: Color
@@ -93,7 +107,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
}
Surface(shape = RoundedCornerShape(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor)
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = createColor)
}
}
@@ -128,24 +142,50 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
}
@Composable
fun ProfileNameField(name: MutableState<String>, focusRequester: FocusRequester? = null) {
fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) {
var valid by rememberSaveable { mutableStateOf(true) }
var focused by rememberSaveable { mutableStateOf(false) }
val strokeColor by remember {
derivedStateOf {
if (valid) {
if (focused) {
HighOrLowlight.copy(alpha = 0.6f)
} else {
HighOrLowlight.copy(alpha = 0.3f)
}
} else Color.Red
}
}
val modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.secondary)
.height(40.dp)
.clip(RoundedCornerShape(5.dp))
.padding(8.dp)
.padding(horizontal = DEFAULT_PADDING)
.navigationBarsWithImePadding()
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true,
cursorBrush = SolidColor(HighOrLowlight)
)
}
.onFocusChanged { focused = it.isFocused }
Box(
Modifier
.fillMaxWidth()
.height(52.dp)
.border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(50)),
contentAlignment = Alignment.Center
) {
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true,
cursorBrush = SolidColor(HighOrLowlight)
)
}
LaunchedEffect(Unit) {
snapshotFlow { name.value }
.distinctUntilChanged()
.collect {
valid = isValid(it)
}
}
}
@@ -106,4 +106,4 @@ class CallManager(val chatModel: ChatModel) {
chatModel.controller.ntfManager.cancelCallNotification()
}
}
}
}
@@ -12,6 +12,7 @@ import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -25,6 +26,7 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -36,7 +38,6 @@ import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.onboarding.SimpleXLogo
import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() {
@@ -186,6 +187,17 @@ fun IncomingCallLockScreenAlertLayout(
}
}
@Composable
private fun SimpleXLogo() {
Image(
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier
.padding(vertical = DEFAULT_PADDING)
.fillMaxWidth(0.80f)
)
}
@Composable
private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color, action: () -> Unit) {
Surface(
@@ -48,4 +48,4 @@ class SoundPlayer {
companion object {
val shared = SoundPlayer()
}
}
}
@@ -230,10 +230,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
}
},
receiveFile = { fileId ->
val user = chatModel.currentUser.value
if (user != null) {
withApi { chatModel.controller.receiveFile(user, fileId) }
}
withApi { chatModel.controller.receiveFile(user, fileId) }
},
cancelFile = { fileId ->
withApi { chatModel.controller.cancelFile(user, fileId) }
},
joinGroup = { groupId ->
withApi { chatModel.controller.apiJoinGroup(groupId) }
@@ -313,6 +313,7 @@ fun ChatLayout(
loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
@@ -357,7 +358,7 @@ fun ChatLayout(
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
)
}
}
@@ -377,7 +378,7 @@ fun ChatInfoToolbar(
onSearchValueChanged: (String) -> Unit,
) {
val scope = rememberCoroutineScope()
var showMenu by rememberSaveable { mutableStateOf(false) }
val showMenu = rememberSaveable { mutableStateOf(false) }
var showSearch by rememberSaveable { mutableStateOf(false) }
val onBackClicked = {
if (!showSearch) {
@@ -392,15 +393,15 @@ fun ChatInfoToolbar(
val menuItems = arrayListOf<@Composable () -> Unit>()
menuItems.add {
ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), Icons.Outlined.Search, onClick = {
showMenu = false
showMenu.value = false
showSearch = true
})
}
if (chat.chatInfo is ChatInfo.Direct) {
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
barButtons.add {
IconButton({
showMenu = false
showMenu.value = false
startCall(CallMediaType.Audio)
}) {
Icon(Icons.Outlined.Phone, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
@@ -408,14 +409,14 @@ fun ChatInfoToolbar(
}
menuItems.add {
ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), Icons.Outlined.Videocam, onClick = {
showMenu = false
showMenu.value = false
startCall(CallMediaType.Video)
})
}
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
barButtons.add {
IconButton({
showMenu = false
showMenu.value = false
addMembers(chat.chatInfo.groupInfo)
}) {
Icon(Icons.Outlined.PersonAdd, stringResource(R.string.icon_descr_add_members), tint = MaterialTheme.colors.primary)
@@ -428,7 +429,7 @@ fun ChatInfoToolbar(
if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
if (ntfsEnabled.value) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications,
onClick = {
showMenu = false
showMenu.value = false
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
scope.launch {
delay(200)
@@ -439,7 +440,7 @@ fun ChatInfoToolbar(
}
barButtons.add {
IconButton({ showMenu = true }) {
IconButton({ showMenu.value = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
}
}
@@ -456,11 +457,7 @@ fun ChatInfoToolbar(
Divider(Modifier.padding(top = AppBarHeight))
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) {
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
Modifier.widthIn(min = 220.dp)
) {
DefaultDropdownMenu(showMenu) {
menuItems.forEach { it() }
}
}
@@ -530,6 +527,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
loadPrevMessages: (ChatInfo) -> Unit,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
@@ -576,6 +574,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
stopListening = true
}
}
DisposableEffectOnGone(
whenGone = {
VideoPlayer.releaseAll()
}
)
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem ->
CompositionLocalProvider(
@@ -595,10 +598,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) {
LaunchedEffect(Unit) {
scope.launch {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
if (cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
}
}
}
@@ -638,11 +643,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, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, 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, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
} else { // direct message
@@ -653,7 +658,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, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
@@ -779,36 +784,27 @@ fun BoxWithConstraintsScope.FloatingButtons(
}
val showButtonWithCounter = topUnreadCount > 0
val height = with(LocalDensity.current) { maxHeight.toPx() }
var showDropDown by remember { mutableStateOf(false) }
val showDropDown = remember { mutableStateOf(false) }
TopEndFloatingButton(
Modifier.padding(end = 16.dp, top = 24.dp).align(Alignment.TopEnd),
topUnreadCount,
showButtonWithCounter,
onClick = { scope.launch { listState.animateScrollBy(height) } },
onLongClick = { showDropDown = true }
onLongClick = { showDropDown.value = true }
)
DropdownMenu(
expanded = showDropDown,
onDismissRequest = { showDropDown = false },
Modifier.width(220.dp),
offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)
) {
DropdownMenuItem(
DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)) {
ItemAction(
generalGetString(R.string.mark_read),
Icons.Outlined.Check,
onClick = {
markRead(
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount
)
showDropDown = false
}
) {
Text(
generalGetString(R.string.mark_read),
maxLines = 1,
)
}
showDropDown.value = false
})
}
}
@@ -933,21 +929,26 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
}
}
sealed class ProviderMedia {
data class Image(val uri: Uri, val image: Bitmap): ProviderMedia()
data class Video(val uri: Uri, val preview: String): ProviderMedia()
}
private fun providerForGallery(
listStateIndex: Int,
chatItems: List<ChatItem>,
cItemId: Long,
scrollTo: (Int) -> Unit
): ImageGalleryProvider {
fun canShowImage(item: ChatItem): Boolean =
item.content.msgContent is MsgContent.MCImage && item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null
fun canShowMedia(item: ChatItem): Boolean =
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null)
fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? {
var processedInternalIndex = -skipInternalIndex.sign
val indexOfFirst = chatItems.indexOfFirst { it.id == initialChatId }
for (chatItemsIndex in if (skipInternalIndex >= 0) indexOfFirst downTo 0 else indexOfFirst..chatItems.lastIndex) {
val item = chatItems[chatItemsIndex]
if (canShowImage(item)) {
if (canShowMedia(item)) {
processedInternalIndex += skipInternalIndex.sign
}
if (processedInternalIndex == skipInternalIndex) {
@@ -961,16 +962,28 @@ private fun providerForGallery(
var initialChatId = cItemId
return object: ImageGalleryProvider {
override val initialIndex: Int = initialIndex
override val totalImagesSize = mutableStateOf(Int.MAX_VALUE)
override fun getImage(index: Int): Pair<Bitmap, Uri>? {
override val totalMediaSize = mutableStateOf(Int.MAX_VALUE)
override fun getMedia(index: Int): ProviderMedia? {
val internalIndex = initialIndex - index
val file = item(internalIndex, initialChatId)?.second?.file
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
val filePath = getLoadedFilePath(SimplexApp.context, file)
return if (imageBitmap != null && filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
imageBitmap to uri
} else null
val item = item(internalIndex, initialChatId)?.second ?: return null
return when (item.content.msgContent) {
is MsgContent.MCImage -> {
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, item.file)
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
if (imageBitmap != null && filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Image(uri, imageBitmap)
} else null
}
is MsgContent.MCVideo -> {
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
if (filePath != null) {
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
} else null
}
else -> null
}
}
override fun currentPageChanged(index: Int) {
@@ -982,7 +995,7 @@ private fun providerForGallery(
override fun scrollToStart() {
initialIndex = 0
initialChatId = chatItems.first { canShowImage(it) }.id
initialChatId = chatItems.first { canShowMedia(it) }.id
}
override fun onDismiss(index: Int) {
@@ -1060,6 +1073,7 @@ fun PreviewChatLayout() {
loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
@@ -1119,6 +1133,7 @@ fun PreviewGroupChatLayout() {
loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
@@ -4,22 +4,29 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.icons.outlined.Close
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.asImageBitmap
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.durationText
import chat.simplex.app.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.chat.item.SentColorLight
import chat.simplex.app.views.helpers.UploadContent
import chat.simplex.app.views.helpers.base64ToBitmap
@Composable
fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabled: Boolean) {
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
Row(
Modifier
.padding(top = 8.dp)
@@ -31,13 +38,32 @@ fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabl
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
) {
items(images.size) { index ->
val imageBitmap = base64ToBitmap(images[index]).asImageBitmap()
Image(
imageBitmap,
"preview image",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
itemsIndexed(media.images) { index, item ->
val content = media.content[index]
if (content is UploadContent.Video) {
Box(contentAlignment = Alignment.Center) {
val imageBitmap = base64ToBitmap(item).asImageBitmap()
Image(
imageBitmap,
"preview video",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
Icon(
Icons.Default.Videocam,
"preview video",
Modifier
.size(20.dp),
tint = Color.White
)
}
} else {
val imageBitmap = base64ToBitmap(item).asImageBitmap()
Image(
imageBitmap,
"preview image",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
}
}
}
if (cancelEnabled) {
@@ -12,11 +12,10 @@ import android.graphics.drawable.AnimatedImageDrawable
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
@@ -32,10 +31,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 androidx.core.content.ContextCompat
import androidx.core.net.toFile
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -52,7 +49,7 @@ import java.nio.file.Files
sealed class ComposePreview {
@Serializable object NoPreview: ComposePreview()
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
@Serializable class ImagePreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable class MediaPreview(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()
}
@@ -98,7 +95,7 @@ data class ComposeState(
val sendEnabled: () -> Boolean
get() = {
val hasContent = when (preview) {
is ComposePreview.ImagePreview -> true
is ComposePreview.MediaPreview -> true
is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true
else -> message.isNotEmpty() || liveMessage != null
@@ -111,7 +108,7 @@ data class ComposeState(
val linkPreviewAllowed: Boolean
get() =
when (preview) {
is ComposePreview.ImagePreview -> false
is ComposePreview.MediaPreview -> false
is ComposePreview.VoicePreview -> false
is ComposePreview.FilePreview -> false
else -> useLinkPreviews
@@ -161,7 +158,8 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview {
is MsgContent.MCText -> ComposePreview.NoPreview
is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview)
// TODO: include correct type
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCImage -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCVideo -> ComposePreview.MediaPreview(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
@@ -182,6 +180,7 @@ fun ComposeView(
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
@@ -189,7 +188,7 @@ fun ComposeView(
val bitmap: Bitmap? = getBitmapFromUri(uri)
if (bitmap != null) {
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
}
}
}
@@ -200,43 +199,56 @@ fun ComposeView(
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
}
}
val processPickedImage = { uris: List<Uri>, text: String? ->
val processPickedMedia = { uris: List<Uri>, text: String? ->
val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
val drawable = getDrawableFromUri(uri)
var bitmap: Bitmap? = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
content.add(UploadContent.AnimatedImage(uri))
} else {
bitmap = null
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE))
)
var bitmap: Bitmap? = null
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(SimplexApp.context, uri)?.split(".")?.last())?.contains("image/") == true
when {
isImage -> {
// Image
val drawable = getDrawableFromUri(uri)
bitmap = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri))
} else {
bitmap = null
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
} else {
content.add(UploadContent.SimpleImage(uri))
}
}
else -> {
// Video
val res = getBitmapFromVideo(uri)
bitmap = res.preview
val durationMs = res.duration
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
}
} else {
content.add(UploadContent.SimpleImage(uri))
}
if (bitmap != null) {
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
}
}
if (imagesPreview.isNotEmpty()) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview, content))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content))
}
}
val processPickedFile = { uri: Uri?, text: String? ->
if (uri != null) {
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
if (fileSize != null && fileSize <= maxFileSize) {
val fileName = getFileName(SimplexApp.context, uri)
if (fileName != null) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
@@ -244,19 +256,22 @@ fun ComposeView(
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE))
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
}
}
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it, null) }
val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) {
AttachmentOption.TakePhoto -> {
AttachmentOption.CameraPhoto -> {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
cameraLauncher.launchWithFallback()
@@ -267,15 +282,23 @@ fun ComposeView(
}
attachmentOption.value = null
}
AttachmentOption.PickImage -> {
AttachmentOption.GalleryImage -> {
try {
galleryLauncher.launch(0)
galleryImageLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
galleryLauncherFallback.launch("image/*")
galleryImageLauncherFallback.launch("image/*")
}
attachmentOption.value = null
}
AttachmentOption.PickFile -> {
AttachmentOption.GalleryVideo -> {
try {
galleryVideoLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
galleryVideoLauncherFallback.launch("video/*")
}
attachmentOption.value = null
}
AttachmentOption.File -> {
filesLauncher.launch("*/*")
attachmentOption.value = null
}
@@ -392,6 +415,7 @@ fun ComposeView(
is MsgContent.MCText -> checkLinkPreview()
is MsgContent.MCLink -> checkLinkPreview()
is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image)
is MsgContent.MCVideo -> MsgContent.MCVideo(msgText, image = msgContent.image, duration = msgContent.duration)
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)
@@ -431,15 +455,20 @@ fun ComposeView(
when (val preview = cs.preview) {
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.ImagePreview -> {
is ComposePreview.MediaPreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
is UploadContent.Video -> saveFileFromUri(context, it.uri)
}
if (file != null) {
files.add(file)
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
if (it is UploadContent.Video) {
msgs.add(MsgContent.MCVideo(if (preview.content.lastIndex == index) msgText else "", preview.images[index], it.duration))
} else {
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
}
}
}
}
@@ -473,7 +502,11 @@ fun ComposeView(
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)) {
if (sent == null &&
(cs.preview is ComposePreview.MediaPreview ||
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
}
}
@@ -595,8 +628,8 @@ fun ComposeView(
when (val preview = composeState.value.preview) {
ComposePreview.NoPreview -> {}
is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview)
is ComposePreview.ImagePreview -> ComposeImageView(
preview.images,
is ComposePreview.MediaPreview -> ComposeImageView(
preview,
::cancelImages,
cancelEnabled = !composeState.value.editing
)
@@ -634,7 +667,7 @@ fun ComposeView(
when (val shared = chatModel.sharedContent.value) {
is SharedContent.Text -> onMessageChange(shared.text)
is SharedContent.Images -> processPickedImage(shared.uris, shared.text)
is SharedContent.Media -> processPickedMedia(shared.uris, shared.text)
is SharedContent.File -> processPickedFile(shared.uri, shared.text)
null -> {}
}
@@ -767,7 +800,7 @@ class PickFromGallery: ActivityResultContract<Int, Uri?>() {
override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data
}
class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() {
class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() {
override fun createIntent(context: Context, input: Int) =
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
@@ -792,3 +825,30 @@ class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() {
else
emptyList()
}
class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() {
override fun createIntent(context: Context, input: Int) =
Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI).apply {
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
type = "video/*"
}
override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> =
if (intent?.data != null)
listOf(intent.data!!)
else if (intent?.clipData != null)
with(intent.clipData!!) {
val uris = ArrayList<Uri>()
for (i in 0 until kotlin.math.min(itemCount, 10)) {
val uri = getItemAt(i).uri
if (uri != null) uris.add(uri)
}
if (itemCount > 10) {
AlertManager.shared.showAlertMsg(R.string.videos_limit_title, R.string.videos_limit_desc)
}
uris
}
else
emptyList()
}
@@ -88,7 +88,7 @@ private fun ContactPreferencesLayout(
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))
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl))
}
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
@@ -104,6 +104,11 @@ private fun ContactPreferencesLayout(
applyPrefs(featuresAllowed.copy(voice = it))
}
SectionSpacer()
val allowCalls: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) }
FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) {
applyPrefs(featuresAllowed.copy(calls = it))
}
SectionSpacer()
ResetSaveButtons(
reset = reset,
save = savePrefs,
@@ -138,6 +143,7 @@ private fun FeatureSection(
ContactFeatureAllowed.values(userDefault).map { it to it.text },
allowFeature,
icon = null,
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
onSelected = onSelected
)
}
@@ -147,7 +153,7 @@ private fun FeatureSection(
pref.contactPreference.allow.text
)
}
SectionTextFooter(feature.enabledDescription(enabled))
SectionTextFooter(feature.enabledDescription(enabled) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
}
@Composable
@@ -75,7 +75,7 @@ fun SendMsgView(
) {
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 showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || 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) }
@@ -155,20 +155,18 @@ fun SendMsgView(
cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null
) {
var showDropdown by rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
val showDropdown = rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false },
Modifier.width(220.dp),
DefaultDropdownMenu(
showDropdown,
) {
ItemAction(
generalGetString(R.string.send_live_message),
Icons.Filled.Bolt,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown = false
showDropdown.value = false
}
)
}
@@ -227,7 +225,7 @@ private fun NativeKeyboard(
} catch (e: Exception) {
return@OnCommitContentListener false
}
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Images("", listOf(inputContentInfo.contentUri))
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri))
true
}
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)
@@ -134,4 +134,4 @@ private fun splitToParts(s: String, length: Int): String {
return (0..(s.length - 1) / length)
.map { s.drop(it * length).take(length) }
.joinToString(separator = "\n")
}
}
@@ -28,6 +28,7 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.ChatInfoToolbarTitle
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.InfoAboutIncognito
import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable
@@ -37,6 +38,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
var allowModifyMembers by remember { mutableStateOf(true) }
BackHandler(onBack = close)
AddGroupMembersLayout(
chatModel.incognito.value,
groupInfo = groupInfo,
creatingGroup = creatingGroup,
contactsToAdd = getContactsToAdd(chatModel),
@@ -85,6 +87,7 @@ fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
@Composable
fun AddGroupMembersLayout(
chatModelIncognito: Boolean,
groupInfo: GroupInfo,
creatingGroup: Boolean,
contactsToAdd: List<Contact>,
@@ -105,6 +108,14 @@ fun AddGroupMembersLayout(
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.button_add_members))
InfoAboutIncognito(
chatModelIncognito,
false,
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
generalGetString(R.string.group_main_profile_sent),
true
)
Spacer(Modifier.size(DEFAULT_PADDING))
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
@@ -318,6 +329,7 @@ fun showProhibitedToInviteIncognitoAlertDialog() {
fun PreviewAddGroupMembersLayout() {
SimpleXTheme {
AddGroupMembersLayout(
chatModelIncognito = false,
groupInfo = GroupInfo.sampleData,
creatingGroup = false,
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
@@ -5,7 +5,6 @@ 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.*
@@ -13,7 +12,6 @@ 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
@@ -23,7 +21,6 @@ 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.*
@@ -54,20 +51,15 @@ fun GroupMemberInfoView(
developerTools,
connectionCode,
getContactChat = { chatModel.getContactChat(it) },
knownDirectChat = {
withApi {
chatModel.chatItems.clear()
chatModel.chatItems.addAll(it.chatItems)
chatModel.chatId.value = it.chatInfo.id
closeAll()
}
},
newDirectChat = {
openDirectChat = {
withApi {
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
if (c != null) {
chatModel.addChat(c)
if (chatModel.getContactChat(it) == null) {
chatModel.addChat(c)
}
chatModel.chatItems.clear()
chatModel.chatItems.addAll(c.chatItems)
chatModel.chatId.value = c.id
closeAll()
}
@@ -150,8 +142,7 @@ fun GroupMemberInfoLayout(
developerTools: Boolean,
connectionCode: String?,
getContactChat: (Long) -> Chat?,
knownDirectChat: (Chat) -> Unit,
newDirectChat: (Long) -> Unit,
openDirectChat: (Long) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
@@ -176,13 +167,8 @@ fun GroupMemberInfoLayout(
if (contactId != null) {
SectionView {
val chat = getContactChat(contactId)
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
OpenChatButton(onClick = { knownDirectChat(chat) })
if (connectionCode != null) {
SectionDivider()
}
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { newDirectChat(contactId) })
if ((chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) || groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { openDirectChat(contactId) })
if (connectionCode != null) {
SectionDivider()
}
@@ -364,8 +350,7 @@ fun PreviewGroupMemberInfoLayout() {
developerTools = false,
connectionCode = "123",
getContactChat = { Chat.sampleData },
knownDirectChat = {},
newDirectChat = {},
openDirectChat = {},
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},
@@ -1,7 +1,6 @@
package chat.simplex.app.views.chat.group
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -14,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -23,6 +23,7 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.ProfileNameField
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.usersettings.*
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
@@ -53,14 +54,32 @@ fun GroupProfileLayout(
saveProfile: (GroupProfile) -> Unit,
) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val displayName = remember { mutableStateOf(groupProfile.displayName) }
val fullName = remember { mutableStateOf(groupProfile.fullName) }
val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) }
val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) }
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() }
val dataUnchanged =
displayName.value == groupProfile.displayName &&
fullName.value == groupProfile.fullName &&
chosenImage.value == null
val closeWithAlert = {
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) {
close()
} else {
showUnsavedChangesAlert({
saveProfile(
groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
)
)
}, close)
}
}
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
@@ -76,23 +95,16 @@ fun GroupProfileLayout(
sheetState = bottomSheetModalState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) {
ModalView(close = close) {
ModalView(close = closeWithAlert) {
Column(
Modifier
.verticalScroll(scrollState)
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
Text(
stringResource(R.string.group_profile_is_stored_on_members_devices),
Modifier.padding(bottom = 24.dp),
color = MaterialTheme.colors.onBackground,
lineHeight = 22.sp
)
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
Modifier.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING)
) {
ReadableText(R.string.group_profile_is_stored_on_members_devices, TextAlign.Center)
Box(
Modifier
.fillMaxWidth()
@@ -101,7 +113,7 @@ fun GroupProfileLayout(
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(192.dp, profileImage.value)
ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f))
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
@@ -109,51 +121,54 @@ fun GroupProfileLayout(
}
}
}
Text(
stringResource(R.string.group_display_name_field),
Modifier.padding(bottom = 3.dp)
)
ProfileNameField(displayName, focusRequester)
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.group_full_name_field),
Modifier.padding(bottom = 5.dp)
)
ProfileNameField(fullName)
Spacer(Modifier.height(16.dp))
Row {
TextButton(stringResource(R.string.cancel_verb)) {
close.invoke()
}
Spacer(Modifier.padding(horizontal = 8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.group_display_name_field),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text(
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable {
saveProfile(groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
))
},
color = MaterialTheme.colors.primary
)
} else {
Text(
stringResource(R.string.save_group_profile),
color = HighOrLowlight
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName)
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
Text(
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable {
saveProfile(
groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
)
)
},
color = MaterialTheme.colors.primary
)
} else {
Text(
stringResource(R.string.save_group_profile),
color = HighOrLowlight
)
}
}
Spacer(Modifier.height(DEFAULT_BOTTOM_BUTTON_PADDING))
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
@@ -164,6 +179,16 @@ fun GroupProfileLayout(
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -154,4 +154,4 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
// }
// }
//}
//}
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat.item
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
@@ -16,6 +17,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.dp
@@ -64,7 +66,7 @@ fun CIFileView(
fun fileSizeValid(): Boolean {
if (file != null) {
return file.fileSize <= MAX_FILE_SIZE
return file.fileSize <= getMaxFileSize(file.fileProtocol)
}
return false
}
@@ -72,22 +74,30 @@ fun CIFileView(
fun fileAction() {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation -> {
is CIFileStatus.RcvInvitation -> {
if (fileSizeValid()) {
receiveFile(file.fileId)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE))
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
}
CIFileStatus.RcvAccepted ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_file),
String.format(generalGetString(R.string.file_will_be_received_when_contact_is_online), MAX_FILE_SIZE)
)
CIFileStatus.RcvComplete -> {
is CIFileStatus.RcvAccepted ->
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_file),
generalGetString(R.string.file_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_file),
generalGetString(R.string.file_will_be_received_when_contact_is_online)
)
}
is CIFileStatus.RcvComplete -> {
val filePath = getLoadedFilePath(context, file)
if (filePath != null) {
saveFileLauncher.launch(file.fileName)
@@ -105,10 +115,24 @@ fun CIFileView(
CircularProgressIndicator(
Modifier.size(32.dp),
color = if (isInDarkTheme()) FileDark else FileLight,
strokeWidth = 4.dp
strokeWidth = 3.dp
)
}
@Composable
fun progressCircle(progress: Long, total: Long) {
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
val strokeColor = if (isInDarkTheme()) FileDark else FileLight
Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(Modifier.size(32.dp))
}
}
@Composable
fun fileIndicator() {
Box(
@@ -120,19 +144,34 @@ fun CIFileView(
) {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.SndStored -> fileIcon()
CIFileStatus.SndTransfer -> progressIndicator()
CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
CIFileStatus.RcvInvitation ->
is CIFileStatus.SndStored ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressIndicator()
FileProtocol.SMP -> fileIcon()
}
is CIFileStatus.SndTransfer ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator()
}
is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.SndError -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvInvitation ->
if (fileSizeValid())
fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary)
else
fileIcon(innerIcon = Icons.Outlined.PriorityHigh, color = WarningOrange)
CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz)
CIFileStatus.RcvTransfer -> progressIndicator()
CIFileStatus.RcvComplete -> fileIcon()
CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz)
is CIFileStatus.RcvTransfer ->
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
} else {
progressIndicator()
}
is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvError -> fileIcon(innerIcon = Icons.Outlined.Close)
}
} else {
fileIcon()
@@ -191,7 +230,7 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
ChatItem.getFileMsgContentSample(),
ChatItem.getFileMsgContentSample(fileName = "some_long_file_name_here", fileStatus = CIFileStatus.RcvInvitation),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvAccepted),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10)),
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvCancelled),
ChatItem.getFileMsgContentSample(fileSize = 1_000_000_000, fileStatus = CIFileStatus.RcvInvitation),
ChatItem.getFileMsgContentSample(text = "Hello there", fileStatus = CIFileStatus.RcvInvitation),
@@ -2,6 +2,7 @@ package chat.simplex.app.views.chat.item
import android.graphics.Bitmap
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
@@ -9,8 +10,7 @@ import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.MoreHoriz
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.*
@@ -27,8 +28,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.CIFile
import chat.simplex.app.model.CIFileStatus
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.*
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@@ -45,6 +45,25 @@ fun CIImageView(
showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit
) {
@Composable
fun progressIndicator() {
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
}
@Composable
fun fileIcon(icon: ImageVector, @StringRes stringId: Int) {
Icon(
icon,
stringResource(stringId),
Modifier.fillMaxSize(),
tint = Color.White
)
}
@Composable
fun loadingIndicator() {
if (file != null) {
@@ -55,39 +74,20 @@ fun CIImageView(
contentAlignment = Alignment.Center
) {
when (file.fileStatus) {
CIFileStatus.SndTransfer ->
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
CIFileStatus.SndComplete ->
Icon(
Icons.Filled.Check,
stringResource(R.string.icon_descr_image_snd_complete),
Modifier.fillMaxSize(),
tint = Color.White
)
CIFileStatus.RcvAccepted ->
Icon(
Icons.Outlined.MoreHoriz,
stringResource(R.string.icon_descr_waiting_for_image),
Modifier.fillMaxSize(),
tint = Color.White
)
CIFileStatus.RcvTransfer ->
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
CIFileStatus.RcvInvitation ->
Icon(
Icons.Outlined.ArrowDownward,
stringResource(R.string.icon_descr_asked_to_receive),
Modifier.fillMaxSize(),
tint = Color.White
)
is CIFileStatus.SndStored ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressIndicator()
FileProtocol.SMP -> {}
}
is CIFileStatus.SndTransfer -> progressIndicator()
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_image_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
else -> {}
}
}
@@ -136,7 +136,7 @@ fun CIImageView(
fun fileSizeValid(): Boolean {
if (file != null) {
return file.fileSize <= MAX_FILE_SIZE
return file.fileSize <= getMaxFileSize(file.fileProtocol)
}
return false
}
@@ -179,15 +179,23 @@ fun CIImageView(
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE))
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
CIFileStatus.RcvAccepted ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_is_online)
)
CIFileStatus.RcvTransfer -> {} // ?
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_image),
generalGetString(R.string.image_will_be_received_when_contact_is_online)
)
}
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO
else -> {}
@@ -0,0 +1,24 @@
package chat.simplex.app.views.chat.item
import androidx.compose.runtime.Composable
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
CIMsgError(ci, timedMessagesTTL, showMember) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.decryption_error),
text = when (msgDecryptError) {
MsgDecryptError.RatchetHeader -> String.format(generalGetString(R.string.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
MsgDecryptError.TooManySkipped -> String.format(generalGetString(R.string.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
}
)
}
}
@@ -0,0 +1,341 @@
package chat.simplex.app.views.chat.item
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.*
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.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toDrawable
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 com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
import com.google.android.exoplayer2.ui.StyledPlayerView
import java.io.File
@Composable
fun CIVideoView(
image: String,
duration: Int,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit
) {
Box(
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
contentAlignment = Alignment.TopEnd
) {
val context = LocalContext.current
val filePath = remember(file) { getLoadedFilePath(SimplexApp.context, file) }
val preview = remember(image) { base64ToBitmap(image) }
if (file != null && filePath != null) {
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
val view = LocalView.current
VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = {
hideKeyboard(view)
ModalManager.shared.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
}
})
} else {
Box {
ImageView(preview, showMenu, onClick = {
if (file != null) {
when (file.fileStatus) {
CIFileStatus.RcvInvitation ->
receiveFileIfValidSize(file, receiveFile)
CIFileStatus.RcvAccepted ->
when (file.fileProtocol) {
FileProtocol.XFTP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_video),
generalGetString(R.string.video_will_be_received_when_contact_completes_uploading)
)
FileProtocol.SMP ->
AlertManager.shared.showAlertMsg(
generalGetString(R.string.waiting_for_video),
generalGetString(R.string.video_will_be_received_when_contact_is_online)
)
}
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
CIFileStatus.RcvComplete -> {} // ?
CIFileStatus.RcvCancelled -> {} // TODO
else -> {}
}
}
})
if (file != null) {
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
}
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
}
}
}
loadingIndicator(file)
}
}
@Composable
private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true, context) }
val videoPlaying = remember(uri.path) { player.videoPlaying }
val progress = remember(uri.path) { player.progress }
val duration = remember(uri.path) { player.duration }
val preview by remember { player.preview }
// val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled }
val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo }
val play = {
player.enableSound(true)
player.play(true)
}
val stop = {
player.enableSound(false)
player.stop()
}
val showPreview = remember { derivedStateOf { !videoPlaying.value || progress.value == 0L } }
DisposableEffect(Unit) {
onDispose {
stop()
}
}
Box {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
AndroidView(
factory = { ctx ->
StyledPlayerView(ctx).apply {
useController = false
resizeMode = RESIZE_MODE_FIXED_WIDTH
this.player = player.player
}
},
Modifier
.width(width)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = { if (player.player.playWhenReady) stop() else onClick() }
)
)
if (showPreview.value) {
ImageView(preview, showMenu, onClick)
PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play)
}
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
}
}
@Composable
private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, onClick: () -> Unit) {
Surface(
Modifier.align(Alignment.Center),
color = Color.Black.copy(alpha = 0.25f),
shape = RoundedCornerShape(percent = 50)
) {
Box(
Modifier
.defaultMinSize(minWidth = 40.dp, minHeight = 40.dp)
.combinedClickable(onClick = onClick, onLongClick = onLongClick),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = null,
Modifier.size(25.dp),
tint = if (error) WarningOrange else Color.White
)
}
}
}
@Composable
private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, duration: MutableState<Long>, progress: MutableState<Long>/*, soundEnabled: MutableState<Boolean>*/) {
if (duration.value > 0L || progress.value > 0) {
Row {
Box(
Modifier
.padding(DEFAULT_PADDING_HALF)
.background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50))
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
val time = if (progress.value > 0) progress.value else duration.value
val timeStr = durationText((time / 1000).toInt())
val width = if (timeStr.length <= 5) 44 else 50
Text(
timeStr,
Modifier.widthIn(min = with(LocalDensity.current) { width.sp.toDp() }).padding(horizontal = 4.dp),
fontSize = 13.sp,
color = Color.White
)
/*if (!soundEnabled.value) {
Icon(Icons.Outlined.VolumeOff, null,
Modifier.padding(start = 5.dp).size(10.dp),
tint = Color.White
)
}*/
}
if (!playing.value) {
Box(
Modifier
.padding(top = DEFAULT_PADDING_HALF)
.background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50))
.padding(vertical = 2.dp, horizontal = 4.dp)
) {
Text(
formatBytes(file.fileSize),
Modifier.padding(horizontal = 4.dp),
fontSize = 13.sp,
color = Color.White
)
}
}
}
}
}
@Composable
private fun ImageView(preview: Bitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
val windowWidth = LocalWindowWidth()
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
Image(
preview.asImageBitmap(),
contentDescription = stringResource(R.string.video_descr),
modifier = Modifier
.width(width)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
),
contentScale = ContentScale.FillWidth,
)
}
@Composable
private fun LocalWindowWidth(): Dp {
val view = LocalView.current
val density = LocalDensity.current.density
return remember {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
(rect.width() / density).dp
}
}
@Composable
private fun progressIndicator() {
CircularProgressIndicator(
Modifier.size(16.dp),
color = Color.White,
strokeWidth = 2.dp
)
}
@Composable
private fun fileIcon(icon: ImageVector, @StringRes stringId: Int) {
Icon(
icon,
stringResource(stringId),
Modifier.fillMaxSize(),
tint = Color.White
)
}
@Composable
private fun progressCircle(progress: Long, total: Long) {
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
val strokeWidth = with(LocalDensity.current) { 2.dp.toPx() }
val strokeColor = Color.White
Surface(
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
color = Color.Transparent,
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
) {
Box(Modifier.size(16.dp))
}
}
@Composable
private fun loadingIndicator(file: CIFile?) {
if (file != null) {
Box(
Modifier
.padding(8.dp)
.size(20.dp),
contentAlignment = Alignment.Center
) {
when (file.fileStatus) {
is CIFileStatus.SndStored ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressIndicator()
FileProtocol.SMP -> {}
}
is CIFileStatus.SndTransfer ->
when (file.fileProtocol) {
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
FileProtocol.SMP -> progressIndicator()
}
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_video_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_video_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_video)
is CIFileStatus.RcvTransfer ->
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
} else {
progressIndicator()
}
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
else -> {}
}
}
}
}
private fun fileSizeValid(file: CIFile?): Boolean {
if (file != null) {
return file.fileSize <= getMaxFileSize(file.fileProtocol)
}
return false
}
private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
if (fileSizeValid(file)) {
receiveFile(file.fileId)
} else {
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
)
}
}
private fun videoViewFullWidth(windowWidth: Dp): Dp {
val approximatePadding = 100.dp
return minOf(1000.dp, windowWidth - approximatePadding)
}
@@ -210,9 +210,9 @@ private fun VoiceMsgIndicator(
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
}
} else {
if (file?.fileStatus == CIFileStatus.RcvInvitation
|| file?.fileStatus == CIFileStatus.RcvTransfer
|| file?.fileStatus == CIFileStatus.RcvAccepted
if (file?.fileStatus is CIFileStatus.RcvInvitation
|| file?.fileStatus is CIFileStatus.RcvTransfer
|| file?.fileStatus is CIFileStatus.RcvAccepted
) {
Box(
Modifier
@@ -228,7 +228,7 @@ private fun VoiceMsgIndicator(
}
}
private fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache {
fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache {
val brush = Brush.linearGradient(
0f to Color.Transparent,
0f to color,
@@ -22,8 +22,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.ui.theme.*
import chat.simplex.app.views.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
@@ -43,6 +42,7 @@ fun ChatItemView(
linkMode: SimplexLinkMode,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
@@ -104,11 +104,7 @@ fun ChatItemView(
@Composable
fun MsgContentItemDropdownMenu() {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
DefaultDropdownMenu(showMenu) {
if (cItem.meta.itemDeleted == null && !live) {
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
if (composeState.value.editing) {
@@ -131,7 +127,7 @@ fun ChatItemView(
copyText(context, cItem.content.text)
showMenu.value = false
})
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
val filePath = getLoadedFilePath(context, cItem.file)
if (filePath != null) {
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
@@ -144,8 +140,7 @@ fun ChatItemView(
writePermissionState.launchPermissionRequest()
}
}
is MsgContent.MCFile -> saveFileLauncher.launch(cItem.file?.fileName)
is MsgContent.MCVoice -> saveFileLauncher.launch(cItem.file?.fileName)
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
else -> {}
}
showMenu.value = false
@@ -168,6 +163,9 @@ fun ChatItemView(
}
)
}
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
}
if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
@@ -180,11 +178,7 @@ fun ChatItemView(
@Composable
fun MarkedDeletedItemDropdownMenu() {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
DefaultDropdownMenu(showMenu) {
if (!cItem.isDeletedContent) {
ItemAction(
stringResource(R.string.reveal_verb),
@@ -224,11 +218,7 @@ fun ChatItemView(
@Composable fun DeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@@ -244,7 +234,8 @@ 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, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, 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)
@@ -270,6 +261,24 @@ fun ChatItemView(
}
}
@Composable
fun CancelFileItemAction(
fileId: Long,
showMenu: MutableState<Boolean>,
cancelFile: (Long) -> Unit,
cancelAction: CancelAction
) {
ItemAction(
stringResource(cancelAction.uiActionId),
Icons.Outlined.Close,
onClick = {
showMenu.value = false
cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
},
color = Color.Red
)
}
@Composable
fun DeleteItemAction(
cItem: ChatItem,
@@ -307,22 +316,37 @@ fun ModerateItemAction(
}
@Composable
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) {
DropdownMenuItem(onClick) {
Row {
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
val finalColor = if (color == Color.Unspecified) {
if (isInDarkTheme()) MenuTextColorDark else Color.Black
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = color
color = finalColor
)
Icon(icon, text, tint = color)
Icon(icon, text, tint = finalColor)
}
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
AlertManager.shared.showAlertDialog(
title = generalGetString(cancelAction.alert.titleId),
text = generalGetString(cancelAction.alert.messageId),
confirmText = generalGetString(cancelAction.alert.confirmId),
destructive = true,
onConfirm = {
cancelFile(fileId)
}
)
}
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialogButtons(
title = generalGetString(R.string.delete_message__question),
@@ -332,7 +356,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.End,
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
@@ -383,6 +407,7 @@ fun PreviewChatItemView() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
@@ -403,6 +428,7 @@ fun PreviewChatItemViewDeletedContent() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
@@ -125,6 +125,18 @@ fun FramedItemView(
modifier = Modifier.size(68.dp).clipToBounds()
)
}
is MsgContent.MCVideo -> {
Box(Modifier.fillMaxWidth().weight(1f)) {
ciQuotedMsgView(qi)
}
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
Image(
imageBitmap,
contentDescription = stringResource(R.string.video_descr),
contentScale = ContentScale.Crop,
modifier = Modifier.size(68.dp).clipToBounds()
)
}
is MsgContent.MCFile, is MsgContent.MCVoice -> {
Box(Modifier.fillMaxWidth().weight(1f)) {
ciQuotedMsgView(qi)
@@ -151,7 +163,8 @@ fun FramedItemView(
}
}
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
Box(Modifier
.clip(RoundedCornerShape(18.dp))
@@ -198,6 +211,14 @@ fun FramedItemView(
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}
}
is MsgContent.MCVideo -> {
CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White
} else {
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}
}
is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
if (mc.text != "") {
@@ -3,21 +3,28 @@ package chat.simplex.app.views.chat.item
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.view.View
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
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.graphics.*
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import chat.simplex.app.R
import chat.simplex.app.views.chat.ProviderMedia
import chat.simplex.app.views.helpers.*
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@@ -26,13 +33,16 @@ import coil.decode.ImageDecoderDecoder
import coil.request.ImageRequest
import coil.size.Size
import com.google.accompanist.pager.*
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
interface ImageGalleryProvider {
val initialIndex: Int
val totalImagesSize: MutableState<Int>
fun getImage(index: Int): Pair<Bitmap, Uri>?
val totalMediaSize: MutableState<Int>
fun getMedia(index: Int): ProviderMedia?
fun currentPageChanged(index: Int)
fun scrollToStart()
fun onDismiss(index: Int)
@@ -48,13 +58,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
// Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank,
// which makes this blank page visible for a moment. Prevent it by doing the check ourselves
LaunchedEffect(Unit) {
if (provider.getImage(provider.initialIndex - 1) == null) {
if (provider.getMedia(provider.initialIndex - 1) == null) {
provider.scrollToStart()
pagerState.scrollToPage(0)
}
}
val scope = rememberCoroutineScope()
HorizontalPager(count = remember { provider.totalImagesSize }.value, state = pagerState) { index ->
val playersToRelease = rememberSaveable { mutableSetOf<Uri>() }
DisposableEffectOnGone(
whenGone = { playersToRelease.forEach { VideoPlayer.release(it, true, true) } }
)
HorizontalPager(count = remember { provider.totalMediaSize }.value, state = pagerState) { index ->
Column(
Modifier
.fillMaxSize()
@@ -74,13 +88,13 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
if (settledCurrentPage != provider.initialIndex)
provider.currentPageChanged(index)
}
val image = provider.getImage(index)
if (image == null) {
val media = provider.getMedia(index)
if (media == null) {
// No such image. Let's shrink total pages size or scroll to start of the list of pages to remove blank page automatically
SideEffect {
scope.launch {
when (settledCurrentPage) {
index - 1 -> provider.totalImagesSize.value = settledCurrentPage + 1
index - 1 -> provider.totalMediaSize.value = settledCurrentPage + 1
index + 1 -> {
provider.scrollToStart()
pagerState.scrollToPage(0)
@@ -89,7 +103,6 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
}
}
} else {
val (imageBitmap: Bitmap, uri: Uri) = image
var scale by remember { mutableStateOf(1f) }
var translationX by remember { mutableStateOf(0f) }
var translationY by remember { mutableStateOf(0f) }
@@ -100,54 +113,106 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
translationX = 0f
translationY = 0f
}
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
val modifier = Modifier
.onGloballyPositioned {
viewWidth = it.size.width
}
.build()
Image(
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader
),
contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Fit,
modifier = Modifier
.onGloballyPositioned {
viewWidth = it.size.width
}
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = translationX,
translationY = translationY,
)
.pointerInput(Unit) {
detectTransformGestures(
{ allowTranslate },
onGesture = { _, pan, gestureZoom, _ ->
scale = (scale * gestureZoom).coerceIn(1f, 20f)
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
if (scale > 1 && allowTranslate) {
translationX += pan.x * scale
translationY += pan.y * scale
} else if (allowTranslate) {
translationX = 0f
translationY = 0f
}
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = translationX,
translationY = translationY,
)
.pointerInput(Unit) {
detectTransformGestures(
{ allowTranslate },
onGesture = { _, pan, gestureZoom, _ ->
scale = (scale * gestureZoom).coerceIn(1f, 20f)
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
if (scale > 1 && allowTranslate) {
translationX += pan.x * scale
translationY += pan.y * scale
} else if (allowTranslate) {
translationX = 0f
translationY = 0f
}
)
}
)
}
.fillMaxSize()
if (media is ProviderMedia.Image) {
val (uri: Uri, imageBitmap: Bitmap) = media
// I'm making a new instance of imageLoader here because if I use one instance in multiple places
// after end of composition here a GIF from the first instance will be paused automatically which isn't what I want
val imageLoader = ImageLoader.Builder(LocalContext.current)
.components {
if (Build.VERSION.SDK_INT >= 28) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
}
.fillMaxSize(),
)
.build()
Image(
rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
imageLoader = imageLoader
),
contentDescription = stringResource(R.string.image_descr),
contentScale = ContentScale.Fit,
modifier = modifier,
)
} else if (media is ProviderMedia.Video) {
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
VideoView(modifier, media.uri, preview, index == settledCurrentPage)
DisposableEffect(Unit) {
onDispose { playersToRelease.add(media.uri) }
}
}
}
}
}
}
@Composable
private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) {
val context = LocalContext.current
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true, context) }
val isCurrentPage = rememberUpdatedState(currentPage)
val play = {
player.play(true)
}
val stop = {
player.stop()
}
LaunchedEffect(Unit) {
player.enableSound(true)
snapshotFlow { isCurrentPage.value }
.distinctUntilChanged()
.collect { if (it) play() else stop() }
}
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
AndroidView(
factory = { ctx ->
StyledPlayerView(ctx).apply {
resizeMode = if (ctx.resources.configuration.screenWidthDp > ctx.resources.configuration.screenHeightDp) {
AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
} else {
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
}
setShowPreviousButton(false)
setShowNextButton(false)
setShowSubtitleButton(false)
setShowVrButton(false)
controllerAutoShow = false
findViewById<View>(com.google.android.exoplayer2.R.id.exo_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb())
findViewById<View>(com.google.android.exoplayer2.R.id.exo_settings).isVisible = false
this.player = player.player
}
},
modifier
)
}
}
@@ -17,19 +17,41 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatItem
import chat.simplex.app.model.MsgErrorType
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
CIMsgError(ci, timedMessagesTTL, showMember) {
when (msgError) {
is MsgErrorType.MsgSkipped ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
)
is MsgErrorType.MsgBadHash ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_msg_bad_hash),
text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_msg_bad_id),
text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
}
}
}
@Composable
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) {
Surface(
Modifier.clickable(onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
)
}),
Modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight,
) {
@@ -59,6 +81,7 @@ fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boo
fun IntegrityErrorItemViewPreview() {
SimpleXTheme {
IntegrityErrorItemView(
MsgErrorType.MsgBadHash(),
ChatItem.getDeletedContentSampleData(),
null
)
@@ -30,7 +30,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(Modifier.fillMaxWidth().weight(1f)) {
Box(Modifier.weight(1f, false)) {
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
MarkedDeletedText(String.format(generalGetString(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
} else {
@@ -194,26 +194,14 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
@Composable
fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
DropdownMenuItem({
markChatUnread(chat, chatModel)
showMenu.value = false
}) {
Row {
Text(
stringResource(R.string.mark_unread),
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = MaterialTheme.colors.onBackground
)
Icon(
Icons.Outlined.MarkChatUnread,
stringResource(R.string.mark_unread),
tint = MaterialTheme.colors.onBackground
)
ItemAction(
stringResource(R.string.mark_unread),
Icons.Outlined.MarkChatUnread,
onClick = {
markChatUnread(chat, chatModel)
showMenu.value = false
}
}
)
}
@Composable
@@ -447,7 +435,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.End,
horizontalArrangement = Arrangement.Center,
) {
TextButton(onClick = {
AlertManager.shared.hideAlert()
@@ -589,15 +577,7 @@ fun ChatListNavLinkLayout(
chatLinkPreview()
}
if (dropdownMenuItems != null) {
Box(Modifier.padding(horizontal = 16.dp)) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
dropdownMenuItems()
}
}
DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems)
}
}
Divider(Modifier.padding(horizontal = 8.dp))
@@ -21,6 +21,7 @@ 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.*
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -36,7 +37,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@Composable
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit, stopped: Boolean) {
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val showNewChatSheet = {
@@ -208,7 +209,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
} 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 users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
@@ -247,7 +248,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
}
@Composable
private fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
IconButton(onClick = onButtonClicked) {
Box {
ProfileImage(
@@ -86,7 +86,7 @@ fun ChatPreviewView(
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.MediaPreview -> Icons.Outlined.Image to null
is ComposePreview.VoicePreview -> Icons.Filled.PlayArrow to durationText(draft.preview.durationMs / 1000)
else -> null
}
@@ -24,12 +24,15 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.Indigo
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow
@Composable
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
var searchInList by rememberSaveable { mutableStateOf("") }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val switchingUsers = rememberSaveable { mutableStateOf(false) }
Scaffold(
topBar = { Column { ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } },
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
) {
Box(Modifier.padding(it)) {
Column(
@@ -45,23 +48,41 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
}
}
}
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
chatModel.sharedContent.value = null
})
}
@Composable
private fun EmptyList() {
Box {
Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight)
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(stringResource(R.string.you_have_no_chats), color = HighOrLowlight)
}
}
@Composable
private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
var showSearch by rememberSaveable { mutableStateOf(false) }
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
if (showSearch) {
BackHandler(onBack = hideSearchOnBack)
}
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
val navButton: @Composable RowScope.() -> Unit = {
when {
showSearch -> NavigationButtonBack(hideSearchOnBack)
users.size > 1 -> {
val allRead = users
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
userPickerState.value = AnimatedViewState.VISIBLE
}
}
else -> NavigationButtonBack { chatModel.sharedContent.value = null }
}
}
if (chatModel.chats.size >= 8) {
barButtons.add {
IconButton({ showSearch = true }) {
@@ -87,13 +108,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
}
DefaultTopAppBar(
navigationButton = { if (showSearch) NavigationButtonBack(hideSearchOnBack) else NavigationButtonBack { chatModel.sharedContent.value = null } },
navigationButton = navButton,
title = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
when (chatModel.sharedContent.value) {
is SharedContent.Text -> stringResource(R.string.share_message)
is SharedContent.Images -> stringResource(R.string.share_image)
is SharedContent.Media -> stringResource(R.string.share_image)
is SharedContent.File -> stringResource(R.string.share_file)
else -> stringResource(R.string.share_message)
},
@@ -1,19 +1,20 @@
package chat.simplex.app.views.chatlist
import SectionItemView
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.foundation.shape.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
@@ -34,7 +35,15 @@ import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, openSettings: () -> Unit) {
fun UserPicker(
chatModel: ChatModel,
userPickerState: MutableStateFlow<AnimatedViewState>,
switchingUsers: MutableState<Boolean>,
showSettings: Boolean = true,
showCancel: Boolean = false,
cancelClicked: () -> Unit = {},
settingsClicked: () -> Unit = {},
) {
val scope = rememberCoroutineScope()
var newChat by remember { mutableStateOf(userPickerState.value) }
val users by remember {
@@ -97,16 +106,17 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
) {
Column(
Modifier
.widthIn(min = 220.dp)
.widthIn(min = 260.dp)
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
.background(if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp)))
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
) {
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
openSettings()
UserProfilePickerItem(u.user, u.unreadCount, PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING * 2), openSettings = {
settingsClicked()
userPickerState.value = AnimatedViewState.GONE
}) {
userPickerState.value = AnimatedViewState.HIDING
@@ -126,16 +136,24 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
}
}
SettingsPickerItem {
openSettings()
userPickerState.value = AnimatedViewState.GONE
if (showSettings) {
SettingsPickerItem {
settingsClicked()
userPickerState.value = AnimatedViewState.GONE
}
}
if (showCancel) {
CancelPickerItem {
cancelClicked()
userPickerState.value = AnimatedViewState.GONE
}
}
}
}
}
@Composable
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues = PaddingValues(start = 8.dp, end = DEFAULT_PADDING), onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
@@ -146,7 +164,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit
interactionSource = remember { MutableInteractionSource() },
indication = if (!u.activeUser) LocalIndication.current else null
)
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
.padding(padding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -195,6 +213,7 @@ fun UserProfileRow(u: User) {
u.displayName,
modifier = Modifier
.padding(start = 8.dp, end = 8.dp),
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
@@ -202,12 +221,26 @@ fun UserProfileRow(u: User) {
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING * 2.2f, end = DEFAULT_PADDING * 2), minHeight = 68.dp) {
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING * 1.5f))
Text(
text,
color = MaterialTheme.colors.onBackground,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
)
}
}
@Composable
private fun CancelPickerItem(onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick, padding = PaddingValues(start = DEFAULT_PADDING * 2.2f, end = DEFAULT_PADDING * 2), minHeight = 68.dp) {
val text = generalGetString(R.string.cancel_verb)
Icon(Icons.Outlined.Close, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING * 1.5f))
Text(
text,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
)
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
}
}
@@ -42,9 +42,9 @@ fun DatabaseEncryptionView(m: ChatModel) {
val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") }
val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") }
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
val newKey = rememberSaveable { mutableStateOf("") }
val confirmNewKey = rememberSaveable { mutableStateOf("") }
@@ -89,7 +89,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false
if (useKeychain.value) {
DatabaseUtils.setDatabaseKey(newKey.value)
DatabaseUtils.ksDatabasePassword.set(newKey.value)
}
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) {
@@ -150,7 +150,7 @@ fun DatabaseEncryptionLayout(
text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(R.string.remove_passphrase),
onConfirm = {
DatabaseUtils.removeDatabaseKey()
DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, prefs)
storedKey.value = false
},
@@ -366,7 +366,7 @@ fun PassphraseField(
showStrength: Boolean = false,
isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(),
dependsOn: MutableState<String>? = null,
dependsOn: State<Any?>? = null,
) {
var valid by remember { mutableStateOf(validKey(key.value)) }
var showKey by remember { mutableStateOf(false) }
@@ -479,7 +479,7 @@ private fun passphraseEntropy(s: String): Double {
return s.length * log2(poolSize.toDouble())
}
private enum class PassphraseStrength(val color: Color) {
enum class PassphraseStrength(val color: Color) {
VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen);
companion object {
@@ -522,4 +522,4 @@ fun PreviewDatabaseEncryptionLayout() {
onConfirmEncrypt = {},
)
}
}
}
@@ -4,6 +4,7 @@ import SectionSpacer
import SectionView
import android.content.Context
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
@@ -13,6 +14,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.*
@@ -20,6 +22,7 @@ import chat.simplex.app.R
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.AppVersionText
import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.*
import kotlinx.datetime.Clock
@@ -35,28 +38,43 @@ fun DatabaseErrorView(
) {
val progressIndicator = remember { mutableStateOf(false) }
val dbKey = remember { mutableStateOf("") }
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) }
var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) }
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
val context = LocalContext.current
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
val saveAndRunChatOnClick: () -> Unit = {
DatabaseUtils.setDatabaseKey(dbKey.value)
fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
val useKey = if (useKeychain) null else dbKey.value
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences)
}
fun saveAndRunChatOnClick() {
DatabaseUtils.ksDatabasePassword.set(dbKey.value)
storedDBKey = dbKey.value
appPreferences.storeDBPassphrase.set(true)
useKeychain = true
appPreferences.initialRandomDBPassphrase.set(false)
runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
callRunChat()
}
val title = when (chatDbStatus.value) {
is DBMigrationResult.OK -> ""
is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty())
generalGetString(R.string.wrong_passphrase)
else
generalGetString(R.string.encrypted_database)
is DBMigrationResult.Error -> generalGetString(R.string.database_error)
is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error)
is DBMigrationResult.Unknown -> generalGetString(R.string.database_error)
null -> "" // should never be here
@Composable
fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) {
Text(
generalGetString(title),
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp),
style = MaterialTheme.typography.h1
)
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
}
@Composable
fun FileNameText(dbFile: String) {
Text(String.format(generalGetString(R.string.file_with_path), dbFile.split("/").lastOrNull() ?: dbFile))
}
@Composable
fun MigrationsText(ms: List<String>) {
Text(String.format(generalGetString(R.string.database_migrations), ms.joinToString(", ")))
}
Column(
@@ -64,63 +82,91 @@ fun DatabaseErrorView(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
) {
Text(
title,
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
style = MaterialTheme.typography.h1
)
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
when (val status = chatDbStatus.value) {
is DBMigrationResult.ErrorNotADatabase -> {
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
when (val status = chatDbStatus.value) {
is DBMigrationResult.ErrorNotADatabase ->
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
DatabaseErrorDetails(R.string.wrong_passphrase) {
Text(generalGetString(R.string.passphrase_is_different))
DatabaseKeyField(dbKey, buttonEnabled) {
saveAndRunChatOnClick()
}
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
SectionSpacer()
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
} else {
FileNameText(status.dbFile)
}
} else {
DatabaseErrorDetails(R.string.encrypted_database) {
Text(generalGetString(R.string.database_passphrase_is_required))
DatabaseKeyField(dbKey, buttonEnabled) {
if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
}
if (useKeychain) {
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
DatabaseKeyField(dbKey, buttonEnabled, ::saveAndRunChatOnClick)
SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
} else {
OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) }
DatabaseKeyField(dbKey, buttonEnabled) { callRunChat() }
OpenChatButton(buttonEnabled) { callRunChat() }
}
}
}
is DBMigrationResult.Error -> {
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
Text(String.format(generalGetString(R.string.error_with_info), status.migrationError))
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
is MigrationError.Upgrade ->
DatabaseErrorDetails(R.string.database_upgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(R.string.upgrade_and_open_chat))
}
Spacer(Modifier.height(20.dp))
FileNameText(status.dbFile)
MigrationsText(err.upMigrations.map { it.upName })
AppVersionText()
}
is MigrationError.Downgrade ->
DatabaseErrorDetails(R.string.database_downgrade) {
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
Text(generalGetString(R.string.downgrade_and_open_chat))
}
Spacer(Modifier.height(20.dp))
Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold)
FileNameText(status.dbFile)
MigrationsText(err.downMigrations)
AppVersionText()
}
is MigrationError.Error ->
DatabaseErrorDetails(R.string.incompatible_database_version) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(R.string.error_with_info), mtrErrorDescription(err.mtrError)))
}
}
is DBMigrationResult.ErrorSQL ->
DatabaseErrorDetails(R.string.database_error) {
FileNameText(status.dbFile)
Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError))
}
is DBMigrationResult.ErrorKeychain -> {
is DBMigrationResult.ErrorKeychain ->
DatabaseErrorDetails(R.string.keychain_error) {
Text(generalGetString(R.string.cannot_access_keychain))
}
is DBMigrationResult.Unknown -> {
is DBMigrationResult.InvalidConfirmation ->
DatabaseErrorDetails(R.string.invalid_migration_confirmation) {
// this can only happen if incorrect parameter is passed
}
is DBMigrationResult.Unknown ->
DatabaseErrorDetails(R.string.database_error) {
Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json))
}
is DBMigrationResult.OK -> {
}
null -> {
}
}
if (restoreDbFromBackup.value) {
SectionSpacer()
Text(generalGetString(R.string.database_backup_can_be_restored))
Spacer(Modifier.size(16.dp))
RestoreDbButton {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.restore_database_alert_title),
text = generalGetString(R.string.restore_database_alert_desc),
confirmText = generalGetString(R.string.restore_database_alert_confirm),
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
destructive = true,
)
}
is DBMigrationResult.OK -> {}
null -> {}
}
if (restoreDbFromBackup.value) {
SectionSpacer()
Text(generalGetString(R.string.database_backup_can_be_restored))
Spacer(Modifier.size(16.dp))
RestoreDbButton {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.restore_database_alert_title),
text = generalGetString(R.string.restore_database_alert_desc),
confirmText = generalGetString(R.string.restore_database_alert_confirm),
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
destructive = true,
)
}
}
}
@@ -141,7 +187,8 @@ fun DatabaseErrorView(
}
private fun runChat(
dbKey: String,
dbKey: String? = null,
confirmMigrations: MigrationConfirmation? = null,
chatDbStatus: State<DBMigrationResult?>,
progressIndicator: MutableState<Boolean>,
prefs: AppPreferences
@@ -150,7 +197,7 @@ private fun runChat(
if (progressIndicator.value) return@launch
progressIndicator.value = true
try {
SimplexApp.context.initChatController(dbKey)
SimplexApp.context.initChatController(dbKey, confirmMigrations)
} catch (e: Exception) {
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
}
@@ -163,18 +210,17 @@ private fun runChat(
NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp()
}
}
is DBMigrationResult.ErrorNotADatabase -> {
is DBMigrationResult.ErrorNotADatabase ->
AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase))
}
is DBMigrationResult.Error -> {
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationError)
}
is DBMigrationResult.ErrorKeychain -> {
is DBMigrationResult.ErrorSQL ->
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError)
is DBMigrationResult.ErrorKeychain ->
AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error))
}
is DBMigrationResult.Unknown -> {
is DBMigrationResult.Unknown ->
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json)
}
is DBMigrationResult.InvalidConfirmation ->
AlertManager.shared.showAlertMsg(generalGetString(R.string.invalid_migration_confirmation))
is DBMigrationResult.ErrorMigration -> {}
null -> {}
}
}
@@ -204,6 +250,14 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref
}
}
private fun mtrErrorDescription(err: MTRError): String =
when (err) {
is MTRError.NoDown ->
String.format(generalGetString(R.string.mtr_error_no_down_migration), err.dbMigrations.joinToString(", "))
is MTRError.Different ->
String.format(generalGetString(R.string.mtr_error_different), err.appMigration, err.dbMigration)
}
@Composable
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
PassphraseField(
@@ -52,7 +52,7 @@ fun DatabaseView(
) {
val context = LocalContext.current
val progressIndicator = remember { mutableStateOf(false) }
val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) }
val runChat = remember { m.chatRunning }
val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) }
@@ -76,7 +76,7 @@ fun DatabaseView(
) {
DatabaseLayout(
progressIndicator.value,
runChat.value,
runChat.value != false,
m.chatDbChanged.value,
useKeychain.value,
m.chatDbEncrypted.value,
@@ -160,7 +160,7 @@ fun DatabaseLayout(
AppBarTitle(stringResource(R.string.your_chat_database))
SectionView(stringResource(R.string.messages_section_title).uppercase()) {
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected) }
}
SectionTextFooter(
remember(currentUser?.displayName) {
@@ -388,7 +388,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive)
}
private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
withApi {
try {
if (chatDbChanged.value) {
@@ -417,7 +417,7 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStar
}
}
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.stop_chat_question),
text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database),
@@ -434,7 +434,7 @@ private fun exportProhibitedAlert() {
)
}
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
if (m.controller.appPrefs.performLA.get()) {
authenticate(
generalGetString(R.string.auth_stop_chat),
@@ -442,12 +442,13 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
context as FragmentActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success, LAResult.Unavailable -> {
LAResult.Success, is LAResult.Unavailable -> {
stopChat(m, runChat, context)
}
is LAResult.Error -> {
runChat.value = true
}
LAResult.Failed -> {
is LAResult.Failed -> {
runChat.value = true
}
}
@@ -458,7 +459,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
}
}
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
withApi {
try {
m.controller.apiStopChat()
@@ -592,7 +593,7 @@ private fun importArchive(
try {
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
m.controller.apiImportArchive(config)
DatabaseUtils.removeDatabaseKey()
DatabaseUtils.ksDatabasePassword.remove()
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
@@ -647,7 +648,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
try {
m.controller.apiDeleteStorage()
m.chatDbDeleted.value = true
DatabaseUtils.removeDatabaseKey()
DatabaseUtils.ksDatabasePassword.remove()
m.controller.appPrefs.storeDBPassphrase.set(true)
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile))
@@ -3,13 +3,15 @@ package chat.simplex.app.views.helpers
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Dialog
import chat.simplex.app.R
@@ -33,13 +35,14 @@ class AlertManager {
text: String? = null,
buttons: @Composable () -> Unit,
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
onDismissRequest = this::hideAlert,
title = { Text(title) },
text = alertText,
buttons = buttons
title = alertTitle(title),
text = alertText(text),
buttons = buttons,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
}
@@ -51,22 +54,21 @@ class AlertManager {
) {
showAlert {
Dialog(onDismissRequest = this::hideAlert) {
Column(Modifier.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)) {
Text(title,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = if (text == null) DEFAULT_PADDING else DEFAULT_PADDING_HALF),
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
Column(
Modifier
.background(if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp)))
.padding(bottom = DEFAULT_PADDING)
) {
Text(
title,
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
if (text != null) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
Text(
text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
fontSize = 14.sp,
)
}
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = HighOrLowlight)
}
buttons()
}
}
@@ -84,24 +86,28 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = { Text(title) },
text = alertText,
confirmButton = {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
title = alertTitle(title),
text = alertText(text),
buttons = {
Row (
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
},
dismissButton = {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
}
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
}
@@ -116,16 +122,15 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = { Text(title) },
text = alertText,
title = alertTitle(title),
text = alertText(text),
buttons = {
Column(
Modifier.fillMaxWidth().padding(horizontal = 8.dp).padding(top = 16.dp, bottom = 2.dp),
horizontalAlignment = Alignment.End
horizontalAlignment = Alignment.CenterHorizontally
) {
TextButton(onClick = {
onDismiss?.invoke()
@@ -134,9 +139,11 @@ class AlertManager {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
}
},
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
}
@@ -145,18 +152,24 @@ class AlertManager {
title: String, text: String? = null,
confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = this::hideAlert,
title = { Text(title) },
text = alertText,
confirmButton = {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText) }
}
title = alertTitle(title),
text = alertText(text),
buttons = {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.Center
) {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = Color.Unspecified) }
}
},
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
}
@@ -177,3 +190,30 @@ class AlertManager {
val shared = AlertManager()
}
}
private fun alertTitle(title: String): (@Composable () -> Unit)? {
return {
Text(
title,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
}
}
private fun alertText(text: String?): (@Composable () -> Unit)? {
return if (text == null) {
null
} else {
({
Text(
text,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 16.sp,
color = HighOrLowlight
)
})
}
}
@@ -1,21 +1,26 @@
package chat.simplex.app.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.views.newchat.ActionButton
sealed class AttachmentOption {
object TakePhoto: AttachmentOption()
object PickImage: AttachmentOption()
object PickFile: AttachmentOption()
object CameraPhoto: AttachmentOption()
object GalleryImage: AttachmentOption()
object GalleryVideo: AttachmentOption()
object File: AttachmentOption()
}
@Composable
@@ -37,16 +42,20 @@ fun ChooseAttachmentView(
.padding(horizontal = 8.dp, vertical = 30.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
ActionButton(null, stringResource(R.string.use_camera_button), icon = Icons.Outlined.PhotoCamera) {
attachmentOption.value = AttachmentOption.TakePhoto
ActionButton(Modifier.fillMaxWidth(0.25f), null, stringResource(R.string.use_camera_button), icon = painterResource(R.drawable.ic_camera_enhance)) {
attachmentOption.value = AttachmentOption.CameraPhoto
hide()
}
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
attachmentOption.value = AttachmentOption.PickImage
ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(R.string.gallery_image_button), icon = painterResource(R.drawable.ic_add_photo)) {
attachmentOption.value = AttachmentOption.GalleryImage
hide()
}
ActionButton(null, stringResource(R.string.choose_file), icon = Icons.Outlined.InsertDriveFile) {
attachmentOption.value = AttachmentOption.PickFile
ActionButton(Modifier.fillMaxWidth(0.50f), null, stringResource(R.string.gallery_video_button), icon = painterResource(R.drawable.ic_smart_display)) {
attachmentOption.value = AttachmentOption.GalleryVideo
hide()
}
ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(R.string.choose_file), icon = painterResource(R.drawable.ic_note_add)) {
attachmentOption.value = AttachmentOption.File
hide()
}
}
@@ -6,13 +6,15 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.ui.theme.*
@Composable
fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) {
fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> Unit = {}) {
Column(
Modifier
.fillMaxWidth()
@@ -28,7 +30,7 @@ fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
NavigationButtonBack(close)
NavigationButtonBack(onButtonClicked = close)
Row {
endButtons()
}
@@ -54,6 +56,19 @@ fun AppBarTitle(title: String, withPadding: Boolean = true) {
)
}
@Composable
fun ColumnScope.AppBarTitleCentered(title: String, withPadding: Boolean = true) {
Text(
title,
Modifier
.padding(bottom = if (withPadding) DEFAULT_PADDING * 1.5f else 0.dp)
.align(Alignment.CenterHorizontally),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1,
color = MaterialTheme.colors.primary
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -3,6 +3,7 @@ package chat.simplex.app.views.helpers
import android.util.Log
import chat.simplex.app.*
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.model.SharedPreference
import chat.simplex.app.views.usersettings.Cryptor
import kotlinx.serialization.*
import java.io.File
@@ -16,30 +17,36 @@ object DatabaseUtils {
}
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
private const val APP_PASSWORD_ALIAS: String = "appPassword"
val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase)
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
fun get(): String? {
return cryptor.decryptData(
passphrase.get()?.toByteArrayFromBase64() ?: return null,
initVector.get()?.toByteArrayFromBase64() ?: return null,
alias,
)
}
fun set(key: String) {
val data = cryptor.encryptText(key, alias)
passphrase.set(data.first.toBase64String())
initVector.set(data.second.toBase64String())
}
fun remove() {
cryptor.deleteKey(alias)
passphrase.set(null)
initVector.set(null)
}
}
private fun hasDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists()
fun getDatabaseKey(): String? {
return cryptor.decryptData(
appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
DATABASE_PASSWORD_ALIAS,
)
}
fun setDatabaseKey(key: String) {
val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS)
appPreferences.encryptedDBPassphrase.set(data.first.toBase64String())
appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String())
}
fun removeDatabaseKey() {
cryptor.deleteKey(DATABASE_PASSWORD_ALIAS)
appPreferences.encryptedDBPassphrase.set(null)
appPreferences.initializationVectorDBPassphrase.set(null)
}
fun useDatabaseKey(): String {
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
var dbKey = ""
@@ -47,10 +54,10 @@ object DatabaseUtils {
if (useKeychain) {
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
dbKey = randomDatabasePassword()
setDatabaseKey(dbKey)
ksDatabasePassword.set(dbKey)
appPreferences.initialRandomDBPassphrase.set(true)
} else {
dbKey = getDatabaseKey() ?: ""
dbKey = ksDatabasePassword.get() ?: ""
}
}
return dbKey
@@ -66,8 +73,39 @@ object DatabaseUtils {
@Serializable
sealed class DBMigrationResult {
@Serializable @SerialName("ok") object OK: DBMigrationResult()
@Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult()
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
@Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult()
@Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult()
@Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult()
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
}
}
enum class MigrationConfirmation(val value: String) {
YesUp("yesUp"),
YesUpDown ("yesUpDown"),
Error("error")
}
fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation =
if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
@Serializable
sealed class MigrationError {
@Serializable @SerialName("upgrade") class Upgrade(val upMigrations: List<UpMigration>): MigrationError()
@Serializable @SerialName("downgrade") class Downgrade(val downMigrations: List<String>): MigrationError()
@Serializable @SerialName("migrationError") class Error(val mtrError: MTRError): MigrationError()
}
@Serializable
data class UpMigration(
val upName: String,
// val withDown: Boolean
)
@Serializable
sealed class MTRError {
@Serializable @SerialName("noDown") class NoDown(val dbMigrations: List<String>): MTRError()
@Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError()
}
@@ -3,10 +3,15 @@ package chat.simplex.app.views.helpers
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.*
import androidx.compose.material.*
import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Error
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -14,13 +19,18 @@ import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.database.PassphraseStrength
import chat.simplex.app.views.database.validKey
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@@ -110,3 +120,109 @@ fun DefaultBasicTextField(
}
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DefaultConfigurableTextField(
state: MutableState<TextFieldValue>,
placeholder: String,
modifier: Modifier = Modifier,
showPasswordStrength: Boolean = false,
isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(),
keyboardType: KeyboardType = KeyboardType.Text,
dependsOn: State<Any?>? = null,
) {
var valid by remember { mutableStateOf(validKey(state.value.text)) }
var showKey by remember { mutableStateOf(false) }
val icon = if (valid) {
if (showKey) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
} else Icons.Outlined.Error
val iconColor = if (valid) {
if (showPasswordStrength && state.value.text.isNotEmpty()) PassphraseStrength.check(state.value.text).color else HighOrLowlight
} else Color.Red
val keyboard = LocalSoftwareKeyboardController.current
val keyboardOptions = KeyboardOptions(
imeAction = if (keyboardActions.onNext != null) ImeAction.Next else ImeAction.Done,
autoCorrect = keyboardType != KeyboardType.Password,
keyboardType = keyboardType
)
val enabled = true
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
textColor = MaterialTheme.colors.onBackground,
focusedIndicatorColor = Color.Unspecified,
unfocusedIndicatorColor = Color.Unspecified,
)
val color = MaterialTheme.colors.onBackground
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
val interactionSource = remember { MutableInteractionSource() }
BasicTextField(
value = state.value,
modifier = modifier
.fillMaxWidth()
.background(colors.backgroundColor(enabled).value, shape)
.indicatorLine(enabled, false, interactionSource, colors)
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = TextFieldDefaults.MinHeight
),
onValueChange = {
state.value = it
},
cursorBrush = SolidColor(colors.cursorColor(false).value),
visualTransformation = if (showKey || keyboardType != KeyboardType.Password)
VisualTransformation.None
else
VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) },
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(onDone = {
keyboard?.hide()
keyboardActions.onDone?.invoke(this)
}),
singleLine = true,
textStyle = TextStyle.Default.copy(
color = color,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
interactionSource = interactionSource,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = state.value.text,
innerTextField = innerTextField,
placeholder = { Text(placeholder, color = HighOrLowlight) },
singleLine = true,
enabled = enabled,
isError = !valid,
trailingIcon = {
if (keyboardType == KeyboardType.Password || !valid) {
IconButton({ showKey = !showKey }) {
Icon(icon, null, tint = iconColor)
}
}
},
interactionSource = interactionSource,
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
visualTransformation = VisualTransformation.None,
colors = colors
)
}
)
LaunchedEffect(Unit) {
launch {
snapshotFlow { state.value }
.distinctUntilChanged()
.collect {
valid = isValid(it.text)
}
}
launch {
snapshotFlow { dependsOn?.value }
.distinctUntilChanged()
.collect {
valid = isValid(state.value.text)
}
}
}
}
@@ -0,0 +1,58 @@
package chat.simplex.app.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.*
@Composable
fun DefaultDropdownMenu(
showMenu: MutableState<Boolean>,
offset: DpOffset = DpOffset(0.dp, 0.dp),
dropdownMenuItems: (@Composable () -> Unit)?
) {
MaterialTheme(
colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF080808) else MaterialTheme.colors.background),
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier
.widthIn(min = 250.dp)
.padding(vertical = 4.dp),
offset = offset,
) {
dropdownMenuItems?.invoke()
}
}
}
@Composable
fun ExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu(
expanded: MutableState<Boolean>,
modifier: Modifier = Modifier,
dropdownMenuItems: (@Composable () -> Unit)?
) {
MaterialTheme(
colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF080808) else MaterialTheme.colors.background),
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
) {
ExposedDropdownMenu(
modifier = Modifier.widthIn(min = 200.dp).then(modifier),
expanded = expanded.value,
onDismissRequest = {
expanded.value = false
}
) {
dropdownMenuItems?.invoke()
}
}
}
@@ -45,10 +45,10 @@ fun DefaultTopAppBar(
}
@Composable
fun NavigationButtonBack(onButtonClicked: () -> Unit) {
IconButton(onButtonClicked) {
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) {
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
Icon(
Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = MaterialTheme.colors.primary
Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else HighOrLowlight
)
}
}
@@ -11,7 +11,7 @@ import kotlinx.serialization.encoding.Encoder
sealed class SharedContent {
data class Text(val text: String): SharedContent()
data class Images(val text: String, val uris: List<Uri>): SharedContent()
data class Media(val text: String, val uris: List<Uri>): SharedContent()
data class File(val text: String, val uri: Uri): SharedContent()
}
@@ -48,4 +48,5 @@ object UriSerializer : KSerializer<Uri> {
sealed class UploadContent {
@Serializable data class SimpleImage(val uri: Uri): UploadContent()
@Serializable data class AnimatedImage(val uri: Uri): UploadContent()
@Serializable data class Video(val uri: Uri, val duration: Int): UploadContent()
}
@@ -15,8 +15,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.*
@Composable
fun <T> ExposedDropDownSettingRow(
@@ -33,7 +32,7 @@ fun <T> ExposedDropDownSettingRow(
Modifier.fillMaxWidth().padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var expanded by remember { mutableStateOf(false) }
val expanded = remember { mutableStateOf(false) }
if (icon != null) {
Icon(
@@ -46,9 +45,9 @@ fun <T> ExposedDropDownSettingRow(
Text(title, Modifier.weight(1f), color = if (enabled.value) Color.Unspecified else HighOrLowlight)
ExposedDropdownMenuBox(
expanded = expanded,
expanded = expanded.value,
onExpandedChange = {
expanded = !expanded && enabled.value
expanded.value = !expanded.value && enabled.value
}
) {
Row(
@@ -66,29 +65,28 @@ fun <T> ExposedDropDownSettingRow(
)
Spacer(Modifier.size(12.dp))
Icon(
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.icon_descr_more_button),
tint = HighOrLowlight
)
}
ExposedDropdownMenu(
DefaultExposedDropdownMenu(
modifier = Modifier.widthIn(min = 200.dp),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
onSelected(selectionOption.first)
expanded = false
}
expanded.value = false
},
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
) {
Text(
selectionOption.second + (if (label != null) " $label" else ""),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
)
}
}
@@ -18,8 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.PhotoCamera
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
@@ -174,7 +173,18 @@ fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLaunche
@Composable
fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivityResultLauncher<String, List<Uri>> =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetMultipleContents(), cb)
rememberLauncherForActivityResult(contract = GetMultipleContentsAndMimeTypes(), cb)
class GetMultipleContentsAndMimeTypes: ActivityResultContracts.GetMultipleContents() {
override fun createIntent(context: Context, input: String): Intent {
val mimeTypes = input.split(";")
return super.createIntent(context, mimeTypes[0]).apply {
if (mimeTypes.isNotEmpty()) {
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
}
}
}
fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
try {
@@ -248,7 +258,7 @@ fun GetImageBottomSheet(
}
}
}
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Image) {
try {
galleryLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
@@ -1,36 +1,66 @@
package chat.simplex.app.views.helpers
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.widget.Toast
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.LocalAuthView
import chat.simplex.app.views.usersettings.LAMode
sealed class LAResult {
object Success: LAResult()
class Error(val errString: CharSequence): LAResult()
object Failed: LAResult()
object Unavailable: LAResult()
class Failed(val errString: CharSequence? = null): LAResult()
class Unavailable(val errString: CharSequence? = null): LAResult()
}
data class LocalAuthRequest (
val title: String?,
val reason: String,
val password: String,
val completed: (LAResult) -> Unit
) {
companion object {
val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { }
}
}
fun authenticate(
promptTitle: String,
promptSubtitle: String,
activity: FragmentActivity,
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
completed: (LAResult) -> Unit
) {
when {
SDK_INT in 28..29 ->
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
SDK_INT > 29 ->
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
else ->
completed(LAResult.Unavailable)
when (usingLAMode) {
LAMode.SYSTEM -> when {
SDK_INT in 28..29 ->
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
SDK_INT > 29 ->
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
else -> completed(LAResult.Unavailable())
}
LAMode.PASSCODE -> {
val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password)))
ModalManager.shared.showCustomModal(animated = false) { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) {
close()
completed(it)
})
}
}
}
}
}
@@ -66,7 +96,7 @@ private fun authenticateWithBiometricManager(
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
completed(LAResult.Failed)
completed(LAResult.Failed())
}
}
)
@@ -78,9 +108,7 @@ private fun authenticateWithBiometricManager(
.build()
biometricPrompt.authenticate(promptInfo)
}
else -> {
completed(LAResult.Unavailable)
}
else -> completed(LAResult.Unavailable())
}
}
@@ -89,6 +117,18 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume)
)
fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.lock_not_enabled),
generalGetString(R.string.you_can_turn_on_lock)
)
fun laFailedAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.la_auth_failed),
text = generalGetString(R.string.la_could_not_be_verified)
)
}
fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_unavailable),
generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled)
@@ -39,6 +39,7 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
}
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
VideoPlayer.stopAll()
AudioPlayer.stop()
val rec: MediaRecorder
recorder = initRecorder().also { rec = it }
@@ -152,6 +153,7 @@ object AudioPlayer {
return null
}
VideoPlayer.stopAll()
RecorderNative.stopRecording?.invoke()
val current = currentlyPlaying.value
if (current == null || current.first != filePath) {
@@ -12,6 +12,8 @@ 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.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -28,6 +30,21 @@ fun SimpleButton(text: String, icon: ImageVector,
}
}
@Composable
fun SimpleButtonDecorated(text: String, icon: ImageVector,
color: Color = MaterialTheme.colors.primary,
textDecoration: TextDecoration = TextDecoration.Underline,
fontWeight: FontWeight = FontWeight.Normal,
click: () -> Unit) {
SimpleButtonFrame(click) {
Icon(
icon, text, tint = color,
modifier = Modifier.padding(end = 8.dp)
)
Text(text, style = MaterialTheme.typography.caption, fontWeight = fontWeight, color = color, textDecoration = textDecoration)
}
}
@Composable
fun SimpleButton(
text: String, icon: ImageVector,
@@ -61,9 +78,9 @@ fun SimpleButtonIconEnded(
}
@Composable
fun SimpleButtonFrame(click: () -> Unit, disabled: Boolean = false, content: @Composable () -> Unit) {
fun SimpleButtonFrame(click: () -> Unit, modifier: Modifier = Modifier, disabled: Boolean = false, content: @Composable () -> Unit) {
Surface(shape = RoundedCornerShape(20.dp)) {
val modifier = if (disabled) Modifier else Modifier.clickable { click() }
val modifier = if (disabled) modifier else modifier.clickable { click() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.padding(8.dp)
@@ -10,6 +10,7 @@ import android.content.res.Resources
import android.graphics.*
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.*
import android.provider.OpenableColumns
@@ -237,12 +238,18 @@ 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: Int = 43_000
const val MAX_FILE_SIZE: Long = 8000000
const val MAX_FILE_SIZE_SMP: Long = 8000000
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824
fun getFilesDirectory(context: Context): String {
return context.filesDir.toString()
}
fun getTempFilesDirectory(context: Context): String {
return "${getFilesDirectory(context)}/temp_files"
}
fun getAppFilesDirectory(context: Context): String {
return "${getFilesDirectory(context)}/app_files"
}
@@ -489,9 +496,9 @@ fun formatBytes(bytes: Long): String {
return "0 bytes"
}
val bytesDouble = bytes.toDouble()
val k = 1000.toDouble()
val k = 1024.toDouble()
val units = arrayOf("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
val i = kotlin.math.floor(log2(bytesDouble) / log2(k))
val i = floor(log2(bytesDouble) / log2(k))
val size = bytesDouble / k.pow(i)
val unit = units[i.toInt()]
@@ -536,6 +543,26 @@ fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in
return fileCount to bytes
}
fun getMaxFileSize(fileProtocol: FileProtocol): Long {
return when (fileProtocol) {
FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP
FileProtocol.SMP -> MAX_FILE_SIZE_SMP
}
}
fun getBitmapFromVideo(uri: Uri, timestamp: Long? = null, random: Boolean = true): VideoPlayer.PreviewAndDuration {
val mmr = MediaMetadataRetriever()
mmr.setDataSource(SimplexApp.context, uri)
val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
val image = when {
timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST)
random -> mmr.frameAtTime
else -> mmr.getFrameAtIndex(0)
}
mmr.release()
return VideoPlayer.PreviewAndDuration(image, durationMs, timestamp ?: 0)
}
fun Color.darker(factor: Float = 0.1f): Color =
Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha)
@@ -593,3 +620,40 @@ fun UriHandler.openUriCatching(uri: String) {
Log.e(TAG, e.stackTraceToString())
}
}
fun IntSize.Companion.Saver(): Saver<IntSize, *> = Saver(
save = { it.width to it.height },
restore = { IntSize(it.first, it.second) }
)
@Composable
fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) {
val context = LocalContext.current
DisposableEffect(Unit) {
always()
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
val orientation = activity.resources.configuration.orientation
onDispose {
whenDispose()
if (orientation == activity.resources.configuration.orientation) {
whenGone()
}
}
}
}
@Composable
fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) {
val context = LocalContext.current
DisposableEffect(Unit) {
always()
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
val orientation = activity.resources.configuration.orientation
onDispose {
whenDispose()
if (orientation != activity.resources.configuration.orientation) {
whenRotate()
}
}
}
}
@@ -0,0 +1,246 @@
package chat.simplex.app.views.helpers
import android.content.Context
import android.graphics.Bitmap
import android.media.session.PlaybackState
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import chat.simplex.app.*
import chat.simplex.app.R
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.*
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import kotlinx.coroutines.*
import java.io.File
class VideoPlayer private constructor(
private val uri: Uri,
private val gallery: Boolean,
private val defaultPreview: Bitmap,
defaultDuration: Long,
soundEnabled: Boolean,
context: Context
) {
companion object {
private val players: MutableMap<Pair<Uri, Boolean>, VideoPlayer> = mutableMapOf()
private val previewsAndDurations: MutableMap<Uri, PreviewAndDuration> = mutableMapOf()
fun getOrCreate(
uri: Uri,
gallery: Boolean,
defaultPreview: Bitmap,
defaultDuration: Long,
soundEnabled: Boolean,
context: Context
): VideoPlayer =
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled, context) }
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
player(fileName, gallery)?.enableSound(enable) == true
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
fileName ?: return null
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
}
fun release(uri: Uri, gallery: Boolean, remove: Boolean) =
player(uri.path, gallery)?.release(remove)
fun stopAll() {
players.values.forEach { it.stop() }
}
fun releaseAll() {
players.values.forEach { it.release(false) }
players.clear()
previewsAndDurations.clear()
}
}
data class PreviewAndDuration(val preview: Bitmap?, val duration: Long?, val timestamp: Long)
private val currentVolume: Float
val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
val progress: MutableState<Long> = mutableStateOf(0L)
val duration: MutableState<Long> = mutableStateOf(defaultDuration)
val preview: MutableState<Bitmap> = mutableStateOf(defaultPreview)
init {
setPreviewAndDuration()
}
val player = ExoPlayer.Builder(context,
DefaultRenderersFactory(context))
/*.setLoadControl(DefaultLoadControl.Builder()
.setPrioritizeTimeOverSizeThresholds(false) // Could probably save some megabytes in memory in case it will be needed
.createDefaultLoadControl())*/
.setSeekBackIncrementMs(10_000)
.setSeekForwardIncrementMs(10_000)
.build()
.apply {
// Repeat the same track endlessly
repeatMode = 1
currentVolume = volume
if (!soundEnabled) {
volume = 0f
}
setAudioAttributes(
AudioAttributes.Builder()
.setContentType(CONTENT_TYPE_MUSIC)
.setUsage(USAGE_MEDIA)
.build(),
true // disallow to play multiple instances simultaneously
)
}
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
private var progressJob: Job? = null
enum class TrackState {
PLAYING, PAUSED, STOPPED
}
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
val filepath = getAppFilePath(SimplexApp.context, uri)
if (filepath == null || !File(filepath).exists()) {
Log.e(TAG, "No such file: $uri")
brokenVideo.value = true
return false
}
if (soundEnabled.value) {
RecorderNative.stopRecording?.invoke()
}
AudioPlayer.stop()
stopAll()
if (listener.value == null) {
runCatching {
val dataSourceFactory = DefaultDataSource.Factory(SimplexApp.context, DefaultHttpDataSource.Factory())
val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri))
player.setMediaSource(source, seek ?: 0L)
}.onFailure {
Log.e(TAG, it.stackTraceToString())
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), it.message)
brokenVideo.value = true
return false
}
}
if (player.playbackState == PlaybackState.STATE_NONE || player.playbackState == PlaybackState.STATE_STOPPED) {
runCatching { player.prepare() }.onFailure {
// Can happen when video file is broken
Log.e(TAG, it.stackTraceToString())
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), it.message)
brokenVideo.value = true
return false
}
}
if (seek != null) player.seekTo(seek)
player.play()
listener.value = onProgressUpdate
// Player can only be accessed in one specific thread
progressJob = CoroutineScope(Dispatchers.Main).launch {
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
while (isActive && player.playbackState != Player.STATE_IDLE && player.playWhenReady) {
// Even when current position is equal to duration, the player has isPlaying == true for some time,
// so help to make the playback stopped in UI immediately
if (player.currentPosition == player.duration) {
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
break
}
delay(50)
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
}
/*
* Since coroutine is still NOT canceled, means player ended (no stop/no pause). But in some cases
* the player can show position != duration even if they actually equal.
* Let's say to a listener that the position == duration in case of coroutine finished without cancel
* */
if (isActive) {
onProgressUpdate(player.duration, TrackState.PAUSED)
}
onProgressUpdate(null, TrackState.PAUSED)
}
player.addListener(object: Player.Listener{
override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying)
// Produce non-ideal transition from stopped to playing state while showing preview image in ChatView
// videoPlaying.value = isPlaying
}
})
return true
}
fun stop() {
player.stop()
stopListener()
}
private fun stopListener() {
val afterCoroutineCancel: CompletionHandler = {
// Notify prev video listener about stop
listener.value?.invoke(null, TrackState.STOPPED)
}
/** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be:
* [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.STOPPED] (in this order)
* */
if (progressJob != null) {
progressJob?.invokeOnCompletion(afterCoroutineCancel)
} else {
afterCoroutineCancel(null)
}
progressJob?.cancel()
progressJob = null
}
fun play(resetOnEnd: Boolean) {
if (progress.value == duration.value) {
progress.value = 0
}
videoPlaying.value = start(progress.value) { pro, _ ->
if (pro != null) {
progress.value = pro
}
if (pro == null || pro == duration.value) {
videoPlaying.value = false
if (pro == duration.value) {
progress.value = if (resetOnEnd) 0 else duration.value
}/* else if (state == TrackState.STOPPED) {
progress.value = 0 //
}*/
}
}
}
fun enableSound(enable: Boolean): Boolean {
if (soundEnabled.value == enable) return false
soundEnabled.value = enable
player.volume = if (enable) currentVolume else 0f
return true
}
fun release(remove: Boolean) {
player.release()
if (remove) {
players.remove(uri to gallery)
}
}
private fun setPreviewAndDuration() {
// It freezes main thread, doing it in IO thread
CoroutineScope(Dispatchers.IO).launch {
val previewAndDuration = previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
withContext(Dispatchers.Main) {
preview.value = previewAndDuration.preview ?: defaultPreview
duration.value = (previewAndDuration.duration ?: 0)
}
}
}
}
@@ -0,0 +1,22 @@
package chat.simplex.app.views.localauth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.*
@Composable
fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
val passcode = rememberSaveable { mutableStateOf("") }
PasscodeView(passcode, authRequest.title ?: stringResource(R.string.la_enter_app_passcode), authRequest.reason, stringResource(R.string.submit_passcode),
submit = {
val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(R.string.incorrect_passcode))
authRequest.completed(r)
},
cancel = {
authRequest.completed(LAResult.Error(generalGetString(R.string.authentication_cancelled)))
})
}
@@ -0,0 +1,100 @@
package chat.simplex.app.views.localauth
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.views.helpers.*
@Composable
fun PasscodeView(
passcode: MutableState<String>,
title: String,
reason: String? = null,
submitLabel: String,
submitEnabled: ((String) -> Boolean)? = null,
submit: () -> Unit,
cancel: () -> Unit,
) {
@Composable
fun VerticalLayout() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(title, style = MaterialTheme.typography.h1)
if (reason != null) {
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
}
}
PasscodeEntry(passcode, true)
Row {
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
Spacer(Modifier.size(20.dp))
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
}
}
}
@Composable
fun HorizontalLayout() {
Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) {
Column(
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(title, style = MaterialTheme.typography.h1)
if (reason != null) {
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
}
}
PasscodeEntry(passcode, false)
}
Column(
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
// Just to fill space to correctly calculate the height
Column {
Text("", style = MaterialTheme.typography.h1)
if (reason != null) {
Text("", Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
}
PasscodeView(remember { mutableStateOf("") })
}
BoxWithConstraints {
val s = minOf(maxWidth, maxHeight) / 3.5f
Column(
Modifier.padding(start = 30.dp).height(s * 3),
verticalArrangement = Arrangement.SpaceEvenly
) {
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
}
}
}
}
}
if (LocalContext.current.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
VerticalLayout()
} else {
HorizontalLayout()
}
}
@@ -0,0 +1,183 @@
package chat.simplex.app.views.localauth
import androidx.compose.foundation.background
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.filled.Close
import androidx.compose.material.icons.outlined.Backspace
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.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun PasscodeEntry(
password: MutableState<String>,
vertical: Boolean,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
PasscodeView(password)
BoxWithConstraints {
if (vertical) {
VerticalPasswordGrid(password)
} else {
HorizontalPasswordGrid(password)
}
}
}
}
@Composable
fun PasscodeView(password: MutableState<String>) {
var showPasscode by rememberSaveable { mutableStateOf(false) }
Text(
if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) },
Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode },
style = MaterialTheme.typography.body1
)
}
@Composable
private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState<String>) {
val s = minOf(maxWidth, maxHeight) / 4 - 1.dp
Column(Modifier.width(IntrinsicSize.Min)) {
DigitsRow(s, 1, 2, 3, password)
Divider()
DigitsRow(s, 4, 5, 6, password)
Divider()
DigitsRow(s, 7, 8, 9, password)
Divider()
Row(Modifier.requiredHeight(s)) {
PasswordEdit(s, Icons.Default.Close) {
password.value = ""
}
VerticalDivider()
PasswordDigit(s, 0, password)
VerticalDivider()
PasswordEdit(s, Icons.Outlined.Backspace) {
password.value = password.value.dropLast(1)
}
}
}
}
@Composable
private fun BoxWithConstraintsScope.HorizontalPasswordGrid(password: MutableState<String>) {
val s = minOf(maxWidth, maxHeight) / 3.5f - 1.dp
Column(Modifier.width(IntrinsicSize.Min)) {
Row(Modifier.height(IntrinsicSize.Min)) {
DigitsRow(s, 1, 2, 3, password);
VerticalDivider()
PasswordEdit(s, Icons.Default.Close) {
password.value = ""
}
}
Divider()
Row(Modifier.height(IntrinsicSize.Min)) {
DigitsRow(s, 4, 5, 6, password)
VerticalDivider()
PasswordDigit(s, 0, password)
}
Divider()
Row(Modifier.height(IntrinsicSize.Min)) {
DigitsRow(s, 7, 8, 9, password)
VerticalDivider()
PasswordEdit(s, Icons.Outlined.Backspace) {
password.value = password.value.dropLast(1)
}
}
}
}
private fun splitPassword(showPassword: Boolean, password: String): String {
val n = if (password.length < 8) 8 else 4
return password.mapIndexed { index, c -> (if (showPassword) c.toString() else "") + (if ((index + 1) % n == 0) " " else "") }.joinToString("")
}
@Composable
private fun DigitsRow(size: Dp, d1: Int, d2: Int, d3: Int, password: MutableState<String>) {
Row(Modifier.height(size)) {
PasswordDigit(size, d1, password)
VerticalDivider()
PasswordDigit(size, d2, password)
VerticalDivider()
PasswordDigit(size, d3, password)
}
}
@Composable
private fun PasswordDigit(size: Dp, d: Int, password: MutableState<String>) {
val s = d.toString()
return PasswordButton(size, action = {
if (password.value.length < 16) {
password.value += s
}
}) {
Text(
s,
style = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 30.sp,
letterSpacing = (-0.5).sp
),
color = HighOrLowlight
)
}
}
@Composable
private fun PasswordEdit(size: Dp, image: ImageVector, action: () -> Unit) {
PasswordButton(size, action) {
Icon(image, null, tint = HighOrLowlight)
}
}
@Composable
private fun PasswordButton(size: Dp, action: () -> Unit, content: @Composable BoxScope.() -> Unit) {
return Box(
Modifier.size(size)
.background(MaterialTheme.colors.background, RoundedCornerShape(50))
.clickable { action() },
contentAlignment = Alignment.Center
) {
content()
}
}
@Composable
fun VerticalDivider(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha),
thickness: Dp = 1.dp,
startIndent: Dp = 0.dp
) {
val indentMod = if (startIndent.value != 0f) {
Modifier.padding(top = startIndent)
} else {
Modifier
}
val targetThickness = if (thickness == Dp.Hairline) {
(1f / LocalDensity.current.density).dp
} else {
thickness
}
Box(
modifier.then(indentMod)
.fillMaxHeight()
.width(targetThickness)
.background(color = color)
)
}
private const val DividerAlpha = 0.12f
@@ -0,0 +1,48 @@
package chat.simplex.app.views.localauth
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import chat.simplex.app.R
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun SetAppPasscodeView(
submit: () -> Unit,
cancel: () -> Unit,
close: () -> Unit
) {
val passcode = rememberSaveable { mutableStateOf("") }
var enteredPassword by rememberSaveable { mutableStateOf("") }
var confirming by rememberSaveable { mutableStateOf(false) }
@Composable
fun SetPasswordView(title: String, submitLabel: String, submitEnabled: (((String) -> Boolean))? = null, submit: () -> Unit) {
PasscodeView(passcode, title = title, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) {
close()
cancel()
}
}
if (confirming) {
SetPasswordView(
generalGetString(R.string.confirm_passcode),
generalGetString(R.string.confirm_verb),
submitEnabled = { pwd -> pwd == enteredPassword }
) {
if (passcode.value == enteredPassword) {
ksAppPassword.set(passcode.value)
enteredPassword = ""
passcode.value = ""
close()
submit()
}
}
} else {
SetPasswordView(generalGetString(R.string.new_passcode), generalGetString(R.string.save_verb)) {
enteredPassword = passcode.value
passcode.value = ""
confirming = true
}
}
}
@@ -88,13 +88,14 @@ fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit)
}
@Composable
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String) {
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String, centered: Boolean = false) {
if (chatModelIncognito) {
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start
) {
Icon(
if (supportedIncognito) Icons.Filled.TheaterComedy else Icons.Outlined.Info,
@@ -102,14 +103,15 @@ fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean
tint = if (supportedIncognito) Indigo else WarningOrange,
modifier = Modifier.padding(end = 10.dp).size(20.dp)
)
Text(onText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
Text(onText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2)
}
} else {
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start
) {
Icon(
Icons.Outlined.Info,
@@ -117,7 +119,7 @@ fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean
tint = HighOrLowlight,
modifier = Modifier.padding(end = 10.dp).size(20.dp)
)
Text(offText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
Text(offText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2)
}
}
}
@@ -1,6 +1,5 @@
package chat.simplex.app.views.newchat
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -15,6 +14,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -26,6 +27,7 @@ import chat.simplex.app.views.chat.group.AddGroupMembersView
import chat.simplex.app.views.chatlist.setGroupMembers
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.usersettings.DeleteImageButton
import chat.simplex.app.views.usersettings.EditImageButton
import com.google.accompanist.insets.ProvideWindowInsets
@@ -36,7 +38,6 @@ import kotlinx.coroutines.launch
@Composable
fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
AddGroupLayout(
chatModel.incognito.value,
createGroup = { groupProfile ->
withApi {
val groupInfo = chatModel.controller.apiNewGroup(groupProfile)
@@ -57,11 +58,11 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
}
@Composable
fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val displayName = rememberSaveable { mutableStateOf("") }
val fullName = rememberSaveable { mutableStateOf("") }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
val focusRequester = remember { FocusRequester() }
@@ -88,14 +89,8 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitle(stringResource(R.string.create_secret_group_title), false)
Text(stringResource(R.string.group_is_decentralized))
InfoAboutIncognito(
chatModelIncognito,
false,
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
generalGetString(R.string.group_main_profile_sent)
)
AppBarTitleCentered(stringResource(R.string.create_secret_group_title))
ReadableText(R.string.group_is_decentralized, TextAlign.Center)
Box(
Modifier
.fillMaxWidth()
@@ -104,7 +99,7 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(size = 192.dp, image = profileImage.value)
ProfileImage(108.dp, image = profileImage.value)
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
@@ -112,24 +107,28 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
}
}
}
Text(
stringResource(R.string.group_display_name_field),
Modifier.padding(bottom = 3.dp)
)
ProfileNameField(displayName, focusRequester)
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.group_display_name_field),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text(
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.group_full_name_field),
Modifier.padding(bottom = 5.dp)
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName)
ProfileNameField(fullName, "")
Spacer(Modifier.height(8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
@@ -163,7 +162,7 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
) {
Surface(shape = RoundedCornerShape(20.dp)) {
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = color)
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = color, fontWeight = FontWeight.Bold)
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = color)
}
}
@@ -175,7 +174,6 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
fun PreviewAddGroupLayout() {
SimpleXTheme {
AddGroupLayout(
chatModelIncognito = false,
createGroup = {},
close = {}
)
@@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
@@ -211,6 +212,52 @@ fun ActionButton(
}
}
@Composable
fun ActionButton(
modifier: Modifier,
text: String?,
comment: String?,
icon: Painter,
tint: Color = MaterialTheme.colors.primary,
disabled: Boolean = false,
click: () -> Unit = {}
) {
Surface(modifier, shape = RoundedCornerShape(18.dp)) {
Column(
Modifier
.fillMaxWidth()
.clickable(onClick = click)
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val tint = if (disabled) HighOrLowlight else tint
Icon(
icon, text,
tint = tint,
modifier = Modifier
.size(40.dp)
.padding(bottom = 8.dp)
)
if (text != null) {
Text(
text,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
color = tint,
modifier = Modifier.padding(bottom = 4.dp)
)
}
if (comment != null) {
Text(
comment,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body2
)
}
}
}
}
@Preview
@Composable
private fun PreviewNewChatSheet() {
@@ -11,6 +11,7 @@ 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.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -55,8 +56,13 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
}
@Composable
fun ReadableText(@StringRes stringResId: Int) {
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(bottom = 12.dp), lineHeight = 22.sp)
fun ReadableText(@StringRes stringResId: Int, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp)
}
@Composable
fun ReadableText(text: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
Text(text, modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp)
}
@Preview(showBackground = true)
@@ -70,4 +76,4 @@ fun PreviewHowItWorks() {
SimpleXTheme {
HowItWorks(user = null)
}
}
}
@@ -12,6 +12,8 @@ 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.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
@@ -23,22 +25,25 @@ 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))
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 14.dp)
) {
//CloseSheetBar(null)
AppBarTitleCentered(stringResource(R.string.onboarding_notifications_mode_title))
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
Text(stringResource(R.string.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
Spacer(Modifier.height(DEFAULT_PADDING * 2f))
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) {
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
changeNotificationsMode(currentMode.value, m)
}
}
@@ -51,18 +56,24 @@ private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mod
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),
shape = RoundedCornerShape(35.dp),
) {
Column(Modifier.padding(bottom = 6.dp).padding(horizontal = 8.dp)) {
Column(Modifier.padding(horizontal = 14.dp).padding(top = 4.dp, bottom = 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)
modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center
)
Text(annotatedStringResource(description),
Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colors.onBackground,
lineHeight = 24.sp,
textAlign = TextAlign.Center
)
Text(annotatedStringResource(description), color = MaterialTheme.colors.onBackground, lineHeight = 24.sp)
}
}
Spacer(Modifier.height(DEFAULT_PADDING))
Spacer(Modifier.height(14.dp))
}
@@ -4,6 +4,7 @@ import android.content.res.Configuration
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.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
@@ -44,13 +45,13 @@ fun SimpleXInfoLayout(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING),
.padding(start = DEFAULT_PADDING * 1.5f, end = DEFAULT_PADDING * 1.5f, top = DEFAULT_PADDING * 4,/* bottom = DEFAULT_PADDING * 4*/),
) {
Box(Modifier.fillMaxWidth().padding(top = 8.dp), contentAlignment = Alignment.Center) {
Box(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 10.dp), contentAlignment = Alignment.Center) {
SimpleXLogo()
}
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 36.dp).padding(horizontal = 48.dp), textAlign = TextAlign.Center)
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 60.dp).padding(horizontal = 48.dp), textAlign = TextAlign.Center)
InfoRow(painterResource(R.drawable.privacy), R.string.privacy_redefined, R.string.first_platform_without_user_ids, width = 80.dp)
InfoRow(painterResource(R.drawable.shield), R.string.immune_to_spam_and_abuse, R.string.people_can_connect_only_via_links_you_share)
@@ -68,11 +69,12 @@ fun SimpleXInfoLayout(
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 16.dp), contentAlignment = Alignment.Center
.padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING), contentAlignment = Alignment.Center
) {
SimpleButton(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
SimpleButtonDecorated(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
click = showModal { HowItWorks(user, onboardingStage) })
}
Spacer(Modifier.weight(1f))
}
}
@@ -83,7 +85,7 @@ fun SimpleXLogo() {
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier
.padding(vertical = DEFAULT_PADDING)
.fillMaxWidth(0.80f)
.fillMaxWidth(0.60f)
)
}
@@ -103,9 +105,9 @@ 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) {
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, true, onclick)
} else {
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, true, onclick)
}
}
@@ -114,16 +116,30 @@ fun OnboardingActionButton(
@StringRes labelId: Int,
onboarding: OnboardingStage?,
onboardingStage: MutableState<OnboardingStage?>,
border: Boolean,
onclick: (() -> Unit)?
) {
val modifier = if (border) {
Modifier
.border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50))
.padding(
horizontal = DEFAULT_PADDING * 3,
vertical = 4.dp
)
} else {
Modifier
}
SimpleButtonFrame(click = {
onclick?.invoke()
onboardingStage.value = onboarding
}) {
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary)
}, modifier) {
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp)
Icon(
Icons.Outlined.ArrowForwardIos, "next stage", tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 8.dp)
modifier = Modifier
.padding(start = 16.dp, top = 5.dp)
.size(15.dp)
)
}
}
@@ -1,7 +1,7 @@
package chat.simplex.app.views.onboarding
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -40,11 +40,13 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
}
Column(
horizontalAlignment = Alignment.Start
horizontalAlignment = Alignment.Start,
modifier = Modifier.padding(bottom = 12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(bottom = 4.dp)
) {
Icon(icon, stringResource(titleId), tint = HighOrLowlight)
Text(
@@ -107,8 +109,9 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
ModalView(close = close) {
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -307,6 +310,27 @@ private val versionDescriptions: List<VersionDescription> = listOf(
)
)
),
VersionDescription(
version = "v5.0",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.UploadFile,
titleId = R.string.v5_0_large_files_support,
descrId = R.string.v5_0_large_files_support_descr
),
FeatureDescription(
icon = Icons.Outlined.Lock,
titleId = R.string.v5_0_app_passcode,
descrId = R.string.v5_0_app_passcode_descr
),
FeatureDescription(
icon = Icons.Outlined.Translate,
titleId = R.string.v5_0_polish_interface,
descrId = R.string.v5_0_polish_interface_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
)
)
private val lastVersion = versionDescriptions.last().version
@@ -12,6 +12,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
@@ -102,15 +103,6 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
saveCfg(newCfg)
}
fun updateSettingsDialog(action: () -> Unit) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.update_network_settings_question),
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
confirmText = generalGetString(R.string.update_network_settings_confirmation),
onConfirm = action
)
}
AdvancedNetworkSettingsLayout(
networkTCPConnectTimeout,
networkTCPTimeout,
@@ -121,10 +113,10 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPKeepIntvl,
networkTCPKeepCnt,
resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults else currentCfg.value == NetCfg.defaults,
reset = { updateSettingsDialog(::reset) },
reset = { showUpdateNetworkSettingsDialog(::reset) },
footerDisabled = buildCfg() == currentCfg.value,
revert = { updateView(currentCfg.value) },
save = { updateSettingsDialog { saveCfg(buildCfg()) } }
save = { showUpdateNetworkSettingsDialog { saveCfg(buildCfg()) } }
)
}
@@ -262,14 +254,14 @@ fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
var expanded by remember { mutableStateOf(false) }
val expanded = rememberSaveable { mutableStateOf(false) }
Text(title)
ExposedDropdownMenuBox(
expanded = expanded,
expanded = expanded.value,
onExpandedChange = {
expanded = !expanded
expanded.value = !expanded.value
}
) {
Row(
@@ -285,24 +277,22 @@ fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>
)
Spacer(Modifier.size(4.dp))
Icon(
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.invite_to_group_button),
modifier = Modifier.padding(start = 8.dp),
tint = HighOrLowlight
)
}
ExposedDropdownMenu(
DefaultExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selection.value = selectionOption
expanded = false
}
expanded.value = false
},
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
) {
Text(
"$selectionOption $label",
@@ -323,14 +313,14 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
var expanded by remember { mutableStateOf(false) }
val expanded = remember { mutableStateOf(false) }
Text(title)
ExposedDropdownMenuBox(
expanded = expanded,
expanded = expanded.value,
onExpandedChange = {
expanded = !expanded
expanded.value = !expanded.value
}
) {
val df = DecimalFormat("#.#")
@@ -348,24 +338,22 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List
)
Spacer(Modifier.size(4.dp))
Icon(
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.invite_to_group_button),
modifier = Modifier.padding(start = 8.dp),
tint = HighOrLowlight
)
}
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
DefaultExposedDropdownMenu(
expanded = expanded
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selection.value = selectionOption
expanded = false
}
expanded.value = false
},
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
) {
Text(
"${df.format(selectionOption / 1_000_000.toDouble())} $label",
@@ -415,6 +403,15 @@ fun FooterButton(icon: ImageVector, title: String, action: () -> Unit, disabled:
}
}
fun showUpdateNetworkSettingsDialog(action: () -> Unit) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.update_network_settings_question),
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
confirmText = generalGetString(R.string.update_network_settings_confirmation),
onConfirm = action
)
}
@Preview(showBackground = true)
@Composable
fun PreviewAdvancedNetworkSettingsLayout() {
@@ -236,6 +236,7 @@ private fun LangSelector(state: State<String>, onSelected: (String) -> Unit) {
"fr" to "Français",
"it" to "Italiano",
"nl" to "Nederlands",
"pl" to "Polski",
"ru" to "Русский",
"zh-CN" to "简体中文"
)
@@ -0,0 +1,47 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
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.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.TerminalView
import chat.simplex.app.views.helpers.*
@Composable
fun DeveloperView(
m: ChatModel,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
withAuth: (block: () -> Unit) -> Unit
) {
Column(Modifier.fillMaxWidth()) {
val uriHandler = LocalUriHandler.current
AppBarTitle(stringResource(R.string.settings_developer_tools))
val developerTools = m.controller.appPrefs.developerTools
val devTools = remember { mutableStateOf(developerTools.get()) }
SectionView() {
InstallTerminalAppItem(uriHandler)
SectionDivider()
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.Code, stringResource(R.string.show_developer_options), developerTools, devTools)
}
SectionTextFooter(
generalGetString(if (devTools.value) R.string.show_dev_options else R.string.hide_dev_options) + " " +
generalGetString(R.string.developer_options)
)
SectionSpacer()
}
}
@@ -1,33 +0,0 @@
package chat.simplex.app.views.usersettings
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Videocam
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
@Composable
fun ExperimentalFeaturesView(chatModel: ChatModel, enableCalls: MutableState<Boolean>) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Text(
stringResource(R.string.settings_experimental_features),
style = MaterialTheme.typography.h1,
modifier = Modifier.padding(start = 16.dp, bottom = 24.dp)
)
SectionView("") {
SettingsPreferenceItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), chatModel.controller.appPrefs.experimentalCalls, enableCalls)
}
}
}
@@ -69,11 +69,12 @@ private fun HiddenProfileLayout(
val hidePassword = rememberSaveable { mutableStateOf("") }
val confirmHidePassword = rememberSaveable { mutableStateOf("") }
val passwordValid by remember { derivedStateOf { hidePassword.value == hidePassword.value.trim() } }
val confirmValid by remember { derivedStateOf { confirmHidePassword.value == "" || hidePassword.value == confirmHidePassword.value } }
val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || confirmHidePassword.value == "" || !confirmValid } }
val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } }
SectionView(stringResource(R.string.hidden_profile_password).uppercase()) {
SectionItemView {
PassphraseField(hidePassword, generalGetString(R.string.password_to_show), isValid = { true }, showStrength = true)
PassphraseField(hidePassword, generalGetString(R.string.password_to_show), isValid = { passwordValid }, showStrength = true)
}
SectionDivider()
SectionItemView {
@@ -86,4 +87,4 @@ private fun HiddenProfileLayout(
}
SectionTextFooter(stringResource(R.string.to_reveal_profile_enter_password))
}
}
}
@@ -1,18 +1,26 @@
package chat.simplex.app.views.usersettings
import SectionCustomFooter
import SectionDivider
import SectionItemView
import SectionItemWithValue
import SectionSpacer
import SectionTextFooter
import SectionView
import SectionViewSelectable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
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.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
@@ -43,6 +51,7 @@ fun NetworkAndServersView(
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
sessionMode = sessionMode,
proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } },
showModal = showModal,
showSettingsModal = showSettingsModal,
showCustomModal = showCustomModal,
@@ -86,7 +95,7 @@ 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)
}
updateNetworkSettingsDialog(
showUpdateNetworkSettingsDialog(
title = generalGetString(R.string.update_onion_hosts_settings_question),
startsWith,
onDismiss = {
@@ -113,7 +122,7 @@ fun NetworkAndServersView(
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
}
updateNetworkSettingsDialog(
showUpdateNetworkSettingsDialog(
title = generalGetString(R.string.update_network_session_mode_question),
startsWith,
onDismiss = { sessionMode.value = prevValue }
@@ -138,6 +147,7 @@ fun NetworkAndServersView(
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
proxyPort: State<Int>,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
@@ -152,10 +162,14 @@ fun NetworkAndServersView(
) {
AppBarTitle(stringResource(R.string.network_and_servers))
SectionView(generalGetString(R.string.settings_section_title_messages)) {
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> SMPServersView(m, close) })
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.SMP, close) })
SectionDivider()
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.XFTP, close) })
SectionDivider()
SectionItemView {
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
}
SectionDivider()
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
@@ -166,7 +180,10 @@ fun NetworkAndServersView(
}
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
}
Spacer(Modifier.height(8.dp))
if (networkUseSocksProxy.value) {
SectionCustomFooter { Text(annotatedStringResource(R.string.disable_onion_hosts_when_not_supported)) }
}
Spacer(Modifier.height(16.dp))
SectionView(generalGetString(R.string.settings_section_title_calls)) {
SettingsActionItem(Icons.Outlined.ElectricalServices, stringResource(R.string.webrtc_ice_servers), showModal { RTCServersView(it) })
}
@@ -176,7 +193,9 @@ fun NetworkAndServersView(
@Composable
fun UseSocksProxySwitch(
networkUseSocksProxy: MutableState<Boolean>,
toggleSocksProxy: (Boolean) -> Unit
proxyPort: State<Int>,
toggleSocksProxy: (Boolean) -> Unit,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
) {
Row(
Modifier.fillMaxWidth(),
@@ -193,7 +212,19 @@ fun UseSocksProxySwitch(
stringResource(R.string.network_socks_toggle),
tint = HighOrLowlight
)
Text(stringResource(R.string.network_socks_toggle))
if (networkUseSocksProxy.value) {
Row {
Text(generalGetString(R.string.network_socks_toggle_use_socks_proxy) + " (")
Text(
generalGetString(R.string.network_proxy_port).format(proxyPort.value),
Modifier.clickable { showSettingsModal { SockProxySettings(it) }() },
color = MaterialTheme.colors.primary
)
Text(")")
}
} else {
Text(stringResource(R.string.network_socks_toggle))
}
}
Switch(
checked = networkUseSocksProxy.value,
@@ -206,6 +237,83 @@ fun UseSocksProxySwitch(
}
}
@Composable
fun SockProxySettings(m: ChatModel) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_BOTTOM_PADDING),
) {
val defaultHostPort = remember { "localhost:9050" }
AppBarTitle(generalGetString(R.string.network_socks_proxy_settings))
val hostPort by remember { m.controller.appPrefs.networkProxyHostPort.state }
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPort?.split(":")?.firstOrNull() ?: "localhost"))
}
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPort?.split(":")?.lastOrNull() ?: "9050"))
}
val save = {
withBGApi {
m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
}
}
SectionView {
SectionItemView {
ResetToDefaultsButton({
showUpdateNetworkSettingsDialog {
m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort)
val newHost = defaultHostPort.split(":").first()
val newPort = defaultHostPort.split(":").last()
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
save()
}
}, disabled = hostPort == defaultHostPort)
}
SectionDivider()
SectionItemView {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(R.string.host_verb),
modifier = Modifier,
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
}
SectionDivider()
SectionItemView {
DefaultConfigurableTextField(
portUnsaved,
stringResource(R.string.port_verb),
modifier = Modifier,
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
keyboardType = KeyboardType.Number,
)
}
}
SectionCustomFooter {
NetworkSectionFooter(
revert = {
val prevHost = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.firstOrNull() ?: "localhost"
val prevPort = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.lastOrNull() ?: "9050"
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
},
save = { showUpdateNetworkSettingsDialog { save() } },
revertDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
saveDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
)
}
}
}
@Composable
private fun UseOnionHosts(
onionHosts: MutableState<OnionHosts>,
@@ -274,7 +382,32 @@ private fun SessionModePicker(
)
}
private fun updateNetworkSettingsDialog(
@Composable
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
FooterButton(Icons.Outlined.Replay, stringResource(R.string.network_options_revert), revert, revertDisabled)
FooterButton(Icons.Outlined.Check, stringResource(R.string.network_options_save), save, saveDisabled)
}
}
// https://stackoverflow.com/a/106223
private fun validHost(s: String): Boolean {
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
return s.matches(validIp) || s.matches(validHostname)
}
// https://ihateregex.io/expr/port/
private fun validPort(s: String): Boolean {
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
return s.isNotBlank() && s.matches(validPort)
}
private fun showUpdateNetworkSettingsDialog(
title: String,
startsWith: String = "",
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
@@ -298,6 +431,7 @@ fun PreviewNetworkAndServersLayout() {
NetworkAndServersLayout(
developerTools = true,
networkUseSocksProxy = remember { mutableStateOf(true) },
proxyPort = remember { mutableStateOf(9050) },
showModal = { {} },
showSettingsModal = { {} },
showCustomModal = { {} },
@@ -80,6 +80,11 @@ private fun PreferencesLayout(
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
}
SectionSpacer()
val allowCalls = remember(preferences) { mutableStateOf(preferences.calls.allow) }
FeatureSection(ChatFeature.Calls, allowCalls) {
applyPrefs(preferences.copy(calls = SimpleChatPreference(allow = it)))
}
SectionSpacer()
ResetSaveButtons(
reset = reset,
save = savePrefs,
@@ -97,11 +102,12 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllo
FeatureAllowed.values().map { it to it.text },
allowFeature,
icon = feature.icon,
onSelected = onSelected
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
onSelected = onSelected,
)
}
}
SectionTextFooter(feature.allowDescription(allowFeature.value))
SectionTextFooter(feature.allowDescription(allowFeature.value) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
}
@Composable
@@ -7,22 +7,41 @@ import SectionTextFooter
import SectionView
import android.view.WindowManager
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
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.unit.dp
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimplexGreen
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.SetAppPasscodeView
enum class LAMode {
SYSTEM,
PASSCODE;
val text: String
get() = when (this) {
SYSTEM -> generalGetString(R.string.la_mode_system)
PASSCODE -> generalGetString(R.string.la_mode_passcode)
}
}
@Composable
fun PrivacySettingsView(
chatModel: ChatModel,
setPerformLA: (Boolean) -> Unit
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean, FragmentActivity) -> Unit
) {
Column(
Modifier.fillMaxWidth(),
@@ -31,7 +50,7 @@ fun PrivacySettingsView(
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
AppBarTitle(stringResource(R.string.your_privacy))
SectionView(stringResource(R.string.settings_section_title_device)) {
ChatLockItem(chatModel.performLA, setPerformLA)
ChatLockItem(chatModel, showSettingsModal, setPerformLA)
SectionDivider()
val context = LocalContext.current
SettingsPreferenceItem(Icons.Outlined.VisibilityOff, stringResource(R.string.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on ->
@@ -52,10 +71,12 @@ fun PrivacySettingsView(
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SectionDivider()
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
simplexLinkMode.set(it)
chatModel.simplexLinkMode.value = it
}) }
SectionItemView {
SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
simplexLinkMode.set(it)
chatModel.simplexLinkMode.value = it
})
}
}
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
SectionTextFooter(stringResource(R.string.simplex_link_mode_browser_warning))
@@ -83,3 +104,236 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS
onSelected = onSelected
)
}
private val laDelays = listOf(10, 30, 60, 180, 0)
@Composable
fun SimplexLockView(
chatModel: ChatModel,
currentLAMode: SharedPreference<LAMode>,
setPerformLA: (Boolean, FragmentActivity) -> Unit
) {
val performLA = remember { chatModel.performLA }
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
val activity = LocalContext.current as FragmentActivity
fun resetLAEnabled(onOff: Boolean) {
chatModel.controller.appPrefs.performLA.set(onOff)
chatModel.performLA.value = onOff
}
fun disableUnavailableLA() {
resetLAEnabled(false)
currentLAMode.set(LAMode.SYSTEM)
laUnavailableInstructionAlert()
}
fun toggleLAMode(toLAMode: LAMode) {
authenticate(
if (toLAMode == LAMode.SYSTEM) {
generalGetString(R.string.la_enter_app_passcode)
} else {
generalGetString(R.string.chat_lock)
},
generalGetString(R.string.change_lock_mode), activity
) { laResult ->
when (laResult) {
is LAResult.Error -> {
laFailedAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
LAResult.Success -> {
when (toLAMode) {
LAMode.SYSTEM -> {
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity, toLAMode) { laResult ->
when (laResult) {
LAResult.Success -> {
currentLAMode.set(toLAMode)
ksAppPassword.remove()
laTurnedOnAlert()
}
is LAResult.Unavailable, is LAResult.Error -> {
laFailedAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
}
}
}
LAMode.PASSCODE -> {
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
laLockDelay.set(30)
currentLAMode.set(toLAMode)
passcodeAlert(generalGetString(R.string.passcode_set))
},
cancel = {},
close
)
}
}
}
}
}
is LAResult.Unavailable -> disableUnavailableLA()
}
}
}
fun changeLAPassword() {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
passcodeAlert(generalGetString(R.string.passcode_changed))
}, cancel = {
passcodeAlert(generalGetString(R.string.passcode_not_changed))
}, close
)
}
}
}
is LAResult.Error -> laFailedAlert()
is LAResult.Failed -> {}
is LAResult.Unavailable -> disableUnavailableLA()
}
}
}
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
AppBarTitle(stringResource(R.string.chat_lock))
SectionView {
EnableLock(performLA) { performLAToggle ->
performLA.value = performLAToggle
chatModel.controller.appPrefs.laNoticeShown.set(true)
if (performLAToggle) {
when (currentLAMode.state.value) {
LAMode.SYSTEM -> {
setPerformLA(true, activity)
}
LAMode.PASSCODE -> {
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
laLockDelay.set(30)
chatModel.controller.appPrefs.performLA.set(true)
passcodeAlert(generalGetString(R.string.passcode_set))
},
cancel = {
resetLAEnabled(false)
}, close
)
}
}
}
}
} else {
setPerformLA(false, activity)
}
}
SectionDivider()
SectionItemView {
LockModeSelector(laMode) { newLAMode ->
if (laMode.value == newLAMode) return@LockModeSelector
if (chatModel.controller.appPrefs.performLA.get()) {
toggleLAMode(newLAMode)
} else {
currentLAMode.set(newLAMode)
}
}
}
if (performLA.value) {
SectionDivider()
SectionItemView {
LockDelaySelector(remember { laLockDelay.state }) { laLockDelay.set(it) }
}
if (showChangePasscode.value && laMode.value == LAMode.PASSCODE) {
SectionDivider()
SectionItemView({ changeLAPassword() }) {
Text(generalGetString(R.string.la_change_app_passcode))
}
}
}
}
}
}
@Composable
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
SectionItemView {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
stringResource(R.string.enable_lock), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = performLA.value,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
)
)
}
}
}
@Composable
private fun LockModeSelector(state: State<LAMode>, onSelected: (LAMode) -> Unit) {
val values by remember { mutableStateOf(LAMode.values().map { it to it.text }) }
ExposedDropDownSettingRow(
generalGetString(R.string.lock_mode),
values,
state,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
@Composable
private fun LockDelaySelector(state: State<Int>, onSelected: (Int) -> Unit) {
val delays = remember { if (laDelays.contains(state.value)) laDelays else listOf(state.value) + laDelays }
val values by remember { mutableStateOf(delays.map { it to laDelayText(it) }) }
ExposedDropDownSettingRow(
generalGetString(R.string.lock_after),
values,
state,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
private fun laDelayText(t: Int): String {
val m = t / 60
val s = t % 60
return if (t == 0) {
generalGetString(R.string.la_immediately)
} else if (m == 0 || s != 0) {
// there are no options where both minutes and seconds are needed
generalGetString(R.string.la_seconds).format(s)
} else {
generalGetString(R.string.la_minutes).format(m)
}
}
private fun passcodeAlert(title: String) {
AlertManager.shared.showAlertMsg(
title = title,
text = generalGetString(R.string.la_please_remember_to_store_password)
)
}
@@ -32,12 +32,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Composable
fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
var testing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
SMPServerLayout(
ProtocolServerLayout(
testing,
server,
serverProtocol,
testServer = {
testing = true
scope.launch {
@@ -68,9 +69,10 @@ fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit
}
@Composable
private fun SMPServerLayout(
private fun ProtocolServerLayout(
testing: Boolean,
server: ServerCfg,
serverProtocol: ServerProtocol,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
@@ -86,7 +88,7 @@ private fun SMPServerLayout(
if (server.preset) {
PresetServer(testing, server, testServer, onUpdate, onDelete)
} else {
CustomServer(testing, server, testServer, onUpdate, onDelete)
CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete)
}
}
}
@@ -119,12 +121,19 @@ private fun PresetServer(
private fun CustomServer(
testing: Boolean,
server: ServerCfg,
serverProtocol: ServerProtocol,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
) {
val serverAddress = remember { mutableStateOf(server.server) }
val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } }
val valid = remember {
derivedStateOf {
with(parseServerAddress(serverAddress.value)) {
this?.valid == true && this.serverProtocol == serverProtocol
}
}
}
SectionView(
stringResource(R.string.smp_servers_your_server_address).uppercase(),
icon = Icons.Outlined.ErrorOutline,
@@ -187,9 +196,9 @@ fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
else -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Transparent)
}
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, SMPTestFailure?> =
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> =
try {
val r = m.controller.testSMPServer(server.server)
val r = m.controller.testProtoServer(server.server)
server.copy(tested = r == null) to r
} catch (e: Exception) {
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")
@@ -18,6 +18,7 @@ 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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -27,17 +28,19 @@ import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.launch
@Composable
fun SMPServersView(m: ChatModel, close: () -> Unit) {
fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember { mutableStateOf(emptyList<String>()) }
var servers by remember {
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
}
val currServers = remember { mutableStateOf(servers) }
val testing = rememberSaveable { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } }
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember {
derivedStateOf {
servers.isEmpty() ||
servers == m.userSMPServers.value ||
servers == currServers.value ||
testing.value ||
!servers.all { srv ->
val address = parseServerAddress(srv.server)
@@ -47,13 +50,25 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
}
}
LaunchedEffect(Unit) {
val res = m.controller.getUserProtoServers(serverProtocol)
if (res != null) {
currServers.value = res.protoServers
presetServers = res.presetServers
if (servers.isEmpty()) {
servers = currServers.value
}
}
}
fun showServer(server: ServerCfg) {
ModalManager.shared.showModalCloseable(true) { close ->
var old by remember { mutableStateOf(server) }
val index = servers.indexOf(old)
SMPServerView(
ProtocolServerView(
m,
old,
serverProtocol,
onUpdate = { updated ->
val newServers = ArrayList(servers)
newServers.removeAt(index)
@@ -75,11 +90,12 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
ModalView(
close = {
if (saveDisabled.value) close()
else showUnsavedChangesAlert({ saveSMPServers(servers, m, close) }, close)
else showUnsavedChangesAlert({ saveServers(serverProtocol, currServers, servers, m, close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
SMPServersLayout(
ProtocolServersLayout(
serverProtocol,
testing = testing.value,
servers = servers,
serversUnchanged = serversUnchanged.value,
@@ -97,12 +113,12 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
// No saving until something will be changed on the next screen to prevent blank servers on the list
showServer(servers.last())
}) {
Text(stringResource(R.string.smp_servers_enter_manually))
Text(stringResource(R.string.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.shared.showModalCloseable { close ->
ScanSMPServer {
ScanProtocolServer {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
@@ -110,15 +126,15 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
}
}
) {
Text(stringResource(R.string.smp_servers_scan_qr))
Text(stringResource(R.string.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
val hasAllPresets = hasAllPresets(servers, m)
val hasAllPresets = hasAllPresets(presetServers, servers, m)
if (!hasAllPresets) {
SectionItemView({
AlertManager.shared.hideAlert()
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
servers = (servers + addAllPresets(presetServers, servers, m)).sortedByDescending { it.preset }
}) {
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
Text(stringResource(R.string.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground)
}
}
}
@@ -134,11 +150,11 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
}
},
resetServers = {
servers = m.userSMPServers.value ?: emptyList()
servers = currServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveSMPServers(servers, m)
saveServers(serverProtocol, currServers, servers, m)
},
showServer = ::showServer,
)
@@ -161,7 +177,8 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
}
@Composable
private fun SMPServersLayout(
private fun ProtocolServersLayout(
serverProtocol: ServerProtocol,
testing: Boolean,
servers: List<ServerCfg>,
serversUnchanged: Boolean,
@@ -180,12 +197,12 @@ private fun SMPServersLayout(
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(R.string.your_SMP_servers))
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.your_SMP_servers else R.string.your_XFTP_servers))
SectionView(stringResource(R.string.smp_servers).uppercase()) {
SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.smp_servers else R.string.xftp_servers).uppercase()) {
for (srv in servers) {
SectionItemView({ showServer(srv) }, disabled = testing) {
SmpServerView(srv, servers, testing)
ProtocolServerView(serverProtocol, srv, servers, testing)
}
SectionDivider()
}
@@ -232,10 +249,10 @@ private fun SMPServersLayout(
}
@Composable
private fun SmpServerView(srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
val address = parseServerAddress(srv.server)
when {
address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer()
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
!srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight)
else -> ShowTestStatus(srv)
}
@@ -271,12 +288,12 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<Se
}
}
private fun hasAllPresets(servers: List<ServerCfg>, m: ChatModel): Boolean =
m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true
private fun hasAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): Boolean =
presetServers.all { hasPreset(it, servers) } ?: true
private fun addAllPresets(servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
private fun addAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
val toAdd = ArrayList<ServerCfg>()
for (srv in m.presetSMPServers.value ?: emptyList()) {
for (srv in presetServers) {
if (!hasPreset(srv, servers)) {
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
}
@@ -313,8 +330,8 @@ private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
return copy
}
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, SMPTestFailure> {
val fs: MutableMap<String, SMPTestFailure> = mutableMapOf()
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, ProtocolTestFailure> {
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
@@ -331,10 +348,10 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
return fs
}
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
private fun saveServers(protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
withApi {
if (m.controller.setUserSMPServers(servers)) {
m.userSMPServers.value = servers
if (m.controller.setUserProtoServers(protocol, servers)) {
currServers.value = servers
m.userSMPServersUnsaved.value = null
}
afterSave()
@@ -2,7 +2,6 @@ package chat.simplex.app.views.usersettings
import android.Manifest
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
@@ -17,16 +16,16 @@ import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanSMPServer(onNext: (ServerCfg) -> Unit) {
fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ScanSMPServerLayout(onNext)
ScanProtocolServerLayout(onNext)
}
@Composable
private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) {
private fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
Column(
Modifier
.fillMaxSize()
@@ -42,7 +42,7 @@ import chat.simplex.app.views.onboarding.SimpleXInfo
import chat.simplex.app.views.onboarding.WhatsNewView
@Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit) {
val user = chatModel.currentUser.value
val stopped = chatModel.chatRunning.value == false
@@ -57,7 +57,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
chatModel.chatDbEncrypted.value == true,
chatModel.incognito,
chatModel.controller.appPrefs.incognito,
developerTools = chatModel.controller.appPrefs.developerTools,
user.displayName,
setPerformLA = setPerformLA,
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
@@ -126,9 +125,8 @@ fun SettingsLayout(
encrypted: Boolean,
incognito: MutableState<Boolean>,
incognitoPref: SharedPreference<Boolean>,
developerTools: SharedPreference<Boolean>,
userDisplayName: String,
setPerformLA: (Boolean) -> Unit,
setPerformLA: (Boolean, FragmentActivity) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
@@ -176,7 +174,7 @@ fun SettingsLayout(
SectionDivider()
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped)
SectionDivider()
@@ -207,17 +205,8 @@ fun SettingsLayout(
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_develop)) {
val devTools = remember { mutableStateOf(developerTools.get()) }
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
SettingsActionItem(Icons.Outlined.Code, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
SectionDivider()
if (devTools.value) {
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(showVersion)
}
}
@@ -303,13 +292,20 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
)
}
@Composable fun ChatLockItem(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
SectionItemView() {
@Composable
fun ChatLockItem(
chatModel: ChatModel,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean, FragmentActivity) -> Unit
) {
val performLA = remember { chatModel.performLA }
val currentLAMode = remember { chatModel.controller.appPrefs.laMode }
SectionItemView(showSettingsModal { SimplexLockView(chatModel, currentLAMode, setPerformLA) }) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
Icons.Outlined.Lock,
if (performLA.value) Icons.Filled.Lock else Icons.Outlined.Lock,
contentDescription = stringResource(R.string.chat_lock),
tint = HighOrLowlight,
tint = if (performLA.value) SimplexGreen else HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
@@ -318,14 +314,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = performLA.value,
onCheckedChange = { setPerformLA(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
)
)
Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(androidx.compose.ui.R.string.off), color = HighOrLowlight)
}
}
}
@@ -370,7 +359,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) {
@Composable fun ChatConsoleItem(showTerminal: () -> Unit) {
SectionItemView(showTerminal) {
Icon(
painter = painterResource(id = R.drawable.ic_outline_terminal),
@@ -382,7 +371,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable private fun InstallTerminalAppItem(uriHandler: UriHandler) {
@Composable fun InstallTerminalAppItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
@@ -395,9 +384,11 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
@Composable private fun AppVersionItem(showVersion: () -> Unit) {
SectionItemView(showVersion) {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
SectionItemView(showVersion) { AppVersionText() }
}
@Composable fun AppVersionText() {
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) {
@@ -524,7 +515,7 @@ private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
generalGetString(R.string.auth_log_in_using_credential),
context as FragmentActivity,
completed = { laResult ->
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
}
)
}
@@ -544,9 +535,8 @@ fun PreviewSettingsLayout() {
encrypted = false,
incognito = remember { mutableStateOf(false) },
incognitoPref = SharedPreference({ false }, {}),
developerTools = SharedPreference({ false }, {}),
userDisplayName = "Alice",
setPerformLA = {},
setPerformLA = { _, _ -> },
showModal = { {} },
showSettingsModal = { {} },
showSettingsModalWithSearch = { },
@@ -1,14 +1,10 @@
package chat.simplex.app.views.usersettings
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
@@ -16,18 +12,20 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
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.ProfileNameField
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.launch
@@ -36,10 +34,8 @@ import kotlinx.coroutines.launch
fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
val user = chatModel.currentUser.value
if (user != null) {
val editProfile = rememberSaveable { mutableStateOf(false) }
var profile by remember { mutableStateOf(user.profile.toProfile()) }
UserProfileLayout(
editProfile = editProfile,
profile = profile,
close,
saveProfile = { displayName, fullName, image ->
@@ -49,7 +45,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
chatModel.updateCurrentUser(newProfile)
profile = newProfile
}
editProfile.value = false
close()
}
}
)
@@ -58,7 +54,6 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
@Composable
fun UserProfileLayout(
editProfile: MutableState<Boolean>,
profile: Profile,
close: () -> Unit,
saveProfile: (String, String, String?) -> Unit,
@@ -72,6 +67,7 @@ fun UserProfileLayout(
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
val focusRequester = remember { FocusRequester() }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
@@ -87,97 +83,89 @@ fun UserProfileLayout(
sheetState = bottomSheetModalState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) {
ModalView(close = close) {
val dataUnchanged =
displayName.value == profile.displayName &&
fullName.value == profile.fullName &&
chosenImage.value == null
val closeWithAlert = {
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) {
close()
} else {
showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close)
}
}
ModalView(close = closeWithAlert) {
Column(
Modifier
.verticalScroll(scrollState)
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
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),
color = MaterialTheme.colors.onBackground,
lineHeight = 22.sp
)
if (editProfile.value) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
AppBarTitleCentered(stringResource(R.string.your_current_profile))
ReadableText(generalGetString(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it), TextAlign.Center)
Column(
Modifier
.fillMaxWidth()
) {
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
contentAlignment = Alignment.Center
) {
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
contentAlignment = Alignment.Center
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(192.dp, profileImage.value)
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
DeleteImageButton { profileImage.value = null }
}
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f))
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
DeleteImageButton { profileImage.value = null }
}
}
Box {
if (!isValidDisplayName(displayName.value)) {
Icon(Icons.Outlined.Info, tint = Color.Red, contentDescription = stringResource(R.string.display_name_cannot_contain_whitespace))
}
ProfileNameTextField(displayName)
}
ProfileNameTextField(fullName)
Row {
TextButton(stringResource(R.string.cancel_verb)) {
displayName.value = profile.displayName
fullName.value = profile.fullName
profileImage.value = profile.image
editProfile.value = false
}
Spacer(Modifier.padding(horizontal = 8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val saveModifier: Modifier
val saveColor: Color
if (enabled) {
saveModifier = Modifier
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
saveColor = MaterialTheme.colors.primary
} else {
saveModifier = Modifier
saveColor = HighOrLowlight
}
}
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.display_name__field),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text(
stringResource(R.string.save_and_notify_contacts),
modifier = saveModifier,
color = saveColor
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
} else {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp), contentAlignment = Alignment.Center
) {
ProfileImage(192.dp, profile.image)
if (profile.image == null) {
EditImageButton {
editProfile.value = true
scope.launch { bottomSheetModalState.show() }
}
}
}
ProfileNameRow(stringResource(R.string.display_name__field), profile.displayName)
ProfileNameRow(stringResource(R.string.full_name__field), profile.fullName)
TextButton(stringResource(R.string.edit_verb)) { editProfile.value = true }
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.full_name__field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName)
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val saveModifier: Modifier
val saveColor: Color
if (enabled) {
saveModifier = Modifier
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
saveColor = MaterialTheme.colors.primary
} else {
saveModifier = Modifier
saveColor = HighOrLowlight
}
Text(
stringResource(R.string.save_and_notify_contacts),
modifier = saveModifier,
color = saveColor
)
}
Spacer(Modifier.height(DEFAULT_BOTTOM_BUTTON_PADDING))
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
@@ -192,60 +180,17 @@ fun UserProfileLayout(
}
}
@Composable
fun ProfileNameTextField(name: MutableState<String>) {
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = Modifier
.padding(bottom = 24.dp)
.padding(start = 28.dp)
.fillMaxWidth(),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true
)
}
@Composable
fun ProfileNameRow(label: String, text: String) {
Row(Modifier.padding(bottom = 24.dp)) {
Text(
label,
color = MaterialTheme.colors.onBackground
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
text,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onBackground
)
}
}
@Composable
fun TextButton(text: String, click: () -> Unit) {
Text(
text,
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable(onClick = click),
)
}
@Composable
fun EditImageButton(click: () -> Unit) {
IconButton(
onClick = click,
modifier = Modifier.background(Color(1f, 1f, 1f, 0.2f), shape = CircleShape)
modifier = Modifier.size(30.dp)
) {
Icon(
Icons.Outlined.PhotoCamera,
contentDescription = stringResource(R.string.edit_image),
tint = MaterialTheme.colors.primary,
modifier = Modifier.size(36.dp)
modifier = Modifier.size(30.dp)
)
}
}
@@ -261,6 +206,16 @@ fun DeleteImageButton(click: () -> Unit) {
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_contacts),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -273,7 +228,6 @@ fun PreviewUserProfileLayoutEditOff() {
UserProfileLayout(
profile = Profile.sampleData,
close = {},
editProfile = remember { mutableStateOf(false) },
saveProfile = { _, _, _ -> }
)
}
@@ -291,7 +245,6 @@ fun PreviewUserProfileLayoutEditOn() {
UserProfileLayout(
profile = Profile.sampleData,
close = {},
editProfile = remember { mutableStateOf(true) },
saveProfile = { _, _, _ -> }
)
}
@@ -2,9 +2,11 @@ package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionItemViewSpaceBetween
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
@@ -17,6 +19,7 @@ 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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.chatPasswordHash
@@ -24,10 +27,11 @@ 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.chatlist.UserProfileRow
import chat.simplex.app.views.database.PassphraseField
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.CreateProfile
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>) {
@@ -41,6 +45,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
searchTextOrPassword = searchTextOrPassword,
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
visibleUsersCount = visibleUsersCount(m),
prefPerformLA = m.controller.appPrefs.performLA.get(),
addUser = {
ModalManager.shared.showModalCloseable { close ->
CreateProfile(m, close)
@@ -48,7 +53,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
},
activateUser = { user ->
withBGApi {
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value))
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
}
},
removeUser = { user ->
@@ -67,16 +72,16 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, true, searchTextOrPassword.value)
removeUser(m, user, users, true, searchTextOrPassword.value.trim())
}) {
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
Text(stringResource(R.string.users_delete_with_connections), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, false, searchTextOrPassword.value)
removeUser(m, user, users, false, searchTextOrPassword.value.trim())
}
) {
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
Text(stringResource(R.string.users_delete_data_only), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
}
}
@@ -93,15 +98,28 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
}
},
unhideUser = { user ->
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, userViewPassword(user, searchTextOrPassword.value)) }
if (passwordEntryRequired(user, searchTextOrPassword.value)) {
ModalManager.shared.showModalCloseable(true) { close ->
ProfileActionView(UserProfileAction.UNHIDE, user) { pwd ->
withBGApi {
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, pwd) }
close()
}
}
}
} else {
withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, searchTextOrPassword.value.trim()) } }
}
},
muteUser = { user ->
setUserPrivacy(m, onSuccess = { if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert) }) {
m.controller.apiMuteUser(user.userId, userViewPassword(user, searchTextOrPassword.value))
withBGApi {
setUserPrivacy(m, onSuccess = {
if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert)
}) { m.controller.apiMuteUser(user.userId) }
}
},
unmuteUser = { user ->
setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId, userViewPassword(user, searchTextOrPassword.value)) }
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } }
},
showHiddenProfile = { user ->
ModalManager.shared.showModalCloseable(true) { close ->
@@ -125,6 +143,7 @@ private fun UserProfilesView(
searchTextOrPassword: MutableState<String>,
profileHidden: MutableState<Boolean>,
visibleUsersCount: Int,
prefPerformLA: Boolean,
showHiddenProfilesNotice: SharedPreference<Boolean>,
addUser: () -> Unit,
activateUser: (User) -> Unit,
@@ -153,10 +172,10 @@ private fun UserProfilesView(
SectionView {
for (user in filteredUsers) {
UserView(user, users, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
UserView(user, users, visibleUsersCount, prefPerformLA, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
SectionDivider()
}
if (searchTextOrPassword.value.isEmpty()) {
if (searchTextOrPassword.value.trim().isEmpty()) {
SectionItemView(addUser, minHeight = 68.dp) {
Icon(Icons.Outlined.Add, stringResource(R.string.users_add), tint = MaterialTheme.colors.primary)
Spacer(Modifier.padding(horizontal = 4.dp))
@@ -186,6 +205,7 @@ private fun UserView(
user: User,
users: List<User>,
visibleUsersCount: Int,
prefPerformLA: Boolean,
activateUser: (User) -> Unit,
removeUser: (User) -> Unit,
unhideUser: (User) -> Unit,
@@ -193,102 +213,167 @@ private fun UserView(
unmuteUser: (User) -> Unit,
showHiddenProfile: (User) -> Unit,
) {
var showDropdownMenu by remember { mutableStateOf(false) }
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
val showMenu = remember { mutableStateOf(false) }
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showMenu.value = true }) {
activateUser(user)
}
Box(Modifier.padding(horizontal = 16.dp)) {
DropdownMenu(
expanded = showDropdownMenu,
onDismissRequest = { showDropdownMenu = false },
Modifier.width(220.dp)
) {
DefaultDropdownMenu(showMenu) {
if (user.hidden) {
ItemAction(stringResource(R.string.user_unhide), Icons.Outlined.LockOpen, onClick = {
showDropdownMenu = false
showMenu.value = false
unhideUser(user)
})
} else {
if (visibleUsersCount > 1) {
if (visibleUsersCount > 1 && prefPerformLA) {
ItemAction(stringResource(R.string.user_hide), Icons.Outlined.Lock, onClick = {
showDropdownMenu = false
showMenu.value = false
showHiddenProfile(user)
})
}
if (user.showNtfs) {
ItemAction(stringResource(R.string.user_mute), Icons.Outlined.Notifications, onClick = {
showDropdownMenu = false
ItemAction(stringResource(R.string.user_mute), Icons.Outlined.NotificationsOff, onClick = {
showMenu.value = false
muteUser(user)
})
} else {
ItemAction(stringResource(R.string.user_unmute), Icons.Outlined.NotificationsOff, onClick = {
showDropdownMenu = false
ItemAction(stringResource(R.string.user_unmute), Icons.Outlined.Notifications, onClick = {
showMenu.value = false
unmuteUser(user)
})
}
}
ItemAction(stringResource(R.string.delete_verb), Icons.Outlined.Delete, color = Color.Red, onClick = {
removeUser(user)
showDropdownMenu = false
showMenu.value = false
})
}
}
}
enum class UserProfileAction {
DELETE,
UNHIDE
}
@Composable
private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (String) -> Unit) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_BOTTOM_PADDING),
) {
val actionPassword = rememberSaveable { mutableStateOf("") }
val passwordValid by remember { derivedStateOf { actionPassword.value == actionPassword.value.trim() } }
val actionEnabled by remember { derivedStateOf { actionPassword.value != "" && passwordValid && correctPassword(user, actionPassword.value) } }
@Composable fun ActionHeader(@StringRes title: Int) {
AppBarTitle(stringResource(title))
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
UserProfileRow(user)
}
SectionSpacer()
}
@Composable fun PasswordAndAction(@StringRes label: Int, color: Color = MaterialTheme.colors.primary) {
SectionView() {
SectionItemView {
PassphraseField(actionPassword, generalGetString(R.string.profile_password), isValid = { passwordValid }, showStrength = true)
}
SectionItemViewSpaceBetween({ doAction(actionPassword.value) }, disabled = !actionEnabled, minHeight = TextFieldDefaults.MinHeight) {
Text(generalGetString(label), color = if (actionEnabled) color else HighOrLowlight)
}
}
}
when (action) {
UserProfileAction.DELETE -> {
ActionHeader(R.string.delete_profile)
PasswordAndAction(R.string.delete_chat_profile, color = Color.Red)
if (actionEnabled) {
SectionTextFooter(stringResource(R.string.users_delete_all_chats_deleted))
}
}
UserProfileAction.UNHIDE -> {
ActionHeader(R.string.unhide_profile)
PasswordAndAction(R.string.unhide_chat_profile)
}
}
}
}
private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
val s = searchTextOrPassword.trim()
val lower = s.lowercase()
return m.users.filter { u ->
if ((u.user.activeUser || u.user.viewPwdHash == null) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) {
if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) {
true
} else if (u.user.viewPwdHash != null) {
s != "" && chatPasswordHash(s, u.user.viewPwdHash.salt) == u.user.viewPwdHash.hash
} else {
false
correctPassword(u.user, s)
}
}.map { it.user }
}
private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size
private fun correctPassword(user: User, pwd: String): Boolean {
val ph = user.viewPwdHash
return ph != null && pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
}
private fun userViewPassword(user: User, searchTextOrPassword: String): String? =
if (user.activeUser || !user.hidden) null else searchTextOrPassword
if (user.hidden) searchTextOrPassword.trim() else null
private fun passwordEntryRequired(user: User, searchTextOrPassword: String): Boolean =
user.hidden && user.activeUser && !correctPassword(user, searchTextOrPassword.trim())
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, searchTextOrPassword: String) {
if (users.size < 2) return
withBGApi {
suspend fun deleteUser(user: User) {
m.controller.apiDeleteUser(user.userId, delSMPQueues, userViewPassword(user, searchTextOrPassword))
m.removeUser(user)
}
try {
if (user.activeUser) {
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
if (newActive != null) {
m.controller.changeActiveUser_(newActive.userId, null)
deleteUser(user.copy(activeUser = false))
if (passwordEntryRequired(user, searchTextOrPassword)) {
ModalManager.shared.showModalCloseable(true) { close ->
ProfileActionView(UserProfileAction.DELETE, user) { pwd ->
withBGApi {
doRemoveUser(m, user, users, delSMPQueues, pwd)
close()
}
} else {
deleteUser(user)
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
}
} else {
withBGApi { doRemoveUser(m, user, users, delSMPQueues, userViewPassword(user, searchTextOrPassword.trim())) }
}
}
private fun setUserPrivacy(m: ChatModel, onSuccess: (() -> Unit)? = null, api: suspend () -> User) {
withBGApi {
try {
m.updateUser(api())
onSuccess?.invoke()
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.error_updating_user_privacy),
text = e.stackTraceToString()
)
private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, viewPwd: String?) {
if (users.size < 2) return
suspend fun deleteUser(user: User) {
m.controller.apiDeleteUser(user.userId, delSMPQueues, viewPwd)
m.removeUser(user)
}
try {
if (user.activeUser) {
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
if (newActive != null) {
m.controller.changeActiveUser_(newActive.userId, null)
deleteUser(user.copy(activeUser = false))
}
} else {
deleteUser(user)
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
}
}
private suspend fun setUserPrivacy(m: ChatModel, onSuccess: (() -> Unit)? = null, api: suspend () -> User) {
try {
m.updateUser(api())
onSuccess?.invoke()
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.error_updating_user_privacy),
text = e.stackTraceToString()
)
}
}
@@ -302,4 +387,4 @@ private fun showMuteProfileAlert(showMuteProfileAlert: SharedPreference<Boolean>
showMuteProfileAlert.set(false)
},
)
}
}
@@ -23,7 +23,6 @@ fun VersionInfoView(info: CoreVersionInfo) {
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,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M181.5,835q-22.97,0 -40.23,-17.27Q124,800.47 124,777.5L124,182q0,-22.97 17.27,-40.23Q158.53,124.5 181.5,124.5L561,124.5q12.25,0 20.63,8.46T590,153.18q0,12.32 -8.38,20.58T561,182L181.5,182v595.5L777,777.5L777,399q0,-12.25 8.43,-20.63 8.43,-8.38 20.5,-8.38 12.07,0 20.33,8.38T834.5,399v378.5q0,22.97 -17.27,40.23Q799.97,835 777,835L181.5,835ZM727.83,341.5q-12.32,0 -20.58,-8.38T699,312.5L699,261h-51.5q-12.25,0 -20.63,-8.43 -8.38,-8.43 -8.38,-20.5 0,-12.07 8.38,-20.33t20.63,-8.25L699,203.5v-52q0,-11.68 8.43,-20.09 8.43,-8.41 20.5,-8.41 12.07,0 20.33,8.41 8.25,8.41 8.25,20.09v52h52q11.68,0 20.09,8.46Q837,220.43 837,232.17q0,12.32 -8.41,20.58Q820.17,261 808.5,261h-52v51.5q0,12.25 -8.46,20.63t-20.21,8.38ZM273,676.5h413.17q9.82,0 13.82,-7.75T698,653L585.58,503.6q-4.7,-6.1 -11.46,-6.1 -6.76,0 -11.61,6L448,653.5l-81.46,-106.39q-4.69,-5.61 -11.5,-5.61 -6.81,0 -11.72,5.58L261.57,653.02q-5.07,7.98 -0.95,15.73T273,676.5ZM181.5,399v378.5L181.5,182v217Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M480.03,697Q551,697 602.75,645.94t51.75,-122.5Q654.5,452 602.72,400q-51.78,-52 -122.75,-52Q408,348 356.75,400.06t-51.25,123.5Q305.5,595 356.78,646q51.28,51 123.25,51ZM435.5,482.5l31,-71.42Q470,402.5 480,403t13.5,9.08l30.16,70.42 65.92,27.58q9.42,4.16 9.42,13.79t-9.42,13.05L523.5,564l-30,69.92Q490,643 480,643.5t-13.5,-8.58L435.5,564l-65.58,-27.08Q360,533.5 360,523.87t9.92,-13.79L435.5,482.5ZM142.5,835.5q-22.97,0 -40.23,-17.27Q85,800.97 85,778L85,268.5q0,-21.97 17.27,-39.73Q119.53,211 142.5,211h147l54.91,-66.5q7.59,-10 19.11,-15 11.52,-5 24.98,-5h183q13.47,0 24.98,5 11.52,5 19.52,15l54.5,66.5h147q21.97,0 39.73,17.77Q875,246.53 875,268.5L875,778q0,22.97 -17.77,40.23Q839.47,835.5 817.5,835.5h-675ZM817.5,778L817.5,268.5h-675L142.5,778h675ZM480,522.5Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M255.69,838.85q-22.26,0 -38.46,-16.2 -16.2,-16.2 -16.2,-38.44L201.04,175.79q0,-22.24 16.2,-38.44 16.2,-16.2 38.61,-16.2h333.61l169.5,169v493.89q0,22.41 -16.2,38.61 -16.2,16.2 -38.46,16.2L255.69,838.85ZM574.42,304.23L574.42,151.35L255.85,151.35q-9.23,0 -16.92,7.69 -7.69,7.69 -7.69,16.92v608.08q0,9.23 7.69,16.92 7.69,7.69 16.92,7.69h448.31q9.23,0 16.92,-7.69 7.69,-7.69 7.69,-16.92L728.77,304.23L574.42,304.23ZM231.23,151.35v152.88,-152.88 657.31,-657.31Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M451.5,595v99q0,12.25 8.43,20.63 8.43,8.38 20.5,8.38 12.07,0 20.33,-8.38T509,694v-99h100.5q11.68,0 20.09,-8.43 8.41,-8.43 8.41,-20.5 0,-12.07 -8.41,-20.33 -8.41,-8.25 -20.09,-8.25L509,537.5L509,437q0,-11.68 -8.46,-20.09 -8.46,-8.41 -20.21,-8.41 -12.32,0 -20.58,8.41 -8.25,8.41 -8.25,20.09v100.5h-100q-12.25,0 -20.63,8.46t-8.38,20.21q0,12.32 8.38,20.58T351.5,595h100ZM222,875q-22.97,0 -40.23,-17.27Q164.5,840.47 164.5,817.5v-675q0,-22.97 17.27,-40.23Q199.03,85 222,85h335q11.91,0 22.71,4.75 10.79,4.75 18.91,12.34l179.26,179.31Q786,289.5 790.75,300.29q4.75,10.8 4.75,22.71v494.5q0,22.97 -17.27,40.23Q760.97,875 738,875L222,875ZM552.5,296L552.5,142.5L222,142.5v675h516L738,325L581.5,325q-12.25,0 -20.63,-8.38T552.5,296ZM222,142.5L222,325 222,142.5v675,-675Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="m436.5,615.5 l175.5,-114q13,-8.5 13,-23.51T612,454L436.5,340q-14.5,-9.5 -29,-1.27Q393,346.96 393,364v227.5q0,17.76 14.5,25.88 14.5,8.12 29,-1.88ZM142.5,795.5q-22.97,0 -40.23,-17.27Q85,760.97 85,738L85,222q0,-22.97 17.27,-40.23Q119.53,164.5 142.5,164.5h675q23.72,0 40.61,17.27Q875,199.03 875,222v516q0,22.97 -16.89,40.23Q841.22,795.5 817.5,795.5h-675ZM142.5,738L142.5,222v516ZM142.5,738h675L817.5,222h-675v516Z"/>
</vector>
@@ -26,4 +26,25 @@
<string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
<string name="switch_receiving_address_desc">هذه الميزة تجريبية! ستعمل فقط إذا كان لدى العميل الآخر الإصدار 4.2 مثبتًا. يجب أن ترى الرسالة في المحادثة بمجرد اكتمال تغيير العنوان - يرجى التحقق من أنه لا يزال بإمكانك تلقي الرسائل من جهة الاتصال هذه (أو عضو المجموعة).</string>
<string name="this_link_is_not_a_valid_connection_link">هذا الارتباط ليس ارتباط اتصال صالح!</string>
<string name="allow_verb">يسمح</string>
<string name="smp_servers_preset_add">أضف خوادم محددة مسبقًا</string>
<string name="smp_servers_add_to_another_device">أضف إلى جهاز آخر</string>
<string name="users_delete_all_chats_deleted">سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string>
<string name="network_enable_socks_info">الوصول إلى الخوادم عبر بروكسي SOCKS على المنفذ 9050؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار.</string>
<string name="accept_requests">قبول طلبات</string>
<string name="smp_servers_add">إضافة خادم …</string>
<string name="network_settings">إعدادات الشبكة المتقدمة</string>
<string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string>
<string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة الاتصال الخاصة بك بذلك.</string>
<string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك.</string>
<string name="group_member_role_admin">مسؤل</string>
<string name="users_add">إضافة ملف التعريف</string>
<string name="allow_direct_messages">السماح بإرسال رسائل مباشرة إلى الأعضاء.</string>
<string name="accept_contact_incognito_button">قبول التخفي</string>
<string name="button_add_welcome_message">أضف رسالة ترحيب</string>
<string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string>
<string name="v4_2_group_links_desc">يمكن للمسؤولين إنشاء روابط للانضمام إلى المجموعات.</string>
<string name="accept_connection_request__question">قبول طلب الاتصال؟</string>
<string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string>
<string name="callstatus_accepted">مكالمة مقبولة</string>
</resources>
@@ -11,7 +11,7 @@
<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 mizící zprávy.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Povolte svým kontaktům odesílat mizí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>
@@ -93,7 +93,7 @@
<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řipoje</string>
<string name="server_connecting">připojová</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>
@@ -101,7 +101,7 @@
<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="display_name_connecting">připojování…</string>
<string name="description_you_shared_one_time_link">sdíleli jste jednorázový odkaz</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>
@@ -186,9 +186,11 @@
<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="use_camera_button">Fotoaparát</string>
<string name="from_gallery_button">Z Galerie</string>
<string name="choose_file">Vybrat soubor</string>
<string name="choose_file">Soubor</string>
<string name="gallery_image_button">Obrázek</string>
<string name="gallery_video_button">Video</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">potom:</string>
@@ -277,7 +279,7 @@
<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_messages">ZPRÁVY A SOUBORY</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>
@@ -655,14 +657,11 @@
<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: %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.
\n
\n<xliff:g id="appName">SimpleX</xliff:g> servery váš profil vidět nemohou.</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 vidět nemohou.</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>
@@ -685,7 +684,7 @@
<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="callstate_connecting">připojování…</string>
<string name="privacy_redefined">Nové vymezení soukromí</string>
<string name="first_platform_without_user_ids">1. platforma bez jakýchkoliv uživatelských identifikátorů soukromá již od návrhu.</string>
<string name="immune_to_spam_and_abuse">Odolná vůči spamu a zneužití</string>
@@ -726,11 +725,9 @@
<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ž:
\n1. Zprávy na serveru vyprší, pokud nebyly přijaty po dobu 30 dnů,
\n2. Server, který používáte pro příjem zpráv od tohoto kontaktu, byl aktualizován a restartován.
\n3. Spojení je narušeno.
\nPřipojte se k vývojářům prostřednictvím Nastavení, abyste mohli dostávat aktualizace o serverech.
\nBudeme přidávat redundantní servery, abychom zabránili ztrátě zpráv.</string>
\n1. Zprávy vypršely v odesílajícím klientovi po 2 dnech nebo na serveru po 30 dnech.
\n2. Dešifrování zprávy se nezdařilo, protože vy nebo váš kontakt jste použili starou zálohu databáze.
\n3. Spojení je kompromitováno.</string>
<string name="settings_section_title_you">VY</string>
<string name="settings_section_title_support">PODPOŘIT SIMPLEX CHAT</string>
<string name="settings_section_title_develop">VÝVOJ</string>
@@ -854,7 +851,7 @@
<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="num_contacts_selected">%d 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>
@@ -908,7 +905,7 @@
<string name="theme">Téma</string>
<string name="chat_preferences_contact_allows">Kontakt povolil</string>
<string name="chat_preferences_on">zapnuto</string>
<string name="chat_preferences_off">vypnuto</string>
<string name="chat_preferences_off">vypnout</string>
<string name="chat_preferences">Chat předvolby</string>
<string name="contact_preferences">Předvolby kontaktu</string>
<string name="group_preferences">Předvolby skupiny</string>
@@ -963,7 +960,6 @@
<string name="your_contact_address">Vaše adresa</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Váš chat profil bude odeslán
\nvašemu kontaktu</string>
<string name="your_chat_profiles_stored_locally">Vaše chat profily jsou uloženy lokálně, pouze ve vašem zařízení.</string>
<string name="your_chats">Vaše konverzace</string>
<string name="paste_connection_link_below_to_connect">Do níže uvedeného pole vložte odkaz, který jste obdrželi pro spojení s kontaktem.</string>
<string name="share_invitation_link">Sdílet pozvánku</string>
@@ -988,7 +984,7 @@
<string name="v4_6_chinese_spanish_interface">Čínské a Španělské rozhranní</string>
<string name="v4_6_audio_video_calls">Hlasové a video hovory</string>
<string name="confirm_password">Potvrdit heslo</string>
<string name="enter_password_to_show">Pro zobrazení zadejte heslo výše!</string>
<string name="enter_password_to_show">Zadejte heslo do hledání</string>
<string name="v4_6_reduced_battery_usage">Další snížení spotřeby baterie</string>
<string name="error_saving_user_password">Chyba ukládání hesla uživatele</string>
<string name="error_updating_user_privacy">Chyba aktualizace soukromí uživatele</string>
@@ -1018,4 +1014,129 @@
<string name="button_welcome_message">Uvítací zpráva</string>
<string name="group_welcome_title">Uvítací zpráva</string>
<string name="user_unmute">Zrušit ztlumení</string>
<string name="to_reveal_profile_enter_password">Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce Chat profily.</string>
<string name="should_be_at_least_one_visible_profile">Měl by tam být alespoň jeden viditelný uživatelský profil.</string>
<string name="you_will_still_receive_calls_and_ntfs">Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní.</string>
<string name="you_can_hide_or_mute_user_profile">Můžete skrýt nebo ztlumit uživatelský profil - Podržte pro menu.</string>
<string name="user_unhide">Odkrýt</string>
<string name="database_upgrade">Aktualizace databáze</string>
<string name="database_downgrade_warning">Upozornění: můžete ztratit nějaká data!</string>
<string name="confirm_database_upgrades">Potvrdit aktualizaci databáze</string>
<string name="database_downgrade">Původní databáze</string>
<string name="mtr_error_no_down_migration">verze databáze je novější než aplikace, ale žádný přechod dolů pro: %s</string>
<string name="downgrade_and_open_chat">Snížit a otevřít chat</string>
<string name="database_migrations">Migrací: %s</string>
<string name="mtr_error_different">různé migrace v aplikaci/databázi: %s / %s</string>
<string name="incompatible_database_version">Nekompatibilní verze databáze</string>
<string name="invalid_migration_confirmation">Neplatné potvrzení migrace</string>
<string name="upgrade_and_open_chat">Zvýšit a otevřít chat</string>
<string name="hide_dev_options">Skrýt:</string>
<string name="show_developer_options">Zobrazit možnosti vývojáře</string>
<string name="settings_section_title_experimenta">POKUSNÝ</string>
<string name="image_will_be_received_when_contact_completes_uploading">Obrázek bude přijat, až kontakt dokončí jeho nahrání.</string>
<string name="show_dev_options">Zobrazit:</string>
<string name="developer_options">ID databáze a možnost Izolace přenosu.</string>
<string name="file_will_be_received_when_contact_completes_uploading">Soubor bude přijat, jakmile váš kontakt dokončí nahrávání.</string>
<string name="file_transfer_will_be_cancelled_warning">Přenos souboru bude zrušen. Pokud probíhá, bude zastaven.</string>
<string name="delete_chat_profile">Smazat chat profil</string>
<string name="delete_profile">Smazat profil</string>
<string name="profile_password">Heslo profilu</string>
<string name="unhide_chat_profile">Odkrýt chat profil</string>
<string name="unhide_profile">Odkrýt profil</string>
<string name="cancel_file__question">Zrušit přenos souboru\?</string>
<string name="icon_descr_video_asked_to_receive">Žádost o přijetí videa</string>
<string name="videos_limit_desc">Současně lze odeslat pouze 10 videí</string>
<string name="videos_limit_title">Příliš mnoho videí!</string>
<string name="video_descr">Video</string>
<string name="icon_descr_waiting_for_video">Čekám na video</string>
<string name="icon_descr_video_snd_complete">Video odesláno</string>
<string name="video_will_be_received_when_contact_completes_uploading">Video bude přijato, až kontakt dokončí jeho nahrávání.</string>
<string name="video_will_be_received_when_contact_is_online">Video obdržíte, až bude váš kontakt online, vyčkejte prosím nebo zkontrolujte později!</string>
<string name="waiting_for_video">Čekám na video</string>
<string name="error_loading_smp_servers">Chyba načítání serverů SMP</string>
<string name="error_loading_xftp_servers">Chyba načítání serverů XFTP</string>
<string name="error_saving_xftp_servers">Chyba ukládání XFTP serverů</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy XFTP serverů jsou ve správném formátu s oddělenými řádky a nejsou duplicitní.</string>
<string name="error_xftp_test_server_auth">Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo</string>
<string name="smp_server_test_compare_file">Porovnat soubor</string>
<string name="smp_server_test_create_file">Vytvořit soubor</string>
<string name="smp_server_test_delete_file">Smazat soubor</string>
<string name="smp_server_test_download_file">Stáhnout soubor</string>
<string name="smp_server_test_upload_file">Nahrát soubor</string>
<string name="xftp_servers">XFTP servery</string>
<string name="network_socks_toggle_use_socks_proxy">Použít SOCKS proxy</string>
<string name="host_verb">Host</string>
<string name="network_proxy_port">port %d</string>
<string name="network_socks_proxy_settings">Nastavení SOCKS proxy</string>
<string name="la_lock_mode_passcode">Zadat heslo</string>
<string name="la_lock_mode">SimpleX zámek</string>
<string name="la_lock_mode_system">Ověření systému</string>
<string name="la_authenticate">Ověřit</string>
<string name="la_auth_failed">Ověření selhalo</string>
<string name="la_change_app_passcode">Změnit heslo</string>
<string name="la_current_app_passcode">Aktuální heslo</string>
<string name="la_enter_app_passcode">Zadat heslo</string>
<string name="la_no_app_password">Bez hesla aplikace</string>
<string name="la_could_not_be_verified">Nemohli jste být ověřeni; Zkuste to prosím znovu.</string>
<string name="la_minutes">%d minut</string>
<string name="la_seconds">%d vteřin</string>
<string name="la_immediately">Ihned</string>
<string name="la_please_remember_to_store_password">Zapamatujte si jej nebo bezpečně uložte - neexistuje způsob, jak obnovit ztracené heslo!</string>
<string name="lock_not_enabled">Zámek SimpleX není povolen!</string>
<string name="you_can_turn_on_lock">Zámek SimpleX můžete zapnout v Nastavení.</string>
<string name="port_verb">Port</string>
<string name="disable_onion_hosts_when_not_supported">Nastavte <i>Použít .onion hostitele</i> na Ne, pokud je SOCKS proxy nepodporuje.</string>
<string name="your_XFTP_servers">Vaše XFTP servery</string>
<string name="enable_lock">Povolit zámek</string>
<string name="lock_after">Zamknout po</string>
<string name="lock_mode">Režim zámku</string>
<string name="authentication_cancelled">Ověření zrušeno</string>
<string name="confirm_passcode">Potvrdit heslo</string>
<string name="incorrect_passcode">Nesprávné heslo</string>
<string name="new_passcode">Nové heslo</string>
<string name="submit_passcode">Odeslat</string>
<string name="la_mode_system">Systém</string>
<string name="change_lock_mode">Změnit zamykání</string>
<string name="la_mode_passcode">Heslo</string>
<string name="passcode_changed">Heslo změněno!</string>
<string name="passcode_not_changed">Heslo nezměněno!</string>
<string name="passcode_set">Heslo nastaveno!</string>
<string name="decryption_error">Chyba dešifrování</string>
<string name="alert_title_msg_bad_hash">Špatný hash zprávy</string>
<string name="alert_title_msg_bad_id">Špatné ID zprávy</string>
<string name="alert_text_msg_bad_hash">Hash předchozí zprávy se liší.</string>
<string name="alert_text_msg_bad_id">ID další zprávy je nesprávné (menší nebo rovno předchozí).
\nMůže se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitováno.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> zprávy se nepodařilo dešifrovat.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> zprývy přeskočeny.</string>
<string name="alert_text_fragment_please_report_to_developers">Nahlaste to prosím vývojářům.</string>
<string name="alert_text_fragment_permanent_error_reconnect">Tato chyba je pro toto připojení trvalá, připojte se znovu.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Může se to stát, když vy nebo vaše připojení použijete starou zálohu databáze.</string>
<string name="no_spaces">Žádné mezery!</string>
<string name="revoke_file__message">Soubor bude smazán ze serverů.</string>
<string name="stop_rcv_file__message">Příjem souboru bude zastaven.</string>
<string name="allow_calls_only_if">Povolte hovory, pouze pokud je váš kontakt povolí.</string>
<string name="allow_your_contacts_to_call">Povolte svým kontaktům vám volat.</string>
<string name="audio_video_calls">Audio/video hovory</string>
<string name="available_in_v51">"
\nDostupné ve verzi 5.1"</string>
<string name="both_you_and_your_contact_can_make_calls">Volat můžete vy i váš kontakt.</string>
<string name="only_you_can_make_calls">Volat můžete pouze vy.</string>
<string name="prohibit_calls">Zákaz audio/video hovorů.</string>
<string name="calls_prohibited_with_this_contact">Audio/video hovory jsou zakázány.</string>
<string name="only_your_contact_can_make_calls">Volat může pouze váš kontakt.</string>
<string name="v5_0_app_passcode">Heslo aplikace</string>
<string name="v5_0_large_files_support_descr">Rychle a bez čekání, než bude odesílatel online!</string>
<string name="v5_0_polish_interface">Polské rozhraní</string>
<string name="v5_0_app_passcode_descr">Nastavte jej namísto ověřování systému.</string>
<string name="v5_0_large_files_support">Videa a soubory až do velikosti 1 gb</string>
<string name="v5_0_polish_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
<string name="revoke_file__title">Odvolat soubor\?</string>
<string name="revoke_file__confirm">Odvolat</string>
<string name="revoke_file__action">Odvolat soubor</string>
<string name="stop_snd_file__message">Odesílání souboru bude zastaveno.</string>
<string name="stop_file__confirm">Zastavit</string>
<string name="stop_file__action">Zastavit soubor</string>
<string name="stop_snd_file__title">Zastavit odesílání souboru\?</string>
<string name="stop_rcv_file__title">Zastavit příjem souboru\?</string>
</resources>
@@ -180,7 +180,7 @@
<string name="you_have_no_chats">Sie haben keine Chats</string>
<!-- ShareListView.kt -->
<string name="share_message">Nachricht teilen…</string>
<string name="share_image">Bild teilen…</string>
<string name="share_image">Medien teilen…</string>
<string name="share_file">Datei teilen…</string>
<!-- ComposeView.kt, helpers -->
<string name="attach">Anhängen</string>
@@ -254,9 +254,11 @@
<string name="only_stored_on_members_devices">(Wird nur von Gruppenmitgliedern gespeichert)</string>
<!-- GetImageView -->
<string name="toast_permission_denied">Berechtigung verweigert!</string>
<string name="use_camera_button">Kamera\nverwenden</string>
<string name="use_camera_button">Kamera</string>
<string name="from_gallery_button">Aus dem\nFotoalbum</string>
<string name="choose_file">Datei\nauswählen</string>
<string name="choose_file">Datei</string>
<string name="gallery_image_button">Bild</string>
<string name="gallery_video_button">Video</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Danke, dass Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> installiert haben!</string>
<string name="you_can_connect_to_simplex_chat_founder">Sie können sich <font color="#0088ff">mit <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.</string>
@@ -428,7 +430,7 @@
<string name="display_name__field">Angezeigter Name:</string>
<string name="full_name__field">"Vollständiger Name:</string>
<string name="your_current_profile">Mein aktuelles Chat-Profil</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. <xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
<string name="edit_image">Bild bearbeiten</string>
<string name="delete_image">Bild löschen</string>
<string name="save_preferences_question">Präferenzen speichern?</string>
@@ -555,7 +557,10 @@
<string name="integrity_msg_bad_id">Falsche Nachrichten-ID</string>
<string name="integrity_msg_duplicate">Doppelte Nachricht</string>
<string name="alert_title_skipped_messages">Übersprungene Nachrichten</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Dies kann unter folgenden Umständen passieren:\n1. Die Nachrichten verfallen auf dem Server, wenn sie 30 Tage lang nicht empfangen wurden.\n2. Der Server, den Sie zum Empfangen der Nachrichten von diesem Kontakt verwenden, wurde aktualisiert und neu gestartet.\n3. Die Verbindung ist kompromittiert.\nBitte nehmen Sie über die Einstellungen Verbindung mit den Entwicklern auf, um Updates zu den Servern zu erhalten.\nWir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu verhindern.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Dies kann unter folgenden Umständen passieren:
\n1. Die Nachrichten verfallen auf dem sendenden Client-System nach 2 Tagen oder auf dem Server nach 30 Tagen.
\n2. Die Nachrichten-Entschlüsselung ist fehlgeschlagen, da von Ihnen oder Ihrem Kontakt ein altes Datenbank-Backup genutzt wurde.
\n3. Die Verbindung wurde kompromittiert.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Datenschutz &amp; Sicherheit</string>
<string name="your_privacy">Meine Privatsphäre</string>
@@ -576,7 +581,7 @@
<string name="settings_section_title_socks">SOCKS-PROXY</string>
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">DESIGN</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_messages">NACHRICHTEN und DATEIEN</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_incognito">Inkognito Modus</string>
<!-- DatabaseView.kt -->
@@ -765,7 +770,7 @@
<string name="select_contacts">Kontakte auswählen</string>
<string name="icon_descr_contact_checked">Kontakt geprüft</string>
<string name="clear_contacts_selection_button">Löschen</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> Kontakt(e) ausgewählt</string>
<string name="num_contacts_selected">%d Kontakt(e) ausgewählt</string>
<string name="no_contacts_selected">Keine Kontakte ausgewählt</string>
<string name="invite_prohibited">Kontakt kann nicht eingeladen werden!</string>
<string name="invite_prohibited_description">Sie versuchen, einen Kontakt, mit dem Sie ein Inkognito-Profil geteilt haben, in die Gruppe einzuladen, in der Sie Ihr Hauptprofil verwenden.</string>
@@ -1000,7 +1005,6 @@
<string name="app_version_code">App Build: %s</string>
<string name="app_version_title">App Version</string>
<string name="app_version_name">App Version: v%s</string>
<string name="core_build_timestamp">Core übersetzt am: %s</string>
<string name="core_version">Core Version: v%s</string>
<string name="users_add">Profil hinzufügen</string>
<string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string>
@@ -1023,7 +1027,6 @@
<string name="users_delete_data_only">Nur lokale Profildaten</string>
<string name="users_delete_with_connections">Profil und Serververbindungen</string>
<string name="messages_section_description">Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil</string>
<string name="your_chat_profiles_stored_locally">Ihre Chat-Profile werden nur lokal auf Ihrem Endgerät gespeichert</string>
<string name="failed_to_create_user_duplicate_title">Doppelter Anzeigename!</string>
<string name="failed_to_create_user_title">Fehler beim Erstellen des Profils!</string>
<string name="failed_to_active_user_title">Fehler beim Umschalten des Profils!</string>
@@ -1055,15 +1058,15 @@
<string name="observer_cant_send_message_desc">Bitte kontaktieren Sie den Gruppen-Administrator.</string>
<string name="moderate_message_will_be_deleted_warning">Diese Nachricht wird für alle Gruppenmitglieder gelöscht.</string>
<string name="language_system">System</string>
<string name="confirm_password">Bestätigen Sie das Passwort</string>
<string name="confirm_password">Passwort bestätigen</string>
<string name="cant_delete_user_profile">Das Benutzerprofil kann nicht gelöscht werden!</string>
<string name="dont_show_again">Nicht nochmals anzeigen</string>
<string name="v4_6_chinese_spanish_interface">Chinesische und spanische Bedienoberfläche</string>
<string name="v4_6_audio_video_calls">Audio- und Videoanrufe</string>
<string name="button_add_welcome_message">Fügen Sie eine Begrüßungsmeldung hinzu</string>
<string name="button_add_welcome_message">Begrüßungsmeldung hinzufügen</string>
<string name="error_updating_user_privacy">Fehler beim Aktualisieren der Benutzer-Privatsphäre</string>
<string name="smp_save_servers_question">Alle Server speichern\?</string>
<string name="hide_profile">Verberge das Profil</string>
<string name="hide_profile">Profil verbergen</string>
<string name="password_to_show">Passwort anzeigen</string>
<string name="save_profile_password">Profil-Passwort speichern</string>
<string name="error_saving_user_password">Fehler beim Speichern des Benutzer-Passworts</string>
@@ -1071,10 +1074,10 @@
<string name="button_welcome_message">Begrüßungsmeldung</string>
<string name="save_welcome_message_question">Begrüßungsmeldung speichern\?</string>
<string name="user_unhide">Verbergen aufheben</string>
<string name="enter_password_to_show">Geben Sie oben das Passwort für die Anzeige an!</string>
<string name="make_profile_private">Erzeugen Sie ein privates Profil!</string>
<string name="enter_password_to_show">Für die Anzeige das Passwort im Suchfeld eingeben</string>
<string name="make_profile_private">Privates Profil erzeugen!</string>
<string name="user_mute">Stummschalten</string>
<string name="tap_to_activate_profile">Tippen Sie, um das Profil zu aktivieren.</string>
<string name="tap_to_activate_profile">Tippen Sie auf das Profil um es zu aktivieren.</string>
<string name="should_be_at_least_one_profile">Es muss mindestens ein Benutzer-Profil vorhanden sein.</string>
<string name="should_be_at_least_one_visible_profile">Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein.</string>
<string name="user_unmute">Stummschaltung aufheben</string>
@@ -1087,14 +1090,135 @@
<string name="v4_6_group_welcome_message">Gruppen-Begrüßungsmeldung</string>
<string name="v4_6_reduced_battery_usage">Weiter reduzierter Batterieverbrauch</string>
<string name="v4_6_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string>
<string name="v4_6_group_welcome_message_descr">Legen Sie die Nachricht fest, die neuen Mitgliedern angezeigt werden soll!</string>
<string name="v4_6_group_welcome_message_descr">Definieren Sie eine Begrüßungsmeldung, die neuen Mitgliedern angezeigt wird!</string>
<string name="v4_6_chinese_spanish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="v4_6_group_moderation">Gruppenmoderation</string>
<string name="v4_6_hidden_chat_profiles">Verborgene Chat-Profile</string>
<string name="user_hide">Verberge</string>
<string name="save_and_update_group_profile">Sichern und aktualisieren des Gruppen-Profils</string>
<string name="save_and_update_group_profile">Gruppen-Profil sichern und aktualisieren</string>
<string name="you_will_still_receive_calls_and_ntfs">Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</string>
<string name="group_welcome_title">Begrüßungsmeldung</string>
<string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten - für das Menü gedrückt halten.</string>
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Meine Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string>
<string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string>
<string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string>
<string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string>
<string name="show_dev_options">Anzeigen:</string>
<string name="show_developer_options">Entwickleroptionen anzeigen</string>
<string name="settings_section_title_experimenta">EXPERIMENTELL</string>
<string name="database_upgrade">Datenbank-Aktualisierung</string>
<string name="mtr_error_different">Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s</string>
<string name="downgrade_and_open_chat">Datenbank herabstufen und den Chat öffnen</string>
<string name="incompatible_database_version">Inkompatible Datenbank-Version</string>
<string name="database_downgrade_warning">Warnung: Sie könnten einige Daten verlieren!</string>
<string name="database_downgrade">Datenbank auf alte Version herabstufen</string>
<string name="developer_options">Datenbank-IDs und Transport-Isolationsoption.</string>
<string name="mtr_error_no_down_migration">Die Datenbank-Version ist neuer als die App, keine Abwärts-Migration für: %s</string>
<string name="hide_dev_options">Verberge:</string>
<string name="database_migrations">Migrationen: %s</string>
<string name="image_will_be_received_when_contact_completes_uploading">Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string>
<string name="file_will_be_received_when_contact_completes_uploading">Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string>
<string name="cancel_file__question">Dateitransfer abbrechen\?</string>
<string name="file_transfer_will_be_cancelled_warning">Der Dateitransfer wird abgebrochen. Falls er gerade abläuft, wird er angehalten.</string>
<string name="delete_chat_profile">Chat-Profil löschen</string>
<string name="delete_profile">Profil löschen</string>
<string name="unhide_profile">Verbergen des Profils aufheben</string>
<string name="profile_password">Passwort für Profil</string>
<string name="unhide_chat_profile">Verbergen des Chat-Profils aufheben</string>
<string name="icon_descr_video_asked_to_receive">Aufforderung zum Empfang des Videos</string>
<string name="videos_limit_desc">Es können nur 10 Videos zur gleichen Zeit versendet werden</string>
<string name="videos_limit_title">Zu viele Videos auf einmal!</string>
<string name="video_descr">Video</string>
<string name="icon_descr_video_snd_complete">Video gesendet</string>
<string name="video_will_be_received_when_contact_completes_uploading">Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat.</string>
<string name="icon_descr_waiting_for_video">Auf das Video warten</string>
<string name="waiting_for_video">Auf das Video warten</string>
<string name="video_will_be_received_when_contact_is_online">Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</string>
<string name="your_XFTP_servers">Ihre XFTP-Server</string>
<string name="host_verb">Host</string>
<string name="error_saving_xftp_servers">Fehler beim Speichern der XFTP-Server</string>
<string name="error_loading_smp_servers">Fehler beim Laden der SMP-Server</string>
<string name="error_xftp_test_server_auth">Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung</string>
<string name="smp_server_test_download_file">Datei herunterladen</string>
<string name="smp_server_test_compare_file">Datei vergleichen</string>
<string name="smp_server_test_delete_file">Datei löschen</string>
<string name="lock_mode">Sperr-Modus</string>
<string name="authentication_cancelled">Authentifizierung abgebrochen</string>
<string name="confirm_passcode">Passwort bestätigen</string>
<string name="enable_lock">Sperre aktivieren</string>
<string name="incorrect_passcode">Passwort falsch</string>
<string name="lock_after">Sperre nach</string>
<string name="new_passcode">Neues Passwort</string>
<string name="la_mode_passcode">Passwort</string>
<string name="passcode_changed">Passwort geändert!</string>
<string name="passcode_set">Passwort wurde geändert!</string>
<string name="change_lock_mode">Sperr-Modus ändern</string>
<string name="passcode_not_changed">Passwort wurde nicht geändert!</string>
<string name="decryption_error">Entschlüsselungsfehler</string>
<string name="decryption_error_permanent">Dauerhafter Entschlüsselungsfehler</string>
<string name="error_loading_xftp_servers">Fehler beim Laden der XFTP-Server</string>
<string name="la_lock_mode_passcode">Passworteingabe</string>
<string name="la_enter_app_passcode">Passwort eingeben</string>
<string name="la_no_app_password">Kein App-Passwort</string>
<string name="la_authenticate">Authentifizieren</string>
<string name="la_minutes">%d Minuten</string>
<string name="la_seconds">%d Sekunden</string>
<string name="la_immediately">Sofort</string>
<string name="port_verb">Port</string>
<string name="network_proxy_port">Port %d</string>
<string name="smp_server_test_create_file">Datei erstellen</string>
<string name="la_current_app_passcode">Aktuelles Passwort</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Dies kann passieren, falls Sie oder Ihre Verbindung ein altes Datenbank-Backup genutzt haben.</string>
<string name="la_please_remember_to_store_password">Es gibt keine Möglichkeit ein vergessenes Passwort wiederherzustellen - bitte erinnern Sie sich gut daran oder speichern Sie es sicher ab!</string>
<string name="alert_text_fragment_please_report_to_developers">Bitte melden Sie es den Entwicklern.</string>
<string name="la_change_app_passcode">Passwort ändern</string>
<string name="alert_title_msg_bad_hash">Ungültiger Nachrichten-Hash</string>
<string name="la_auth_failed">Authentifizierung fehlgeschlagen</string>
<string name="alert_title_msg_bad_id">Falsche Nachrichten-ID</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string>
<string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string>
<string name="la_lock_mode">SimpleX Sperr-Modus</string>
<string name="lock_not_enabled">SimpleX Sperre ist nicht aktiviert!</string>
<string name="disable_onion_hosts_when_not_supported">Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.</string>
<string name="submit_passcode">Übermitteln</string>
<string name="la_mode_system">System</string>
<string name="la_could_not_be_verified">Sie können nicht überprüft werden - bitte versuchen Sie es nochmal.</string>
<string name="xftp_servers">XFTP-Server</string>
<string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der Vorherigen).
\nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> Nachrichten konnten nicht entschlüsselt werden.</string>
<string name="alert_text_msg_bad_hash">Der Hash der vorherigen Nachricht ist unterschiedlich.</string>
<string name="you_can_turn_on_lock">Sie können die SimpleX Sperre über die Einstellungen aktivieren.</string>
<string name="network_socks_proxy_settings">SOCKS-Proxy Einstellungen</string>
<string name="la_lock_mode_system">System-Authentifizierung</string>
<string name="alert_text_fragment_permanent_error_reconnect">Es handelt sich um einen permanenten Fehler für diese Verbindung - bitte verbinden Sie sich neu.</string>
<string name="smp_server_test_upload_file">Datei hochladen</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> Nachrichten übersprungen.</string>
<string name="allow_calls_only_if">Anrufe sind nur erlaubt, wenn Ihr Kontakt das ebenfalls erlaubt.</string>
<string name="allow_your_contacts_to_call">Erlaubt Ihren Kontakten Sie anzurufen.</string>
<string name="audio_video_calls">Audio/Video Anrufe</string>
<string name="available_in_v51">"
\nVerfügbar in v5.1"</string>
<string name="both_you_and_your_contact_can_make_calls">Sowohl Sie, als auch Ihr Kontakt können Anrufe tätigen.</string>
<string name="only_you_can_make_calls">Nur Sie können Anrufe tätigen.</string>
<string name="prohibit_calls">Audio/Video Anrufe nicht erlauben.</string>
<string name="stop_rcv_file__title">Den Empfang der Datei beenden\?</string>
<string name="stop_snd_file__title">Das Senden der Datei beenden\?</string>
<string name="stop_rcv_file__message">Der Empfang der Datei wird beendet.</string>
<string name="stop_snd_file__message">Das Senden der Datei wird beendet.</string>
<string name="stop_file__action">Datei beenden</string>
<string name="revoke_file__message">Die Datei wird von den Servern gelöscht.</string>
<string name="no_spaces">Keine Leerzeichen!</string>
<string name="revoke_file__confirm">Widerrufen</string>
<string name="revoke_file__action">Datei widerrufen</string>
<string name="revoke_file__title">Datei widerrufen\?</string>
<string name="stop_file__confirm">Beenden</string>
<string name="v5_0_large_files_support">Videos und Dateien bis zu 1GB</string>
<string name="v5_0_large_files_support_descr">Schnell und ohne warten auf den Absender, bis er online ist!</string>
<string name="v5_0_polish_interface">Polnische Bedienoberfläche</string>
<string name="v5_0_app_passcode_descr">Anstelle der System-Authentifizierung festlegen.</string>
<string name="v5_0_polish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="only_your_contact_can_make_calls">Nur Ihr Kontakt kann Anrufe tätigen.</string>
<string name="v5_0_app_passcode">App Passwort</string>
<string name="calls_prohibited_with_this_contact">Audio/Video Anrufe sind nicht erlaubt.</string>
</resources>
@@ -10,20 +10,20 @@
<string name="chat_item_ttl_day">un dia</string>
<string name="chat_item_ttl_month">un mes</string>
<string name="chat_item_ttl_week">una semana</string>
<string name="allow_disappearing_messages_only_if">Permitir mensajes temporales sólo si tu contacto los permite.</string>
<string name="allow_disappearing_messages_only_if">Se permiten mensajes temporales sólo si tu contacto también los permite.</string>
<string name="v4_3_improved_server_configuration_desc">Añadir servidores escaneando códigos QR.</string>
<string name="smp_servers_preset_add">Añadir servidores predefinidos</string>
<string name="all_group_members_will_remain_connected">Todos los miembros del grupo permanecerán conectados.</string>
<string name="allow_irreversible_message_deletion_only_if">Permitir la eliminación irreversible de mensajes sólo si tu contacto también lo permite.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la frase de contraseña después de reiniciar la aplicación o cambiar la frase de contraseña - permitirá recibir notificaciones.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Permitir a tus contactos enviar mensajes temporales</string>
<string name="allow_your_contacts_to_send_voice_messages">Permitir a tus contactos enviar mensajes de voz.</string>
<string name="allow_irreversible_message_deletion_only_if">Se permite la eliminación irreversible de mensajes sólo si tu contacto también lo permite para tí.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la contraseña después de reiniciar la aplicación o cambiar la frase de contraseña - permitirá recibir notificaciones.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Permites a tus contactos enviar mensajes temporales</string>
<string name="allow_your_contacts_to_send_voice_messages">Permites a tus contactos enviar mensajes de voz.</string>
<string name="chat_preferences_always">siempre</string>
<string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando, no se iniciará ningún servicio en segundo plano.</string>
<string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano.</string>
<string name="settings_section_title_icon">ICONO DE LA APLICACIÓN</string>
<string name="incognito_random_profile_from_contact_description">Se enviará un perfil aleatorio al contacto del que recibió este enlace</string>
<string name="turning_off_service_and_periodic">La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración.</string>
<string name="notifications_mode_service_desc">El servicio en segundo plano está siempre en funcionamiento las notificaciones se muestran en cuanto los mensajes estén disponibles.</string>
<string name="notifications_mode_service_desc">El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Se puede desactivar en Configuración</b> las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.</string>
<string name="notifications_mode_service">Siempre activo</string>
<string name="allow_verb">Permitir</string>
@@ -47,7 +47,7 @@
<string name="accept">Aceptar</string>
<string name="audio_call_no_encryption">llamada de audio (sin cifrado e2e)</string>
<string name="icon_descr_audio_call">llamada de audio</string>
<string name="settings_audio_video_calls">Llamadas de audio y vídeo</string>
<string name="settings_audio_video_calls">Llamadas y Videollamadas</string>
<string name="icon_descr_audio_off">Audio desactivado</string>
<string name="icon_descr_audio_on">Audio activado</string>
<string name="integrity_msg_bad_id">ID de mensaje erróneo</string>
@@ -55,12 +55,12 @@
<string name="users_delete_all_chats_deleted">Se eliminarán todos los chats y mensajes. ¡No puede deshacerse!</string>
<string name="accept_feature">Aceptar</string>
<string name="allow_to_send_disappearing">Permitir enviar mensajes temporales.</string>
<string name="keychain_is_storing_securely">Android Keystore se utiliza para almacenar de forma segura la frase de contraseña - permite que el servicio de notificación funcione.</string>
<string name="keychain_is_storing_securely">Android Keystore se utiliza para almacenar de forma segura la contraseña - permite que el servicio de notificación funcione.</string>
<string name="users_add">Añadir perfil</string>
<string name="incognito_random_profile_description">Se enviará un perfil aleatorio a tu contacto</string>
<string name="color_primary">Acento</string>
<string name="allow_your_contacts_irreversibly_delete">Permitir a tus contactos eliminar irreversiblemente los mensajes enviados.</string>
<string name="allow_voice_messages_only_if">Permitir mensajes de voz sólo si tu contacto los permite.</string>
<string name="color_primary">Color</string>
<string name="allow_your_contacts_irreversibly_delete">Permites a tus contactos eliminar irreversiblemente los mensajes enviados.</string>
<string name="allow_voice_messages_only_if">Se permiten mensajes de voz sólo si tu contacto también los permite.</string>
<string name="allow_direct_messages">Permitir el envío de mensajes directos a los miembros.</string>
<string name="allow_to_delete_messages">Permitir la eliminación irreversible de los mensajes enviados.</string>
<string name="allow_to_send_voice">Permitir enviar mensajes de voz.</string>
@@ -70,7 +70,7 @@
<string name="integrity_msg_bad_hash">hash de mensaje erróneo</string>
<string name="answer_call">Responder llamada</string>
<string name="group_member_role_admin">administrador</string>
<string name="allow_voice_messages_question">¿Permitir mensajes de voz\?</string>
<string name="allow_voice_messages_question">¿Permites los mensajes de voz\?</string>
<string name="back">Volver</string>
<string name="about_simplex_chat">Sobre <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="smp_servers_add_to_another_device">Añadir a otro dispositivo</string>
@@ -85,31 +85,31 @@
<string name="both_you_and_your_contact_can_send_disappearing">Tanto tú como tu contacto podéis enviar mensajes temporales.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Escanear código QR</b>: para conectar con tu contacto que te muestre código QR.</string>
<string name="create_profile_button">Crear</string>
<string name="create_one_time_link">Crear enlace de invitación de un uso.</string>
<string name="create_one_time_link">Crear enlace de invitación de un solo uso.</string>
<string name="create_group">Crear grupo secreto</string>
<string name="database_passphrase_will_be_updated">La contraseña de cifrado de la base de datos será actualizada.</string>
<string name="info_row_database_id">ID de la base de datos</string>
<string name="direct_messages_are_prohibited_in_chat">Los mensajes directos entre miembros del grupo están prohibidos.</string>
<string name="direct_messages_are_prohibited_in_chat">Los mensajes directos entre miembros del grupo no están permitidos.</string>
<string name="passphrase_is_different">La contraseña es distinta a la almacenada en Keystore</string>
<string name="database_will_be_encrypted_and_passphrase_stored">La base de datos será cifrada y la contraseña se guardará en Keystore.</string>
<string name="delete_contact_question">¿Eliminar contacto\?</string>
<string name="delete_message__question">Eliminar mensaje\?</string>
<string name="delete_message__question">¿Eliminar mensaje\?</string>
<string name="delete_chat_profile_question">¿Eliminar el perfil de chat\?</string>
<string name="rcv_group_event_group_deleted">grupo eliminado</string>
<string name="delete_group_question">¿Eliminar grupo\?</string>
<string name="delete_messages_after">Eliminar mensaje después</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar SimpleX Lock en Configuración, después de activar la autenticación de dispositivo.</string>
<string name="delete_messages_after">Eliminar después de</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo.</string>
<string name="no_call_on_lock_screen">Desactivar</string>
<string name="disappearing_prohibited_in_this_chat">Los mensajes temporales están prohibidos en este chat.</string>
<string name="disappearing_messages_are_prohibited">Los mensajes temporales están prohibidos en este grupo.</string>
<string name="disappearing_prohibited_in_this_chat">Los mensajes temporales no están permitidos en este chat.</string>
<string name="disappearing_messages_are_prohibited">Los mensajes temporales no están permitidos en este grupo.</string>
<string name="display_name_cannot_contain_whitespace">El nombre mostrado no puede contener espacios en blanco.</string>
<string name="encrypted_video_call">Videollamada con cifrado e2e</string>
<string name="display_name_connection_established">conexión establecida</string>
<string name="simplex_link_mode_description">Descripción</string>
<string name="smp_server_test_connect">Conectar</string>
<string name="notification_contact_connected">Conectado</string>
<string name="auth_disable_simplex_lock">Desactivar SimpleX Lock</string>
<string name="auth_device_authentication_is_disabled_turning_off">Autenticación de dispositivo desactivada. SimpleX Lock deshabilitado.</string>
<string name="auth_disable_simplex_lock">Desactivar Bloqueo SimpleX</string>
<string name="auth_device_authentication_is_disabled_turning_off">Autenticación de dispositivo desactivada. Bloqueo SimpleX deshabilitado.</string>
<string name="maximum_supported_file_size">El tamaño máximo de archivo admitido es <xliff:g id="maxFileSize">%1$s</xliff:g></string>
<string name="clear_verification">Eliminar verificación</string>
<string name="create_profile">Crear perfil</string>
@@ -118,7 +118,7 @@
<string name="integrity_msg_duplicate">mensaje duplicado</string>
<string name="settings_section_title_develop">DESARROLLO</string>
<string name="settings_developer_tools">Herramientas desarrollo</string>
<string name="delete_files_and_media_for_all_users">Eliminar archivos para todos los perfiles de chat</string>
<string name="delete_files_and_media_for_all_users">Eliminar archivos para todos los perfiles Chat</string>
<string name="delete_messages">Eliminar mensaje</string>
<string name="database_encrypted">¡Base de datos cifrada!</string>
<string name="encrypted_with_random_passphrase">La base de datos está cifrada con una contraseña aleatoria, puedes cambiarla.</string>
@@ -158,7 +158,7 @@
<string name="enable_automatic_deletion_question">¿Activar eliminación automática de mensajes\?</string>
<string name="contact_preferences">Preferencias de contacto</string>
<string name="ttl_s">%ds</string>
<string name="delete_after">Eliminar después</string>
<string name="delete_after">Eliminar después de</string>
<string name="ttl_sec">%d seg</string>
<string name="contact_already_exists">El contácto ya existe</string>
<string name="connection_error_auth">Error de conexión (Autenticación)</string>
@@ -166,7 +166,7 @@
<string name="icon_descr_server_status_disconnected">Desconectado</string>
<string name="icon_descr_server_status_connected">Conectado</string>
<string name="copied">Copiado en portapapeles</string>
<string name="share_one_time_link">Crear enlace de invitación de un uso.</string>
<string name="share_one_time_link">Crear enlace de invitación de un solo uso.</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 PC: escanéa el código QR desde la app mediante <b>Escanéo de código QR </b></string>
<string name="delete_contact_menu_action">Eliminar</string>
<string name="delete_group_menu_action">Eliminar</string>
@@ -231,7 +231,7 @@
<string name="chat_preferences_default">predefinido (%s)</string>
<string name="full_deletion">Eliminar para todos</string>
<string name="feature_enabled">activado</string>
<string name="contacts_can_mark_messages_for_deletion">El contacto puede marcar los mensajes para eliminar; tu podrás verlos.</string>
<string name="contacts_can_mark_messages_for_deletion">El contacto solo puede marcar los mensajes para eliminar. Tu podrás verlos.</string>
<string name="ttl_w">%ds</string>
<string name="deleted_description">eliminado</string>
<string name="connect_via_contact_link">¿Conectar mediante enlace de contacto\?</string>
@@ -250,20 +250,20 @@
<string name="icon_descr_email">Email</string>
<string name="connect_button">Conectar</string>
<string name="connect_via_link">Conectar mediante enlace</string>
<string name="database_passphrase_and_export">Contraseña y exportar la base de datos</string>
<string name="database_passphrase_and_export">Base de Datos y
\nContraseña</string>
<string name="contribute">Contribuye</string>
<string name="core_build_timestamp">Core compilado: %s</string>
<string name="core_version">Core versión: v%s</string>
<string name="contact_requests">Solicitud del contacto</string>
<string name="delete_image">Eliminar imagen</string>
<string name="edit_image">Editar imagen</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="change_verb">Cambiar</string>
<string name="notifications_mode_periodic_desc">Comprueba mensajes nuevos de hasta un minuto cada 10 minutos</string>
<string name="notifications_mode_periodic_desc">Se realizan comprobaciones de mensajes nuevos periódicas de hasta un minuto de duración cada 10 minutos</string>
<string name="clear_contacts_selection_button">Limpiar</string>
<string name="change_member_role_question">¿Cambiar rol de grupo\?</string>
<string name="v4_4_verify_connection_security_desc">Compara los códigos de seguridad con tus contactos</string>
<string name="choose_file">Elije archivo</string>
<string name="choose_file">Archivo</string>
<string name="clear_verb">Limpiar</string>
<string name="clear_chat_button">Limpiar chat</string>
<string name="configure_ICE_servers">Configurar servidores ICE</string>
@@ -280,14 +280,14 @@
<string name="database_initialization_error_title">No se puede iniciar la base de datos</string>
<string name="clear_chat_question">Limpiar chat\?</string>
<string name="network_session_mode_user">Perfil de Chat</string>
<string name="chat_is_stopped_indication">El chat está detenido</string>
<string name="chat_is_stopped_indication">Chat está detenido</string>
<string name="rcv_group_event_changed_member_role">rol de %s cambiado a %s</string>
<string name="change_role">Cambiar rol</string>
<string name="v4_5_transport_isolation_descr">Mediante perfil de Chat (por defecto) o por conexión (BETA)</string>
<string name="snd_conn_event_switch_queue_phase_changing">cambiando dirección…</string>
<string name="chat_preferences">Preferencias de chat</string>
<string name="chat_preferences">Preferencias de Chat</string>
<string name="feature_cancelled_item">cancelado %s</string>
<string name="chat_is_stopped">El chat está detenido</string>
<string name="chat_is_stopped">Chat está detenido</string>
<string name="settings_section_title_calls">LLAMADAS</string>
<string name="chat_is_running">El chat está en ejecución</string>
<string name="rcv_conn_event_switch_queue_phase_changing">cambiando dirección…</string>
@@ -328,7 +328,7 @@
<string name="group_invitation_expired">Invitación de grupo caducada</string>
<string name="alert_message_group_invitation_expired">La invitación al grupo ya no es válida, ha sido eliminada por el remitente.</string>
<string name="delete_group_for_self_cannot_undo_warning">El grupo se eliminará para tí. ¡No puede deshacerse!</string>
<string name="how_to_use_markdown">Cómo usar el marcador</string>
<string name="how_to_use_markdown">Cómo usar sintaxis markdown</string>
<string name="description_via_one_time_link_incognito">Incógnito mediante enlace de un uso</string>
<string name="simplex_link_contact">Dirección de contacto SimpleX</string>
<string name="error_saving_smp_servers">Error guardando servidores SMP</string>
@@ -336,7 +336,7 @@
<string name="error_setting_network_config">Error al actualizar la configuración de red</string>
<string name="error_creating_address">Error creando dirección</string>
<string name="error_deleting_user">Error eliminando perfil de usuario</string>
<string name="auth_enable_simplex_lock">Activar SimpleX Lock</string>
<string name="auth_enable_simplex_lock">Activar Bloqueo SimpleX</string>
<string name="one_time_link">Enlace de invitación de un uso</string>
<string name="smp_servers">Servidores SMP</string>
<string name="settings_experimental_features">Características experimentales</string>
@@ -346,7 +346,7 @@
<string name="failed_to_active_user_title">¡Error cambiando perfil!</string>
<string name="smp_servers_enter_manually">Introduce el servidor manualmente</string>
<string name="how_to_use_your_servers">Cómo usar tus servidores</string>
<string name="error_stopping_chat">Error deteniendo el chat</string>
<string name="error_stopping_chat">Error deteniendo Chat</string>
<string name="enter_correct_passphrase">Introduce la contraseña correcta.</string>
<string name="enter_passphrase">Introduce la contraseña…</string>
<string name="icon_descr_group_inactive">Grupo inactivo</string>
@@ -359,6 +359,8 @@
<string name="error_saving_file">Error al guardar archivo</string>
<string name="icon_descr_server_status_error">Error</string>
<string name="from_gallery_button">De la Galería</string>
<string name="gallery_image_button">Imagen</string>
<string name="gallery_video_button">Vídeo</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si has recibido el enlace de invitación a <xliff:g id="appName">SimpleX Chat</xliff:g>, puedes abrirlo en tu navegador:</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si eliges rechazar, el remitente NO será notificado.</string>
<string name="invalid_contact_link">¡Enlace no válido!</string>
@@ -383,8 +385,8 @@
<string name="error_deleting_group">Error al eliminar grupo</string>
<string name="error_deleting_pending_contact_connection">Error al eliminar conexión de contacto pendiente</string>
<string name="hide_notification">Ocultar</string>
<string name="notification_preview_mode_hidden">Oculto</string>
<string name="notification_display_mode_hidden_desc">Ocultar contacto y mensaje</string>
<string name="notification_preview_mode_hidden">Oculta</string>
<string name="notification_display_mode_hidden_desc">Se ocultan tanto el contacto como el mensaje</string>
<string name="hide_verb">Ocultar</string>
<string name="for_everybody">Para todos</string>
<string name="icon_descr_file">Archivo</string>
@@ -406,7 +408,7 @@
<string name="v4_4_french_interface">Interfaz en francés</string>
<string name="image_descr">Imagen</string>
<string name="file_not_found">Archivo no encontrado</string>
<string name="how_to_use_simplex_chat">Cómo usar</string>
<string name="how_to_use_simplex_chat">Guía de uso</string>
<string name="full_name_optional__prompt">Nombre completo (opcional)</string>
<string name="callstate_ended">finalizado</string>
<string name="settings_section_title_help">AYUDA</string>
@@ -419,7 +421,7 @@
<string name="error_changing_role">Error cambiando rol</string>
<string name="conn_stats_section_title_servers">SERVIDORES</string>
<string name="group_display_name_field">Nombre mostrado del grupo:</string>
<string name="group_preferences">Preferencias del grupo</string>
<string name="group_preferences">Preferencias de grupo</string>
<string name="group_members_can_send_dms">Los miembros del grupo pueden enviar mensajes directos.</string>
<string name="group_members_can_delete">Los miembros del grupo pueden eliminar mensajes de forma irreversible.</string>
<string name="v4_3_improved_privacy_and_security_desc">Ocultar pantalla de aplicaciones en aplicaciones recientes.</string>
@@ -444,16 +446,16 @@
<string name="share_link">Compartir enlace</string>
<string name="how_it_works">Cómo funciona</string>
<string name="delete_message_cannot_be_undone_warning">El mensaje se eliminará. ¡No puede deshacerse!</string>
<string name="incognito_info_protects">El modo incógnito protege la identidad del perfil principal, por cada contacto nuevo se genera un perfil nuevo aleatorio.</string>
<string name="incognito_info_protects">La función del modo incógnito es proteger la identidad del perfil principal: por cada contacto nuevo se genera un perfil aleatorio.</string>
<string name="turn_off_battery_optimization">Para poder usarse <b>deshabilita la optimización de batería</b> para <xliff:g id="appName">SimpleX</xliff:g> en el siguiente cuadro de diálogo. De lo contrario las notificaciones estarán desactivadas.</string>
<string name="install_simplex_chat_for_terminal">Instalar <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para terminal</string>
<string name="group_invitation_item_description">invitación al grupo <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="rcv_group_event_member_added">invitado <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="incognito_info_allows">Permite tener varias conexiones anónimas sin datos compartidos entre estas en un único perfil de chat.</string>
<string name="incognito_info_allows">Permite tener varias conexiones anónimas sin datos compartidos entre estas dentro del mismo perfil.</string>
<string name="invite_to_group_button">Invitar al grupo</string>
<string name="to_verify_compare">Para comprobar el cifrado de extremo a extremo con su contacto compare (o escanee) el código en sus dispositivos.</string>
<string name="database_is_not_encrypted">La base de datos no está cifrada. Establece una contraseña para protegerla.</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no duplicadas.</string>
<string name="database_is_not_encrypted">La base de datos no está cifrada. Escribe una contraseña para protegerla.</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string>
<string name="icon_descr_instant_notifications">Notificación instantánea</string>
<string name="network_settings_title">Configuración de red</string>
<string name="network_use_onion_hosts_no_desc_in_alert">No se usarán hosts .onion</string>
@@ -465,15 +467,13 @@
<string name="conn_level_desc_indirect">indirecto (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="theme_light">Claro</string>
<string name="chat_preferences_on">Activado</string>
<string name="message_deletion_prohibited">La eliminación irreversible de mensajes está prohibida en este chat.</string>
<string name="message_deletion_prohibited_in_chat">La eliminación irreversible de mensajes está prohibida en este grupo.</string>
<string name="message_deletion_prohibited">La eliminación irreversible de mensajes no está permitida en este chat.</string>
<string name="message_deletion_prohibited_in_chat">La eliminación irreversible de mensajes no está permitida en este grupo.</string>
<string name="v4_3_improved_server_configuration">Configuración del servidor mejorada</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Esto puede suceder cuando:
\n1. Los mensajes caducan en el servidor si no se han recibido durante 30 días.
\n2. El servidor que utiliza para recibir los mensajes de este contacto fue actualizado y reiniciado.
\n3. La conexión está comprometida.
\nPor favor, contacta con los desarrolladores a través del menú Configuración para recibir actualizaciones sobre los servidores.
\nAñadiremos redundancia de servidores para evitar la pérdida de mensajes.</string>
\n1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días.
\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.
\n3. La conexión ha sido comprometida.</string>
<string name="notification_preview_mode_message">Texto del mensaje</string>
<string name="member_info_section_title_member">MIEMBRO</string>
<string name="chat_item_ttl_none">nunca</string>
@@ -492,7 +492,7 @@
<string name="mark_unread">Marcar como no leído</string>
<string name="invalid_QR_code">Código QR inválido</string>
<string name="incorrect_code">¡Código de seguridad incorrecto!</string>
<string name="markdown_in_messages">Marcadores en mensajes</string>
<string name="markdown_in_messages">Sintaxis markdown en mensajes</string>
<string name="network_use_onion_hosts_no">No</string>
<string name="callstatus_missed">llamada perdida</string>
<string name="import_database_confirmation">Importar</string>
@@ -506,7 +506,7 @@
<string name="invalid_data">datos no válidos</string>
<string name="invalid_message_format">formato de mensaje no válido</string>
<string name="live">EN VIVO</string>
<string name="moderated_description">moderado</string>
<string name="moderated_description">eliminado por el moderador</string>
<string name="display_name_invited_to_connect">invitado a conectarse</string>
<string name="service_notifications_disabled">¡Las notificaciones instantáneas están desactivadas!</string>
<string name="notification_preview_new_message">mensaje nuevo</string>
@@ -528,7 +528,7 @@
<string name="snd_group_event_user_left">has salido</string>
<string name="snd_conn_event_switch_queue_phase_completed">has cambiado la dirección</string>
<string name="feature_off">apagado</string>
<string name="v4_3_irreversible_message_deletion">Eliminación del mensaje irreversible</string>
<string name="v4_3_irreversible_message_deletion">Eliminación irreversible del mensaje</string>
<string name="v4_3_voice_messages_desc">Máximo 40 segundos, recibido al instante.</string>
<string name="v4_5_italian_interface">Interfaz en italiano</string>
<string name="v4_5_message_draft">Borrador de mensaje</string>
@@ -538,15 +538,15 @@
<string name="no_details">sin detalles</string>
<string name="ok">OK</string>
<string name="only_stored_on_members_devices">(sólo almacenado por miembros del grupo)</string>
<string name="markdown_help">Ayuda marcadores</string>
<string name="network_and_servers">Redes y servidores</string>
<string name="markdown_help">Ayuda sintaxis markdown</string>
<string name="network_and_servers">Redes y Servidores</string>
<string name="network_use_onion_hosts_prefer_desc">Se usarán hosts .onion cuando estén disponibles.</string>
<string name="italic">cursiva</string>
<string name="incoming_audio_call">Llamada entrante</string>
<string name="video_call_no_encryption">videollamada (sin cifrado e2e)</string>
<string name="status_no_e2e_encryption">sin cifrado e2e</string>
<string name="import_database">Importar base de datos</string>
<string name="settings_section_title_messages">MENSAJES</string>
<string name="settings_section_title_messages">MENSAJES Y ARCHIVOS</string>
<string name="import_database_question">¿Importar base de datos\?</string>
<string name="no_received_app_files">Sin archivos recibidos o enviados</string>
<string name="messages_section_title">Mensajes</string>
@@ -574,7 +574,7 @@
<string name="snd_conn_event_switch_queue_phase_completed_for_member">has cambiado la dirección por %s</string>
<string name="rcv_group_event_member_left">ha salido</string>
<string name="button_leave_group">Salir del grupo</string>
<string name="only_group_owners_can_change_prefs">Sólo los propietarios del grupo pueden cambiar las preferencias de grupo.</string>
<string name="only_group_owners_can_change_prefs">Sólo los propietarios del grupo pueden modificar las preferencias de grupo.</string>
<string name="users_delete_data_only">Sólo datos del perfil local</string>
<string name="chat_preferences_no">no</string>
<string name="thousand_abbreviation">k</string>
@@ -602,8 +602,8 @@
<string name="reset_color">Restablecer colores</string>
<string name="only_you_can_send_disappearing">Sólo tú puedes enviar mensajes temporales</string>
<string name="only_your_contact_can_send_disappearing">Sólo tu contacto puede enviar mensajes temporales.</string>
<string name="prohibit_sending_voice">Prohibir el envío de mensajes de voz.</string>
<string name="notifications_mode_off">Se ejecuta cuando la aplicación está abierta</string>
<string name="prohibit_sending_voice">Prohibes el envío de mensajes de voz.</string>
<string name="notifications_mode_off">Se ejecuta sólo cuando la aplicación está abierta</string>
<string name="auth_open_chat_console">Abrir la consola de chat</string>
<string name="save_verb">Guardar</string>
<string name="restore_database_alert_confirm">Restaurar</string>
@@ -626,7 +626,7 @@
<string name="restore_database_alert_title">¿Restaurar copia de seguridad de la base de datos\?</string>
<string name="onboarding_notifications_mode_title">Notificaciones privadas</string>
<string name="image_descr_profile_image">imagen del perfil</string>
<string name="prohibit_sending_voice_messages">Prohibir el envío de mensajes de voz.</string>
<string name="prohibit_sending_voice_messages">Prohibes el envío de mensajes de voz.</string>
<string name="protect_app_screen">Proteger la pantalla de la aplicación</string>
<string name="read_more_in_github_with_link">Más información en nuestro <font color="#0088ff">repositorio GitHub</font> .</string>
<string name="icon_descr_record_voice_message">Grabar mensaje de voz</string>
@@ -635,7 +635,7 @@
<string name="send_live_message_desc">Envía un mensaje en vivo: se actualizará para el(los) destinatario(s) a medida que se escribe</string>
<string name="icon_descr_sent_msg_status_send_failed">error de envío</string>
<string name="sending_via">Enviando mediante</string>
<string name="contact_developers">Actualiza la aplicación y ponte en contacto con los desarrolladores.</string>
<string name="contact_developers">Por favor, actualiza la aplicación y ponte en contacto con los desarrolladores.</string>
<string name="sender_cancelled_file_transfer">El remitente ha cancelado la transferencia de archivos.</string>
<string name="smp_server_test_secure_queue">Cola segura</string>
<string name="enter_passphrase_notification_title">Se necesita contraseña</string>
@@ -651,7 +651,7 @@
<string name="network_options_revert">Revertir</string>
<string name="network_option_ping_interval">Intervalo PING</string>
<string name="network_option_ping_count">Contador PING</string>
<string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcar para eliminar).</string>
<string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar).</string>
<string name="v4_5_message_draft_descr">Conserva el último borrador del mensaje con los datos adjuntos.</string>
<string name="v4_5_private_filenames">Nombres de archivos privados</string>
<string name="v4_5_reduced_battery_usage">Reducción del uso de la batería</string>
@@ -663,7 +663,7 @@
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(escanear o pegar desde el portapapeles)</string>
<string name="icon_descr_profile_image_placeholder">Espacio reservado para la imagen del perfil</string>
<string name="image_descr_qr_code">Código QR</string>
<string name="chat_with_the_founder">Envía consultas e ideas</string>
<string name="chat_with_the_founder">Consultas y sugerencias</string>
<string name="smp_servers_preset_address">Dirección del servidor predefinida</string>
<string name="send_us_an_email">Contacta por email</string>
<string name="rate_the_app">Valora la aplicación</string>
@@ -676,7 +676,7 @@
<string name="reject">Rechazar</string>
<string name="open_verb">Abrir</string>
<string name="icon_descr_call_pending_sent">Llamada pendiente</string>
<string name="privacy_and_security">Privacidad y seguridad</string>
<string name="privacy_and_security">Privacidad y Seguridad</string>
<string name="store_passphrase_securely_without_recover">Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes.</string>
<string name="save_archive">Guardar archivo</string>
<string name="restore_database_alert_desc">Introduce la contraseña anterior después de restaurar la copia de seguridad de la base de datos. Esta acción no se puede deshacer.</string>
@@ -685,16 +685,16 @@
<string name="network_option_protocol_timeout">Tiempo de espera del protocolo</string>
<string name="network_option_seconds_label">seg</string>
<string name="users_delete_with_connections">Perfil y conexiones de servidor</string>
<string name="prohibit_sending_disappearing_messages">Prohibir el envío de mensajes temporales</string>
<string name="prohibit_sending_disappearing_messages">Prohibes el envío de mensajes temporales</string>
<string name="only_you_can_send_voice">Sólo tú puedes enviar mensajes de voz.</string>
<string name="only_your_contact_can_send_voice">Sólo tu contacto puede enviar mensajes de voz.</string>
<string name="run_chat_section">EJECUTAR CHAT</string>
<string name="restart_the_app_to_use_imported_chat_database">Reinicia la aplicación para poder usar la base de datos importada.</string>
<string name="enter_correct_current_passphrase">Introduce la contraseña actual correcta.</string>
<string name="feature_received_prohibited">recepción prohibida</string>
<string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcar para eliminar).</string>
<string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar).</string>
<string name="prohibit_direct_messages">Prohibir el envío de mensajes directos a los miembros.</string>
<string name="prohibit_sending_disappearing">Prohibir el envío de mensajes temporales</string>
<string name="prohibit_sending_disappearing">Prohibes el envío de mensajes temporales</string>
<string name="v4_2_security_assessment">Evaluación de la seguridad</string>
<string name="receiving_files_not_yet_supported">la recepción de archivos aún no está disponible</string>
<string name="sending_files_not_yet_supported">el envío de archivos aún no está disponible</string>
@@ -705,7 +705,7 @@
<string name="open_chat">Abrir chat</string>
<string name="restore_database">Restaurar copia de seguridad de la base de datos</string>
<string name="save_passphrase_and_open_chat">Guardar contraseña y abrir el chat</string>
<string name="restore_passphrase_not_found_desc">La contraseña no se ha encontrado en Keystore, introdúzcala manualmente. Esto puede haber ocurrido si has restaurado los datos de la aplicación con una herramienta de copia de seguridad. Si no es así, ponte en contacto con los desarrolladores.</string>
<string name="restore_passphrase_not_found_desc">La contraseña no se ha encontrado en Keystore, introdúzcala manualmente. Esto puede haber ocurrido si has restaurado los datos de la aplicación con una herramienta de copia de seguridad. Si no es así, por favor ponte en contacto con los desarrolladores.</string>
<string name="remove_member_confirmation">Eliminar</string>
<string name="button_remove_member">Eliminar miembro</string>
<string name="button_send_direct_message">Enviar mensaje directo</string>
@@ -736,11 +736,11 @@
<string name="stop_chat_confirmation">Detener</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Esta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</string>
<string name="skip_inviting_button">Omitir invitación a miembros</string>
<string name="settings_notification_preview_mode_title">Mostrar vista previa</string>
<string name="settings_notification_preview_mode_title">Vista previa</string>
<string name="la_notice_turn_on">Activar</string>
<string name="share_verb">Compartir</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string>
<string name="set_contact_name">Introduce el nombre del contacto</string>
<string name="set_contact_name">Escribe un nombre para el contacto</string>
<string name="network_socks_toggle">Usar proxy SOCKS (puerto 9050)</string>
<string name="unknown_error">Error desconocido</string>
<string name="member_role_will_be_changed_with_notification">El rol cambiará a \"%s\". Se notificará a todos los miembros del grupo.</string>
@@ -748,9 +748,9 @@
<string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string>
<string name="ntf_channel_messages">Mensajes de chat SimpleX</string>
<string name="icon_descr_received_msg_status_unread">no leído</string>
<string name="text_field_set_contact_placeholder">Introduce el nombre del contacto…</string>
<string name="text_field_set_contact_placeholder">Escribe un nombre para el contacto…</string>
<string name="switch_receiving_address_question">¿Cambiar dirección de recepción\?</string>
<string name="use_camera_button">Usar cámara</string>
<string name="use_camera_button">Cámara</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">¡El contacto con el que has compartido este enlace NO podrá conectarse!</string>
<string name="show_QR_code">Mostrar código QR</string>
<string name="this_link_is_not_a_valid_connection_link">¡El enlace no es un enlace de conexión válido!</string>
@@ -758,23 +758,23 @@
<string name="share_invitation_link">Compartir enlace de invitación</string>
<string name="update_network_session_mode_question">¿Actualizar el modo de aislamiento de transporte\?</string>
<string name="icon_descr_speaker_on">Altavoz activado</string>
<string name="stop_chat_to_enable_database_actions">Detener Chat para habilitar acciones sobre la base de datos.</string>
<string name="stop_chat_to_enable_database_actions">Detén Chat para habilitar las acciones sobre la base de datos.</string>
<string name="connection_you_accepted_will_be_cancelled">¡La conexión que has aceptado se cancelará!</string>
<string name="database_initialization_error_desc">La base de datos no funciona correctamente. Pulsa para obtener más información</string>
<string name="moderate_message_will_be_marked_warning">El mensaje será marcado como moderado para todos los miembros.</string>
<string name="next_generation_of_private_messaging">La próxima generación de mensajería privada</string>
<string name="delete_files_and_media_desc">Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</string>
<string name="enable_automatic_deletion_message">Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos.</string>
<string name="messages_section_description">Esta configuración se aplica a los mensajes en su perfil actual de Chat</string>
<string name="messages_section_description">Esta configuración se aplica a los mensajes en tu perfil actual</string>
<string name="this_string_is_not_a_connection_link">¡Esta cadena no es un enlace de conexión!</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo plano<xliff:g id="appName">SimpleX</xliff:g></b>, utiliza un pequeño porcentaje de la batería al día.</string>
<string name="icon_descr_settings">Configuración</string>
<string name="icon_descr_speaker_off">Altavoz apagado</string>
<string name="add_contact_or_create_group">Inciar chat nuevo</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Detener Chat para exportar, importar o eliminar la base de datos del chat. No podrá recibir ni enviar mensajes mientras el chat esté detenido.</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">Detén Chat para poder exportar, importar o eliminar la base de datos. No puedes recibir ni enviar mensajes mientras Chat esté detenido.</string>
<string name="thank_you_for_installing_simplex">Gracias por instalar <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger la privacidad, en lugar de los identificadores de usuario que utilizan el resto de plataformas, <xliff:g id="appName">SimpleX</xliff:g> dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos.</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">Para proteger tu información, activa SimpleX Lock.
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa Bloqueo SimpleX.
\nSe te pedirá que completes la autenticación antes de activar esta función.</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Al actualizar la configuración, el cliente se reconectará a todos los servidores.</string>
<string name="use_simplex_chat_servers__question">¿Usar servidores <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\?</string>
@@ -784,22 +784,22 @@
<string name="error_smp_test_server_auth">El servidor requiere autorización para crear colas, comprueba la contraseña</string>
<string name="enter_passphrase_notification_desc">Para recibir notificaciones, introduce la contraseña de la base de datos</string>
<string name="ntf_channel_calls">Llamadas de chat SimpleX</string>
<string name="notifications_mode_periodic">Se inicia periódicamente</string>
<string name="notification_preview_mode_message_desc">Mostrar contacto y mensaje</string>
<string name="notification_preview_mode_contact_desc">Mostrar sólo contacto</string>
<string name="notifications_mode_periodic">Se ejecuta periódicamente</string>
<string name="notification_preview_mode_message_desc">Se muestran el nombre del contacto y el mensaje</string>
<string name="notification_preview_mode_contact_desc">Se muestra solo el nombre del contacto</string>
<string name="auth_simplex_lock_turned_on">Bloqueo SimpleX activado</string>
<string name="auth_stop_chat">Detener chat</string>
<string name="moderate_message_will_be_deleted_warning">El mensaje se eliminará para todos los miembros.</string>
<string name="share_file">Compartir archivo…</string>
<string name="images_limit_title">¡Demasiadas imágenes!</string>
<string name="image_decoding_exception_desc">La imagen no se puede decodificar. Pruebe otra imagen o pónte en contacto con los desarrolladores.</string>
<string name="image_decoding_exception_desc">La imagen no se puede decodificar. Pruebe con otra imagen o contacta con los desarrolladores.</string>
<string name="network_enable_socks">¿Usa proxy SOCKS\?</string>
<string name="network_use_onion_hosts">Usar hosts .onion</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plataforma de mensajería y aplicaciones que protege tu privacidad y seguridad.</string>
<string name="first_platform_without_user_ids">La primera plataforma sin identificadores de usuario: diseñada para la privacidad.</string>
<string name="alert_message_no_group">Este grupo ya no existe.</string>
<string name="incognito_info_find">Para encontrar el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</string>
<string name="incognito_info_find">Para conocer el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</string>
<string name="accept_feature_set_1_day">Establecer 1 día</string>
<string name="v4_4_french_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
<string name="v4_5_italian_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
@@ -811,7 +811,7 @@
<string name="is_not_verified">%s no está verificado</string>
<string name="smp_servers_test_server">Probar servidor</string>
<string name="smp_servers_test_servers">Probar servidores</string>
<string name="star_on_github">Comienza en GitHub</string>
<string name="star_on_github">Estrella en GitHub</string>
<string name="smp_servers_per_user">Los servidores para nuevas conexiones de tu perfil de Chat actual</string>
<string name="network_disable_socks">¿Usar conexión directa a Internet\?</string>
<string name="update_onion_hosts_settings_question">¿Actualizar la configuración de los hosts .onion\?</string>
@@ -825,9 +825,9 @@
<string name="rcv_group_event_updated_group_profile">perfil de grupo actualizado</string>
<string name="network_option_tcp_connection_timeout">Tiempo de espera de la conexión TCP agotado</string>
<string name="theme">Tema</string>
<string name="set_group_preferences">Establecer preferencias de grupo</string>
<string name="set_group_preferences">Establece preferencias de grupo</string>
<string name="settings_section_title_support">SOPORTE SIMPLEX CHAT</string>
<string name="set_password_to_export">Seleccióna contraseña para exportar</string>
<string name="set_password_to_export">Escribe la contraseña para exportar</string>
<string name="update_database">Actualizar</string>
<string name="update_database_passphrase">Actualizar contraseña base de datos</string>
<string name="group_invitation_tap_to_join_incognito">Pulsa para unirte en modo incógnito</string>
@@ -841,7 +841,7 @@
<string name="error_smp_test_failed_at_step">Prueba fallida en el paso %s.</string>
<string name="tap_to_start_new_chat">Pulsa para iniciar chat nuevo</string>
<string name="share_message">Compartir mensaje…</string>
<string name="share_image">Compartir imagen</string>
<string name="share_image">Compartir medios</string>
<string name="show_call_on_lock_screen">Mostrar</string>
<string name="unknown_database_error_with_info">Error desconocido en la base de datos: %s</string>
<string name="database_backup_can_be_restored">El intento de cambiar la contraseña de la base de datos no se ha completado.</string>
@@ -858,23 +858,23 @@
<string name="description_via_one_time_link">mediante enlace de un uso</string>
<string name="your_chats">Tus chats</string>
<string name="voice_message_send_text">Mensaje de voz…</string>
<string name="your_contact_address">Tu dirección de contacto</string>
<string name="your_contact_address">Mi dirección de contacto</string>
<string name="icon_descr_video_off">Desactivar vídeo</string>
<string name="icon_descr_video_on">Activar vídeo</string>
<string name="wrong_passphrase">Contraseña de base de datos incorrecta</string>
<string name="wrong_passphrase_title">¡Contraseña incorrecta!</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Te has unido a este grupo. Conectando con miembro del grupo invitado.</string>
<string name="alert_title_cant_invite_contacts_descr">Estás utilizando un perfil incógnito para este grupo. Para evitar compartir tu perfil principal, invitar contactos no está permitido</string>
<string name="alert_title_cant_invite_contacts_descr">Estás usando un perfil incógnito para este grupo, por tanto para evitar compartir tu perfil principal no se permite invitar contactos</string>
<string name="you_are_invited_to_group">Has sido invitado al grupo</string>
<string name="v4_3_voice_messages">Mensajes de voz</string>
<string name="v4_3_irreversible_message_deletion_desc">Tus contactos pueden permitir la eliminación completa de mensajes.</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Tú controlas a través de qué servidor(es) <b>recibes</b> los mensajes. Tus contactos controlan a través de qué servidor(es) <b>envías</b> tus mensajes.</string>
<string name="voice_messages">Mensajes de voz</string>
<string name="voice_messages_are_prohibited">Los mensajes de voz están prohibidos en este grupo.</string>
<string name="voice_messages_are_prohibited">Los mensajes de voz no están permitidos en este grupo.</string>
<string name="v4_4_verify_connection_security">Comprobar la seguridad de la conexión</string>
<string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado a <xliff:g id="contactName" example="Alice">%1$s! </xliff:g>.</string>
<string name="welcome">¡Bienvenido!</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil de chat será enviado
<string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil Chat será enviado
\na tu contacto</string>
<string name="your_ICE_servers">Tus servidores ICE</string>
<string name="you_rejected_group_invitation">Has rechazado la invitación del grupo.</string>
@@ -887,11 +887,11 @@
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> quiere conectarse contigo mediante</string>
<string name="failed_to_create_user_duplicate_desc">Tienes un perfil de chat con el mismo nombre mostrado. Debes elegir otro nombre.</string>
<string name="you_can_also_connect_by_clicking_the_link">También puedes conectarte haciendo clic en el enlace. Si se abre en el navegador, haz clic en <b>Abrir en aplicación móvil</b>.</string>
<string name="you_can_connect_to_simplex_chat_founder">Puedes <font color="#0088ff">conectar con los desarrolladores de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para hacer cualquier pregunta y recibir actualizaciones</font>.</string>
<string name="you_can_connect_to_simplex_chat_founder">Puedes <font color="#0088ff">ponerte en contacto con los desarrolladores de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para consultas y para recibir actualizaciones</font>.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Puedes compartir tu dirección como enlace o como código QR: cualquiera podrá conectarse contigo. Si lo eliminas más tarde tus contactos no se perderán.</string>
<string name="observer_cant_send_message_title">¡No puedes enviar mensajes!</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Puedes usar marcadores para dar formato a los mensajes:</string>
<string name="you_must_use_the_most_recent_version_of_database">Debes usar la versión más reciente de tu base de datos SÓLO en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos.</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Puedes usar la sintaxis markdown para dar formato a los mensajes:</string>
<string name="you_must_use_the_most_recent_version_of_database">Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Tu contacto debe estar en línea para que se complete la conexión.
\nPuedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo).</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">La base de datos actual será ELIMINADA y SUSTITUIDA por la importada.
@@ -900,7 +900,7 @@
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Te conectarás cuando se acepte tu solicitud de conexión, por favor espere o compruébalo más tarde.</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde.</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano.</string>
<string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que has compartido un perfil incógnito, al grupo en el que utilizas tu perfil principal</string>
<string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal</string>
<string name="simplex_link_mode_browser">mediante navegador</string>
<string name="simplex_link_connection">mediante <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_service_notification_title">Servicio <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
@@ -915,7 +915,7 @@
<string name="snd_group_event_member_deleted">has eliminado <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="group_info_member_you">Tú: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puedes compartir un enlace o un código QR: cualquiera podrá unirse al grupo. Si lo eliminas más tarde los miembros del grupo no se perderán.</string>
<string name="incognito_info_share">Cuando compartes un perfil incógnito con alguien, este perfil se usará para los grupos a los que te inviten.</string>
<string name="incognito_info_share">Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten.</string>
<string name="your_preferences">Tus preferencias</string>
<string name="v4_2_auto_accept_contact_requests_desc">Con mensaje de bienvenida opcional.</string>
<string name="you_are_observer">Tu rol es observador</string>
@@ -924,7 +924,7 @@
<string name="you_invited_your_contact">Has invitado a tu contacto</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde.</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Tu contacto puede escanear el código QR desde la aplicación.</string>
<string name="your_settings">Tu configuración</string>
<string name="your_settings">Mi configuración</string>
<string name="your_SMP_servers">Tus servidores SMP</string>
<string name="you_control_your_chat">¡Tú controlas tu chat!</string>
<string name="your_profile_is_stored_on_your_device">Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.</string>
@@ -934,15 +934,14 @@
<string name="icon_descr_video_call">Videollamada</string>
<string name="your_calls">Tus llamadas</string>
<string name="your_ice_servers">Tus servidores ICE</string>
<string name="your_privacy">Tu privacidad</string>
<string name="settings_section_title_you">TU</string>
<string name="your_chat_database">Base de datos de Chat</string>
<string name="your_privacy">Privacidad</string>
<string name="settings_section_title_you">MIS DATOS</string>
<string name="your_chat_database">Base de datos Chat</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puedes iniciar el chat en Configuración / Base de datos o reiniciando la aplicación.</string>
<string name="you_sent_group_invitation">Has enviado una invitación de grupo</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contacto(s) seleccionado(s)</string>
<string name="num_contacts_selected">%d contacto(s) seleccionado(s)</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members"> %1$s </xliff:g> MIEMBROS</string>
<string name="your_chat_profiles_stored_locally">Tus perfiles de chat se almacenan localmente, sólo en tu dispositivo</string>
<string name="voice_prohibited_in_this_chat">Los mensajes de voz están prohibidos en este chat.</string>
<string name="voice_prohibited_in_this_chat">Los mensajes de voz no están permitidos en este chat.</string>
<string name="whats_new">Novedades</string>
<string name="you_have_to_enter_passphrase_every_time">La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación.</string>
<string name="you_joined_this_group">Te has unido a este grupo</string>
@@ -962,22 +961,20 @@
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> mensaje(s) omitido(s)</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Dejarás de recibir mensajes de este grupo. El historial del chat se conservará.</string>
<string name="view_security_code">Ver código de seguridad</string>
<string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz debes permitir que tu contacto pueda enviarlos.</string>
<string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos.</string>
<string name="voice_messages_prohibited">¡Mensajes de voz prohibidos!</string>
<string name="group_main_profile_sent">Tu perfil de chat se envia a los miembros del grupo</string>
<string name="group_main_profile_sent">Tu perfil Chat se enviado a los miembros del grupo</string>
<string name="icon_descr_address">Dirección <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="image_descr_simplex_logo">Logo <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_simplex_team">Equipo <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="your_profile_will_be_sent">Tu perfil de chat se enviará a tu contacto</string>
<string name="your_chat_profiles">Tus perfiles de chat</string>
<string name="your_simplex_contact_address">Tu dirección de contacto <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="your_profile_will_be_sent">Tu perfil Chat se enviará a tu contacto</string>
<string name="your_chat_profiles">Mis perfiles</string>
<string name="your_simplex_contact_address">Mi dirección de contacto <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="smp_servers_your_server">Tu servidor</string>
<string name="smp_servers_your_server_address">Dirección de tu servidor</string>
<string name="section_title_welcome_message">MENSAJE DE BIENVENIDA</string>
<string name="your_current_profile">Tu perfil actual</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.
\n
\nLos servidores <xliff:g id="appName">SimpleX</xliff:g> no pueden ver tu perfil.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. Los servidores <xliff:g id="appName">SimpleX</xliff:g> no pueden ver tu perfil.</string>
<string name="language_system">Sistema</string>
<string name="button_add_welcome_message">Agregar mensaje de bienvenida</string>
<string name="v4_6_audio_video_calls">Llamadas y videollamadas</string>
@@ -993,7 +990,7 @@
<string name="user_hide">Ocultar</string>
<string name="user_mute">Silenciar</string>
<string name="save_and_update_group_profile">Guardar y actualizar perfil del grupo</string>
<string name="tap_to_activate_profile">Pulsa para activar el perfil.</string>
<string name="tap_to_activate_profile">Pulsa sobre un perfil para activarlo.</string>
<string name="should_be_at_least_one_visible_profile">Debe haber al menos un perfil de usuario visible.</string>
<string name="user_unhide">Mostrar</string>
<string name="button_welcome_message">Mensaje de bienvenida</string>
@@ -1004,7 +1001,7 @@
<string name="muted_when_inactive">¡Silenciado cuando está inactivo!</string>
<string name="v4_6_group_moderation">Moderación de grupos</string>
<string name="v4_6_hidden_chat_profiles">Perfiles Chat ocultos</string>
<string name="v4_6_hidden_chat_profiles_descr">¡Proteje los perfiles de Chat con contraseña!</string>
<string name="v4_6_hidden_chat_profiles_descr">¡Protege tus perfiles con contraseña!</string>
<string name="v4_6_audio_video_calls_descr">Soporte bluetooth y otras mejoras.</string>
<string name="v4_6_group_welcome_message_descr">¡Establece el mensaje mostrado a los miembros nuevos!</string>
<string name="v4_6_chinese_spanish_interface">Interfaz en chino y español</string>
@@ -1023,4 +1020,124 @@
\n- borrar mensajes de los miembros.
\n- desactivar el rol a miembros (a rol \"observador\")</string>
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Tus perfiles Chat.</string>
<string name="database_upgrade">Actualización de la base de datos</string>
<string name="database_downgrade">Volviendo a versión anterior de la base de datos</string>
<string name="invalid_migration_confirmation">Confirmación de migración no válida</string>
<string name="upgrade_and_open_chat">Actualizar y abrir Chat</string>
<string name="database_migrations">Migraciones: %s</string>
<string name="mtr_error_different">migración diferente en la aplicación/base de datos: %s / %s</string>
<string name="downgrade_and_open_chat">Volver a versión anterior y abrir Chat</string>
<string name="database_downgrade_warning">Atención: ¡puedes perder algunos datos!</string>
<string name="incompatible_database_version">Versión de base de datos incompatible</string>
<string name="confirm_database_upgrades">Confirmar actualizaciones de la bases de datos</string>
<string name="mtr_error_no_down_migration">la versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia versión anterior para: %s</string>
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
<string name="developer_options">ID de base de datos y opción de aislamiento de transporte.</string>
<string name="file_will_be_received_when_contact_completes_uploading">El archivo se recibirá cuando tu contacto termine de subirlo.</string>
<string name="image_will_be_received_when_contact_completes_uploading">La imagen se recibirá cuando tu contacto termine de subirla.</string>
<string name="show_developer_options">Mostrar opciones de desarrollador</string>
<string name="hide_dev_options">Ocultar:</string>
<string name="show_dev_options">Mostrar:</string>
<string name="cancel_file__question">¿Cancelar el envío de archivos\?</string>
<string name="file_transfer_will_be_cancelled_warning">El envío de archivos será cancelado. Si está en progreso se detendrá.</string>
<string name="delete_chat_profile">Eliminar perfil de chat</string>
<string name="profile_password">Contraseña del perfil</string>
<string name="unhide_chat_profile">Mostrar perfil de chat</string>
<string name="unhide_profile">Mostrar perfil</string>
<string name="delete_profile">Eliminar perfil</string>
<string name="video_descr">Vídeo</string>
<string name="video_will_be_received_when_contact_is_online">El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde.</string>
<string name="waiting_for_video">Esperando el vídeo</string>
<string name="icon_descr_video_asked_to_receive">Ha pedido recibir el video</string>
<string name="videos_limit_title">¡Demasiados vídeos!</string>
<string name="icon_descr_video_snd_complete">Vídeo enviado</string>
<string name="video_will_be_received_when_contact_completes_uploading">El vídeo se recibirá cuando tu contacto termine de subirlo.</string>
<string name="videos_limit_desc">Solo se pueden enviar 10 vídeos de forma simultánea</string>
<string name="icon_descr_waiting_for_video">Esperando el vídeo</string>
<string name="error_saving_xftp_servers">Error guardando servidores SMP</string>
<string name="error_loading_xftp_servers">Error cargando servidores XFTP</string>
<string name="error_loading_smp_servers">Error cargando servidores SMP</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor XFTP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string>
<string name="error_xftp_test_server_auth">El servidor requiere autorización para subir, comprueba la contraseña</string>
<string name="smp_server_test_compare_file">Comparar archivo</string>
<string name="smp_server_test_create_file">Crear archivo</string>
<string name="smp_server_test_delete_file">Eliminar archivo</string>
<string name="smp_server_test_upload_file">Subir archivo</string>
<string name="xftp_servers">Servidores XFTP</string>
<string name="your_XFTP_servers">Tus servidores XFTP</string>
<string name="port_verb">Puerto</string>
<string name="network_proxy_port">puerto %d</string>
<string name="disable_onion_hosts_when_not_supported">Establece <i>Usar hosts .onion</i> en No si el proxy SOCKS no los admite.</string>
<string name="smp_server_test_download_file">Descargar archivo</string>
<string name="network_socks_toggle_use_socks_proxy">Usar proxy SOCKS</string>
<string name="host_verb">Host</string>
<string name="network_socks_proxy_settings">Configuración proxy SOCKS</string>
<string name="la_authenticate">Autenticar</string>
<string name="la_current_app_passcode">Código de acceso actual</string>
<string name="authentication_cancelled">Autenticación cancelada</string>
<string name="change_lock_mode">Cambiar el modo de bloqueo</string>
<string name="la_seconds">%d segundos</string>
<string name="la_auth_failed">Error de autenticación</string>
<string name="la_change_app_passcode">Cambiar el código de acceso</string>
<string name="passcode_not_changed">¡Código de acceso no cambiado!</string>
<string name="la_lock_mode_system">Autenticación del sistema</string>
<string name="la_no_app_password">Sin código de acceso de la aplicación</string>
<string name="la_lock_mode_passcode">Campo de código de acceso</string>
<string name="la_lock_mode">Modo Bloqueo SimpleX</string>
<string name="la_could_not_be_verified">No has podido ser verificado. Inténtelo de nuevo.</string>
<string name="la_minutes">%d minutos</string>
<string name="la_enter_app_passcode">Introducir el código de acceso</string>
<string name="la_immediately">Inmediatamente</string>
<string name="la_please_remember_to_store_password">Por favor, recuerda y guarda la contraseña en un lugar seguro. ¡No hay ninguna manera de recuperar una contraseña perdida!</string>
<string name="lock_not_enabled">¡Bloqueo SimpleX no activado!</string>
<string name="you_can_turn_on_lock">Puedes activar el Bloqueo SimpleX a través de Configuración.</string>
<string name="confirm_passcode">Confirmar el código de acceso</string>
<string name="enable_lock">Activar bloqueo</string>
<string name="lock_after">Bloquear después de</string>
<string name="lock_mode">Modo de bloqueo</string>
<string name="submit_passcode">Enviar</string>
<string name="incorrect_passcode">Código de acceso incorrecto</string>
<string name="new_passcode">Nuevo código de acceso</string>
<string name="la_mode_passcode">Código de acceso</string>
<string name="passcode_changed">¡Código de acceso cambiado!</string>
<string name="passcode_set">¡Código de acceso guardado!</string>
<string name="la_mode_system">Sistema</string>
<string name="decryption_error">Error de descifrado</string>
<string name="alert_text_msg_bad_id">El ID del siguiente mensaje es incorrecto (menor o igual que el anterior).
\nPuede ocurrir por algún bug o cuando la conexión está comprometida.</string>
<string name="alert_text_fragment_please_report_to_developers">Por favor, informa a los desarrolladores.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> mensajes omitidos.</string>
<string name="alert_title_msg_bad_hash">Hash de mensaje incorrecto</string>
<string name="alert_title_msg_bad_id">ID de mensaje incorrecto</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.</string>
<string name="alert_text_msg_bad_hash">El hash del mensaje anterior es diferente.</string>
<string name="alert_text_fragment_permanent_error_reconnect">El error es permanente para esta conexión, por favor vuelve a conectarte.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> mensajes no pudieron ser descifrados.</string>
<string name="no_spaces">¡Sin espacios!</string>
<string name="stop_file__action">Detener archivo</string>
<string name="revoke_file__message">El archivo será eliminado de los servidores.</string>
<string name="stop_rcv_file__message">Se detendrá la recepción del archivo.</string>
<string name="revoke_file__confirm">Revocar</string>
<string name="revoke_file__action">Revocar archivo</string>
<string name="revoke_file__title">¿Revocar archivo\?</string>
<string name="stop_snd_file__message">Se detendrá el envío del archivo.</string>
<string name="stop_file__confirm">Detener</string>
<string name="stop_rcv_file__title">¿Dejar de recibir el archivo\?</string>
<string name="stop_snd_file__title">¿Dejar de enviar el archivo\?</string>
<string name="allow_calls_only_if">Se permiten llamadas sólo si tu contacto también las permite.</string>
<string name="allow_your_contacts_to_call">Permites que tus contactos puedan llamarte.</string>
<string name="audio_video_calls">Llamadas/Videollamadas</string>
<string name="calls_prohibited_with_this_contact">Las llamadas/videollamadas no están permitidas.</string>
<string name="available_in_v51">"
\nDisponible en v5.1"</string>
<string name="both_you_and_your_contact_can_make_calls">Tanto tú como tu contacto podéis realizar llamadas.</string>
<string name="only_you_can_make_calls">Solo tú puedes realizar llamadas.</string>
<string name="only_your_contact_can_make_calls">Sólo tu contacto puede realizar llamadas.</string>
<string name="prohibit_calls">Prohibir las llamadas/videollamadas.</string>
<string name="v5_0_app_passcode">Código de acceso a la aplicación</string>
<string name="v5_0_polish_interface">Interfaz polaco</string>
<string name="v5_0_app_passcode_descr">Úsalo en lugar de la autenticación del sistema.</string>
<string name="v5_0_polish_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
<string name="v5_0_large_files_support">Vídeos y archivos de hasta 1Gb</string>
<string name="v5_0_large_files_support_descr">¡Rápido y sin tener que esperar a que el remitente esté en línea!</string>
</resources>
@@ -156,7 +156,7 @@
<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="share_image">Partager le média</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>
@@ -201,7 +201,9 @@
<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="choose_file">Fichier</string>
<string name="gallery_image_button">Image</string>
<string name="gallery_video_button">Vidéo</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>
@@ -294,7 +296,7 @@
<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="use_camera_button">Appareil\nphoto</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>
@@ -437,9 +439,7 @@
<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="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. Les 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>
@@ -475,12 +475,10 @@
<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="alert_text_skipped_messages_it_can_happen_when">Cela peut arriver quand :
\n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours.
\n2. Le déchiffrement du message a échoué, car vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.
\n3. La connexion a été compromise.</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>
@@ -549,7 +547,7 @@
<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_messages">MESSAGES ET FICHIERS</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>
@@ -744,7 +742,7 @@
<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="num_contacts_selected">%d 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>
@@ -854,7 +852,7 @@
<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="prohibit_sending_disappearing_messages">Interdire 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>
@@ -887,7 +885,7 @@
<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_sending_disappearing">Interdire 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>
@@ -925,7 +923,6 @@
<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>
@@ -942,7 +939,6 @@
<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>
@@ -991,7 +987,7 @@
<string name="confirm_password">Confirmer le mot de passe</string>
<string name="v4_6_reduced_battery_usage">Réduction accrue de l\'utilisation de la batterie</string>
<string name="v4_6_chinese_spanish_interface">Interface en chinois et en espagnol</string>
<string name="enter_password_to_show">Entrez le mot de passe ci-dessus pour continuer !</string>
<string name="enter_password_to_show">Entrez le mot de passe dans le champ de recherche</string>
<string name="v4_6_audio_video_calls">Appels audio et vidéo</string>
<string name="v4_6_group_welcome_message">Message d\'accueil du groupe</string>
<string name="error_saving_user_password">Erreur d\'enregistrement du mot de passe de l\'utilisateur</string>
@@ -1006,13 +1002,13 @@
<string name="hide_profile">Masquer le profil</string>
<string name="v4_6_reduced_battery_usage_descr">D\'autres améliorations sont à venir !</string>
<string name="password_to_show">Mot de passe à afficher</string>
<string name="make_profile_private">Rendre le profil privé !</string>
<string name="make_profile_private">Rendre un profil privé !</string>
<string name="user_mute">Mute</string>
<string name="v4_6_hidden_chat_profiles_descr">Protégez vos profils de chat par un mot de passe !</string>
<string name="tap_to_activate_profile">Appuyez pour activer le profil.</string>
<string name="save_and_update_group_profile">Sauvegarder et mettre à jour le profil du groupe</string>
<string name="save_profile_password">Enregistrer le mot de passe du profil</string>
<string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez un mot de passe complet dans le champ de recherche de la page Profils de chat.</string>
<string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page Profils de chat.</string>
<string name="v4_6_audio_video_calls_descr">Prise en charge du Bluetooth et autres améliorations.</string>
<string name="v4_6_chinese_spanish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="should_be_at_least_one_profile">Il doit y avoir au moins un profil d\'utilisateur.</string>
@@ -1023,4 +1019,124 @@
<string name="group_welcome_title">Message d\'accueil</string>
<string name="you_can_hide_or_mute_user_profile">Vous pouvez masquer ou mettre en sourdine un profil d\'utilisateur - maintenez-le enfoncé pour accéder au menu.</string>
<string name="you_will_still_receive_calls_and_ntfs">Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu\'ils sont actifs.</string>
<string name="database_downgrade">Rétrogradation de la base de données</string>
<string name="database_upgrade">Mise à niveau de la base de données</string>
<string name="incompatible_database_version">Version de la base de données incompatible</string>
<string name="downgrade_and_open_chat">Rétrograder et ouvrir le chat</string>
<string name="invalid_migration_confirmation">Confirmation de migration invalide</string>
<string name="upgrade_and_open_chat">Mettre à niveau et ouvrir le chat</string>
<string name="database_migrations">Migrations : %s</string>
<string name="database_downgrade_warning">Attention : vous risquez de perdre des données !</string>
<string name="confirm_database_upgrades">Confirmer la mise à niveau de la base de données</string>
<string name="mtr_error_no_down_migration">la base de données a une version plus récente que celle de l\'application, mais il n\'y a pas de rétrogradation pour : %s</string>
<string name="mtr_error_different">migration différente dans l\'app/la base de données : %s / %s</string>
<string name="image_will_be_received_when_contact_completes_uploading">L\'image sera reçue lorsque votre contact aura terminé de la mettre en ligne.</string>
<string name="show_dev_options">Afficher :</string>
<string name="show_developer_options">Afficher les options pour les développeurs</string>
<string name="file_will_be_received_when_contact_completes_uploading">Le fichier sera reçu lorsque votre contact aura terminé de le mettre en ligne.</string>
<string name="developer_options">IDs de base de données et option d\'isolation du transport.</string>
<string name="settings_section_title_experimenta">EXPÉRIMENTALE</string>
<string name="hide_dev_options">Cacher :</string>
<string name="unhide_chat_profile">Dévoiler le profil de chat</string>
<string name="unhide_profile">Dévoiler le profil</string>
<string name="delete_chat_profile">Supprimer le profil de chat</string>
<string name="delete_profile">Supprimer le profil</string>
<string name="cancel_file__question">Annuler le transfert de fichiers \?</string>
<string name="file_transfer_will_be_cancelled_warning">Le transfert de fichiers sera annulé. S\'il est en cours, il sera interrompu.</string>
<string name="profile_password">Mot de passe de profil</string>
<string name="videos_limit_title">Trop de vidéos !</string>
<string name="video_descr">Vidéo</string>
<string name="icon_descr_video_snd_complete">Vidéo envoyée</string>
<string name="video_will_be_received_when_contact_completes_uploading">La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer.</string>
<string name="icon_descr_waiting_for_video">En attente de la vidéo</string>
<string name="waiting_for_video">En attente de la vidéo</string>
<string name="video_will_be_received_when_contact_is_online">La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !</string>
<string name="icon_descr_video_asked_to_receive">Requête de réception de la vidéo</string>
<string name="videos_limit_desc">Seulement 10 vidéos peuvent être envoyées en même temps</string>
<string name="smp_server_test_create_file">Créer un fichier</string>
<string name="smp_server_test_delete_file">Supprimer le fichier</string>
<string name="error_saving_xftp_servers">Erreur lors de la sauvegarde des serveurs XFTP</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs XFTP sont au bon format, séparées par des lignes et qu\'elles ne sont pas dupliquées.</string>
<string name="error_xftp_test_server_auth">Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe</string>
<string name="smp_server_test_upload_file">Transférer le fichier</string>
<string name="xftp_servers">Serveurs XFTP</string>
<string name="your_XFTP_servers">Vos serveurs XFTP</string>
<string name="smp_server_test_compare_file">Comparer le fichier</string>
<string name="smp_server_test_download_file">Télécharger le fichier</string>
<string name="error_loading_smp_servers">Erreur lors du chargement des serveurs SMP</string>
<string name="error_loading_xftp_servers">Erreur lors du chargement des serveurs XFTP</string>
<string name="host_verb">Héberger</string>
<string name="port_verb">Port</string>
<string name="network_proxy_port">port %d</string>
<string name="network_socks_proxy_settings">Paramètres de proxy SOCKS</string>
<string name="network_socks_toggle_use_socks_proxy">Utiliser un proxy SOCKS</string>
<string name="disable_onion_hosts_when_not_supported">Définissez <i>Utiliser les hôtes .onion</i> sur Non si le proxy SOCKS ne les prend pas en charge.</string>
<string name="la_lock_mode">Mode de SimpleX Lock</string>
<string name="lock_not_enabled">SimpleX Lock n\'est pas activé !</string>
<string name="la_lock_mode_system">Authentification du système</string>
<string name="la_authenticate">Authentification</string>
<string name="la_auth_failed">Echec de l\'authentification</string>
<string name="la_change_app_passcode">Modifier le code d\'accès</string>
<string name="la_current_app_passcode">Code d\'accès actuel</string>
<string name="la_minutes">%d minutes</string>
<string name="la_seconds">%d secondes</string>
<string name="la_enter_app_passcode">Entrer le code d\'accès</string>
<string name="la_immediately">Immédiatement</string>
<string name="la_no_app_password">Pas de code d\'accès à l\'app</string>
<string name="la_lock_mode_passcode">Saisie du code</string>
<string name="la_please_remember_to_store_password">Veuillez le mémoriser ou le conserver en toute sécurité - il n\'y a aucun moyen de récupérer un mot de passe perdu !</string>
<string name="enable_lock">Activer le verrouillage</string>
<string name="change_lock_mode">Modifier le mode de verrouillage</string>
<string name="incorrect_passcode">Code d\'accès erroné</string>
<string name="new_passcode">Nouveau code d\'accès</string>
<string name="la_mode_passcode">Code d\'accès</string>
<string name="passcode_changed">Code d\'accès modifié !</string>
<string name="passcode_not_changed">Le code d\'accès n\'a pas été modifié !</string>
<string name="passcode_set">Code d\'accès défini !</string>
<string name="la_mode_system">Système</string>
<string name="authentication_cancelled">Authentification annulée</string>
<string name="alert_title_msg_bad_id">Mauvais ID de message</string>
<string name="alert_text_msg_bad_hash">Le hash du message précédent est différent.</string>
<string name="alert_text_msg_bad_id">L\'ID du message suivant est incorrect (inférieur ou égal au précédent).
\nCela peut se produire en raison d\'un bug ou lorsque la connexion est compromise.</string>
<string name="decryption_error">Erreur de déchiffrement</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Cela peut se produire lorsque vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.</string>
<string name="alert_title_msg_bad_hash">Mauvais hash de message</string>
<string name="no_spaces">Pas d\'espace !</string>
<string name="allow_calls_only_if">Autoriser les appels que si votre contact les autorise.</string>
<string name="allow_your_contacts_to_call">Autorise vos contacts à vous appeler.</string>
<string name="audio_video_calls">Appels audio/vidéo</string>
<string name="available_in_v51">"
\nDisponible dans la v5.1"</string>
<string name="prohibit_calls">Interdire les appels audio/vidéo.</string>
<string name="revoke_file__message">Le fichier sera supprimé des serveurs.</string>
<string name="revoke_file__confirm">Révoquer</string>
<string name="revoke_file__action">Révoquer le fichier</string>
<string name="revoke_file__title">Révoquer le fichier \?</string>
<string name="stop_snd_file__message">L\'envoi du fichier sera interrompu.</string>
<string name="alert_text_fragment_please_report_to_developers">Veuillez le signaler aux développeurs.</string>
<string name="submit_passcode">Soumettre</string>
<string name="both_you_and_your_contact_can_make_calls">Vous et votre contact pouvez tous deux passer des appels.</string>
<string name="only_you_can_make_calls">Vous seul pouvez passer des appels.</string>
<string name="calls_prohibited_with_this_contact">Interdire les appels audio/vidéo.</string>
<string name="confirm_passcode">Confirmer le code d\'accès</string>
<string name="lock_after">Verrouillage après</string>
<string name="lock_mode">Mode de verrouillage</string>
<string name="only_your_contact_can_make_calls">Seul votre contact peut passer des appels.</string>
<string name="stop_rcv_file__message">La réception du fichier sera interrompue.</string>
<string name="stop_file__confirm">Arrêter</string>
<string name="stop_snd_file__title">Arrêter l\'envoi du fichier \?</string>
<string name="stop_file__action">Arrêter le fichier</string>
<string name="stop_rcv_file__title">Arrêter de recevoir le fichier \?</string>
<string name="alert_text_fragment_permanent_error_reconnect">Cette erreur est persistante pour cette connexion, veuillez vous reconnecter.</string>
<string name="la_could_not_be_verified">Vous n\'avez pas pu être vérifié·e; veuillez réessayer.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> messages n\'ont pas pu être déchiffrés.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> messages sautés.</string>
<string name="you_can_turn_on_lock">Vous pouvez activer SimpleX Lock dans les Paramètres.</string>
<string name="v5_0_polish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="v5_0_large_files_support">Vidéos et fichiers jusqu\'à 1Go</string>
<string name="v5_0_app_passcode">Code d\'accès à l\'app</string>
<string name="v5_0_large_files_support_descr">Rapide et ne nécessitant pas d\'attendre que l\'expéditeur soit en ligne !</string>
<string name="v5_0_polish_interface">Interface en polonais</string>
<string name="v5_0_app_passcode_descr">Il permet de remplacer l\'authentification du système.</string>
</resources>
@@ -180,4 +180,120 @@
<string name="restore_database_alert_confirm">वापस लौटाना</string>
<string name="a_plus_b">ए + बी</string>
<string name="incognito_random_profile">आपका यादृच्छिक प्रोफ़ाइल</string>
<string name="chat_preferences">चैट वरीयताएँ</string>
<string name="ttl_days">%d दिनों</string>
<string name="allow_voice_messages_question">ध्वनि संदेशों की अनुमति दें\?</string>
<string name="clear_chat_menu_action">साफ़</string>
<string name="icon_descr_audio_off">ऑडियो बंद</string>
<string name="icon_descr_audio_on">ऑडियो चालू</string>
<string name="v4_4_disappearing_messages">गायब होने वाले संदेश</string>
<string name="v4_5_reduced_battery_usage_descr">अधिक सुधार जल्द ही आ रहे हैं!</string>
<string name="copy_verb">नकलना</string>
<string name="change_role">भूमिका बदलें</string>
<string name="ttl_hour">%d घंटा</string>
<string name="auth_unavailable">प्रमाणीकरण अनुपलब्ध</string>
<string name="icon_descr_edited">संपादित</string>
<string name="delete_group_question">समूह मिटाएं\?</string>
<string name="contact_already_exists">संपर्क पहले से मौजूद है</string>
<string name="notification_preview_mode_contact">संपर्क नाम</string>
<string name="notification_preview_somebody">संपर्क छुपाया गया:</string>
<string name="delete_verb">मिटाना</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">जब आप पृष्ठभूमि में 30 सेकंड के बाद ऐप को प्रारंभ या फिर से शुरू करते हैं तो आपको प्रमाणित करने की आवश्यकता होगी।</string>
<string name="icon_descr_asked_to_receive">प्रतिमा प्राप्त करने को कहा</string>
<string name="back">वापस</string>
<string name="icon_descr_close_button">बंद करें बटन</string>
<string name="create_address">पता बनाना</string>
<string name="invite_prohibited">संपर्क आमंत्रित नहीं कर सकते!</string>
<string name="icon_descr_contact_checked">संपर्क जांचा गया</string>
<string name="contact_preferences">संपर्क वरीयताएँ</string>
<string name="rcv_group_event_changed_your_role">आपकी भूमिका को %s में बदल दिया</string>
<string name="clear_contacts_selection_button">साफ़</string>
<string name="timed_messages">गायब होने वाले संदेश</string>
<string name="display_name__field">प्रदर्शित होने वाला नाम:</string>
<string name="theme_dark">अँधेरा</string>
<string name="allow_your_contacts_to_send_voice_messages">अपने संपर्कों को ध्वनि संदेश भेजने की अनुमति दें।</string>
<string name="ttl_day">%d दिन</string>
<string name="ttl_hours">%d घंटे</string>
<string name="v4_6_chinese_spanish_interface">चीनी और स्पेनिश इंटरफ़ेस</string>
<string name="simplex_link_mode_description">विवरण</string>
<string name="notifications_mode_off_desc">ऐप केवल तभी सूचनाएं प्राप्त कर सकता है जब वह चल रहा हो, कोई पृष्ठभूमि सेवा प्रारंभ नहीं की जाएगी</string>
<string name="edit_verb">संपादन करना</string>
<string name="decentralized">विकेन्द्रीकृत</string>
<string name="icon_descr_call_ended">कॉल समाप्त</string>
<string name="icon_descr_call_progress">कॉल चल रहा है</string>
<string name="auto_accept_images">छवियों को स्वत: स्वीकार करें</string>
<string name="chat_archive_header">चैट संग्रह</string>
<string name="chat_is_stopped_indication">चैट रोक दी गई है</string>
<string name="chat_archive_section">चैट संग्रह</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">आप इस समूह से संदेश प्राप्त करना बंद कर देंगे। चैट इतिहास संरक्षित किया जाएगा।</string>
<string name="rcv_group_event_changed_member_role">%s की भूमिका को %s में बदला</string>
<string name="group_member_status_complete">पूर्ण</string>
<string name="disappearing_prohibited_in_this_chat">इस चैट में गायब होने वाले संदेश प्रतिबंधित हैं।</string>
<string name="disappearing_messages_are_prohibited">इस चैट में गायब होने वाले संदेश प्रतिबंधित हैं।</string>
<string name="chat_with_developers">डेवलपर्स के साथ चैट करें</string>
<string name="alert_title_contact_connection_pending">संपर्क अभी तक जुड़ा नहीं है!</string>
<string name="chat_console">चैट कंसोल</string>
<string name="all_your_contacts_will_remain_connected">आपके सभी संपर्क जुड़े रहेंगे।</string>
<string name="network_session_mode_user">चैट प्रोफ़ाइल</string>
<string name="contact_requests">संपर्क अनुरोध</string>
<string name="create_profile_button">बनाएं</string>
<string name="callstatus_error">कॉल त्रुटि</string>
<string name="callstatus_in_progress">कॉल चल रहा है</string>
<string name="always_use_relay">हमेशा रिले का प्रयोग करें</string>
<string name="icon_descr_audio_call">ऑडियो कॉल</string>
<string name="settings_audio_video_calls">ऑडियो और वीडियो कॉल</string>
<string name="call_already_ended">कॉल पहले ही खत्म हो चुकी है!</string>
<string name="no_call_on_lock_screen">अक्षम करना</string>
<string name="status_contact_has_e2e_encryption">संपर्क में ई2ई एन्क्रिप्शन है</string>
<string name="status_contact_has_no_e2e_encryption">संपर्क में कोई ई2ई एन्क्रिप्शन नहीं है</string>
<string name="chat_is_running">चैट चल रही है</string>
<string name="chat_is_stopped">चैट रोक दी गई है</string>
<string name="rcv_conn_event_switch_queue_phase_changing">पता बदल रहा है…</string>
<string name="ttl_min">%d मिनट</string>
<string name="settings_section_title_device">उपकरण</string>
<string name="group_member_status_creator">रचनाकार</string>
<string name="change_member_role_question">समूह भूमिका बदलें\?</string>
<string name="ttl_week">%d सप्ताह</string>
<string name="v4_2_auto_accept_contact_requests">संपर्क अनुरोधों को स्वत: स्वीकार करें</string>
<string name="integrity_msg_bad_hash">खराब संदेश हैश</string>
<string name="allow_voice_messages_only_if">ध्वनि संदेशों को केवल तभी अनुमति दें यदि आपका संपर्क उन्हें अनुमति देता है।</string>
<string name="incognito_random_profile_from_contact_description">जिस संपर्क से आपने यह लिंक प्राप्त किया है, उसे एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string>
<string name="callstatus_calling">कॉल कर रहा है…</string>
<string name="feature_cancelled_item">रद्द %s</string>
<string name="rcv_conn_event_switch_queue_phase_completed">आपके लिए पता बदल दिया</string>
<string name="notifications_mode_periodic_desc">प्रत्येक 10 मिनट में 1 मिनट तक नए संदेशों की जाँच करता है</string>
<string name="ttl_sec">%d सेकंड</string>
<string name="ttl_month">%d महीना</string>
<string name="ttl_months">%d महीने</string>
<string name="settings_developer_tools">डेवलपर उपकरण</string>
<string name="incognito_random_profile_description">आपके संपर्क को एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string>
<string name="snd_conn_event_switch_queue_phase_changing">पता बदल रहा है…</string>
<string name="button_add_welcome_message">स्वागत संदेश जोड़ें</string>
<string name="allow_your_contacts_irreversibly_delete">अपने संपर्कों को भेजे गए संदेशों को अपरिवर्तनीय रूप से हटाने की अनुमति दें।</string>
<string name="allow_to_send_voice">ध्वनि संदेश भेजने की अनुमति दें।</string>
<string name="alert_title_cant_invite_contacts">संपर्कों को आमंत्रित नहीं कर सकते!</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s के लिए पता बदल रहा है…</string>
<string name="dont_show_again">दोबारा मत दिखाओ</string>
<string name="both_you_and_your_contact_can_send_voice">आप और आपका संपर्क दोनों ध्वनि संदेश भेज सकते हैं।</string>
<string name="settings_section_title_develop">विकास करना</string>
<string name="user_mute">आवाज़ बंद करना</string>
<string name="allow_your_contacts_to_send_disappearing_messages">अपने संपर्कों को गायब होने वाले संदेश भेजने की अनुमति दें।</string>
<string name="cancel_file__question">फ़ाइल स्थानांतरण रद्द करें\?</string>
<string name="v4_6_audio_video_calls">ऑडियो और वीडियो कॉल</string>
<string name="v4_6_reduced_battery_usage_descr">अधिक सुधार जल्द ही आ रहे हैं!</string>
<string name="icon_descr_video_asked_to_receive">वीडियो प्राप्त करने के लिए कहा</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">संपर्क और सभी संदेशों को हटा दिया जाएगा - इसे पूर्ववत नहीं किया जा सकता!</string>
<string name="ok">ठीक है</string>
<string name="save_servers_button">बचाना</string>
<string name="language_system">प्रणाली</string>
<string name="theme_system">प्रणाली</string>
<string name="network_options_save">बचाना</string>
<string name="chat_preferences_on">चालू</string>
<string name="la_lock_mode_system">प्रणाली प्रमाणीकरण</string>
<string name="la_mode_system">प्रणाली</string>
<string name="your_SMP_servers">आपका एसएमपी सर्वर</string>
<string name="messages_section_title">संदेशों</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">सदस्य को समूह से निकाल दिया जाएगा - इसे पूर्ववत नहीं किया जा सकता!</string>
<string name="member_info_section_title_member">सदस्य</string>
<string name="group_member_role_member">सदस्य</string>
</resources>
@@ -152,7 +152,7 @@
<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_image">Condividi multimediale…</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>
@@ -298,7 +298,7 @@
<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="choose_file">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>
@@ -308,6 +308,8 @@
<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="gallery_image_button">Immagine</string>
<string name="gallery_video_button">Video</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>
@@ -545,7 +547,7 @@
<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="use_camera_button">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>
@@ -665,9 +667,7 @@
<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="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. I 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>
@@ -696,11 +696,9 @@
<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>
\n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni.
\n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio.
\n3. La connessione è stata compromessa.</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>
@@ -728,7 +726,7 @@
<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="settings_section_title_messages">MESSAGGI E FILE</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>
@@ -819,7 +817,7 @@
<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="num_contacts_selected">%d 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>
@@ -927,7 +925,6 @@
<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>
@@ -948,7 +945,6 @@
<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>
@@ -986,7 +982,7 @@
<string name="save_welcome_message_question">Salvare il messaggio di benvenuto\?</string>
<string name="group_welcome_title">Messaggio di benvenuto</string>
<string name="save_and_update_group_profile">Salva e aggiorna il profilo del gruppo</string>
<string name="enter_password_to_show">Inserisci la password sopra per mostrare!</string>
<string name="enter_password_to_show">Inserisci password nella ricerca</string>
<string name="user_mute">Silenzia</string>
<string name="tap_to_activate_profile">Tocca per attivare il profilo.</string>
<string name="user_unhide">Svela</string>
@@ -1023,4 +1019,124 @@
<string name="user_hide">Nascondi</string>
<string name="v4_6_group_welcome_message_descr">Imposta il messaggio mostrato ai nuovi membri!</string>
<string name="user_unmute">Riattiva audio</string>
<string name="database_downgrade">Downgrade del database</string>
<string name="database_upgrade">Aggiornamento del database</string>
<string name="incompatible_database_version">Versione del database incompatibile</string>
<string name="upgrade_and_open_chat">Aggiorna e apri chat</string>
<string name="developer_options">ID del database e opzione isolamento del trasporto.</string>
<string name="hide_dev_options">Nascondi:</string>
<string name="show_dev_options">Mostra:</string>
<string name="show_developer_options">Mostra opzioni sviluppatore</string>
<string name="downgrade_and_open_chat">Esegui downgrade e apri chat</string>
<string name="database_migrations">Migrazioni: %s</string>
<string name="database_downgrade_warning">Attenzione: potresti perdere alcuni dati!</string>
<string name="confirm_database_upgrades">Conferma aggiornamenti database</string>
<string name="mtr_error_different">migrazione diversa nell\'app/nel database: %s / %s</string>
<string name="invalid_migration_confirmation">Conferma di migrazione non valida</string>
<string name="settings_section_title_experimenta">SPERIMENTALE</string>
<string name="image_will_be_received_when_contact_completes_uploading">L\'immagine verrà ricevuta quando il tuo contatto completerà l\'invio.</string>
<string name="mtr_error_no_down_migration">la versione del database è più recente di quella dell\'app, ma nessuna migrazione downgrade per: %s</string>
<string name="file_will_be_received_when_contact_completes_uploading">Il file verrà ricevuto quando il tuo contatto completerà l\'invio.</string>
<string name="cancel_file__question">Annullare il trasferimento di file\?</string>
<string name="file_transfer_will_be_cancelled_warning">Il trasferimento di file verrà annullato. Se è in corso, verrà interrotto.</string>
<string name="unhide_chat_profile">Svela il profilo chat</string>
<string name="unhide_profile">Svela profilo</string>
<string name="delete_chat_profile">Elimina il profilo di chat</string>
<string name="delete_profile">Elimina profilo</string>
<string name="profile_password">Password del profilo</string>
<string name="videos_limit_desc">È possibile inviare solo 10 video contemporaneamente</string>
<string name="videos_limit_title">Troppi video!</string>
<string name="icon_descr_video_asked_to_receive">Richiesta di ricevere il video</string>
<string name="video_descr">Video</string>
<string name="icon_descr_video_snd_complete">Video inviato</string>
<string name="video_will_be_received_when_contact_completes_uploading">Il video verrà ricevuto quando il tuo contatto completerà l\'invio.</string>
<string name="video_will_be_received_when_contact_is_online">Il video verrà ricevuto quando il tuo contatto sarà in linea, attendi o controlla più tardi!</string>
<string name="icon_descr_waiting_for_video">In attesa del video</string>
<string name="waiting_for_video">In attesa del video</string>
<string name="error_loading_xftp_servers">Errore nel caricamento dei server XFTP</string>
<string name="error_saving_xftp_servers">Errore nel salvataggio dei server XFTP</string>
<string name="error_xftp_test_server_auth">Il server richiede l\'autorizzazione per l\'invio, controlla la password</string>
<string name="smp_server_test_compare_file">Confronta file</string>
<string name="smp_server_test_create_file">Crea file</string>
<string name="smp_server_test_download_file">Scarica file</string>
<string name="smp_server_test_upload_file">Invia file</string>
<string name="xftp_servers">Server XFTP</string>
<string name="your_XFTP_servers">I tuoi server XFTP</string>
<string name="network_socks_proxy_settings">Impostazioni proxy SOCKS</string>
<string name="network_socks_toggle_use_socks_proxy">Usa proxy SOCKS</string>
<string name="host_verb">Host</string>
<string name="port_verb">Porta</string>
<string name="network_proxy_port">porta %d</string>
<string name="disable_onion_hosts_when_not_supported">Imposta <i>Usa gli host .onion</i> su No se il proxy SOCKS non li supporta.</string>
<string name="smp_server_test_delete_file">Elimina file</string>
<string name="error_loading_smp_servers">Errore nel caricamento dei server SMP</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi del server XFTP siano nel formato corretto, uno per riga e non doppi.</string>
<string name="la_auth_failed">Autenticazione fallita</string>
<string name="la_lock_mode_passcode">Inserimento del codice di accesso</string>
<string name="la_lock_mode">Modalità di SimpleX Lock</string>
<string name="la_lock_mode_system">Autenticazione di sistema</string>
<string name="la_authenticate">Autentica</string>
<string name="la_change_app_passcode">Cambia codice di accesso</string>
<string name="la_current_app_passcode">Codice di accesso attuale</string>
<string name="la_minutes">%d minuti</string>
<string name="la_seconds">%d secondi</string>
<string name="la_enter_app_passcode">Inserisci il codice di accesso</string>
<string name="la_immediately">Immediatamente</string>
<string name="la_no_app_password">Nessun codice di accesso dell\'app</string>
<string name="la_please_remember_to_store_password">Ricordala o conservala in modo sicuro: non c\'è modo di recuperare una password persa!</string>
<string name="lock_not_enabled">SimpleX Lock non attivato!</string>
<string name="you_can_turn_on_lock">Puoi attivare SimpleX Lock tramite le impostazioni.</string>
<string name="la_could_not_be_verified">Non è stato possibile verificarti, riprova.</string>
<string name="confirm_passcode">Conferma il codice di accesso</string>
<string name="la_mode_passcode">Codice di accesso</string>
<string name="passcode_changed">Codice di accesso cambiato!</string>
<string name="passcode_not_changed">Codice di accesso non cambiato!</string>
<string name="passcode_set">Codice di accesso impostato!</string>
<string name="submit_passcode">Invia</string>
<string name="la_mode_system">Sistema</string>
<string name="change_lock_mode">Cambia modalità di blocco</string>
<string name="authentication_cancelled">Autenticazione annullata</string>
<string name="lock_after">Blocca dopo</string>
<string name="enable_lock">Attiva blocco</string>
<string name="incorrect_passcode">Codice di accesso errato</string>
<string name="new_passcode">Nuovo codice di accesso</string>
<string name="lock_mode">Modalità di blocco</string>
<string name="decryption_error">Errore di decifrazione</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> messaggi saltati.</string>
<string name="alert_title_msg_bad_id">ID del messaggio errato</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Può accadere quando tu o il tuo contatto avete usato il backup del database vecchio.</string>
<string name="alert_text_msg_bad_id">L\'ID del messaggio successivo non è corretto (inferiore o uguale al precedente).
\nPuò accadere a causa di qualche bug o quando la connessione è compromessa.</string>
<string name="alert_text_fragment_permanent_error_reconnect">L\'errore è permanente per questa connessione, riconnettiti.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> messaggi non decifrati.</string>
<string name="alert_title_msg_bad_hash">Hash del messaggio errato</string>
<string name="alert_text_msg_bad_hash">L\'hash del messaggio precedente è diverso.</string>
<string name="alert_text_fragment_please_report_to_developers">Si prega di segnalarlo agli sviluppatori.</string>
<string name="stop_snd_file__message">L\'invio del file verrà interrotto.</string>
<string name="stop_file__action">Ferma file</string>
<string name="stop_snd_file__title">Fermare l\'invio del file\?</string>
<string name="revoke_file__message">Il file verrà eliminato dai server.</string>
<string name="stop_rcv_file__message">La ricezione del file verrà interrotta.</string>
<string name="revoke_file__confirm">Revoca</string>
<string name="revoke_file__action">Revoca file</string>
<string name="revoke_file__title">Revocare il file\?</string>
<string name="stop_file__confirm">Ferma</string>
<string name="stop_rcv_file__title">Fermare la ricezione del file\?</string>
<string name="no_spaces">Niente spazi!</string>
<string name="allow_calls_only_if">Consenti le chiamate solo se il contatto le consente.</string>
<string name="calls_prohibited_with_this_contact">Le chiamate audio/video sono vietate.</string>
<string name="both_you_and_your_contact_can_make_calls">Sia tu che il tuo contatto potete effettuare chiamate.</string>
<string name="only_you_can_make_calls">Solo tu puoi effettuare chiamate.</string>
<string name="only_your_contact_can_make_calls">Solo il tuo contatto può effettuare chiamate.</string>
<string name="prohibit_calls">Proibisci le chiamate audio/video.</string>
<string name="audio_video_calls">Chiamate audio/video</string>
<string name="available_in_v51">"
\nDisponibile nella v5.1"</string>
<string name="allow_your_contacts_to_call">Consenti ai tuoi contatti di chiamarti.</string>
<string name="v5_0_app_passcode">Codice di accesso dell\'app</string>
<string name="v5_0_large_files_support_descr">Veloce e senza aspettare che il mittente sia in linea!</string>
<string name="v5_0_polish_interface">Interfaccia polacca</string>
<string name="v5_0_app_passcode_descr">Impostala al posto dell\'autenticazione di sistema.</string>
<string name="v5_0_polish_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="v5_0_large_files_support">Video e file fino a 1 GB</string>
</resources>
@@ -540,7 +540,6 @@
<string name="markdown_help">マークダウン (書式編集) ガイド</string>
<string name="smp_servers_enter_manually">サーバを手動で入力</string>
<string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。</string>
<string name="core_build_timestamp">コアのビルド@: %s</string>
<string name="create_address">アドレスを作成</string>
<string name="delete_address__question">アドレスを削除しますか?</string>
<string name="display_name__field">表示の名前:</string>
@@ -802,7 +801,7 @@
<string name="you_sent_group_invitation">グループの招待を送りました</string>
<string name="snd_group_event_changed_member_role">%sの役割を次に変えました:%s</string>
<string name="button_send_direct_message">ダイレクトメッセージを送信</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g>連絡先が選択中</string>
<string name="num_contacts_selected">%d 連絡先が選択中</string>
<string name="snd_group_event_member_deleted">除名しました: <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="switch_verb">切り替える</string>
<string name="member_role_will_be_changed_with_notification">役割が「%s」となります。グループの全員に通知が出ます。</string>
@@ -812,7 +811,6 @@
<string name="network_options_save">保存</string>
<string name="update_network_settings_question">ネットワーク設定を更新しますか?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">設定を更新すると、全サーバにクライントの再接続が行われます。</string>
<string name="your_chat_profiles_stored_locally">チャットプロフィールはローカルであなたの端末だけに保存されます。</string>
<string name="save_color">色を保存</string>
<string name="chat_preferences_you_allow">あなたが次を許可しています:</string>
<string name="chat_preferences_yes">はい</string>
@@ -844,9 +842,7 @@
<string name="this_string_is_not_a_connection_link">このストリングは接続リンクではありません!</string>
<string name="chat_with_the_founder">質問やアイデアを送る</string>
<string name="star_on_github">GithubでStar</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="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">あなたのプロフィールはご自分の端末に保存され、あなたの連絡先のみに共有されます。 <xliff:g id="appName">SimpleX</xliff:g>のサーバには開示されません。</string>
<string name="your_calls">あなたの通話</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g>飛ばしたメッセージ</string>
<string name="send_link_previews">リンクのプレビューを送信</string>
@@ -0,0 +1,896 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="server_connected">연결됨</string>
<string name="server_connecting">연결 중</string>
<string name="connect_via_group_link">그룹 링크를 통해 연결할까요\?</string>
<string name="connect_via_invitation_link">초대 링크로 연결할까요\?</string>
<string name="display_name_connection_established">연결 수립됨</string>
<string name="connection_timeout">연결 시간 초과</string>
<string name="cannot_receive_file">파일을 받을 수 없습니다</string>
<string name="contact_already_exists">이미 추가된 연락처에요.</string>
<string name="smp_server_test_connect">연결</string>
<string name="connection_error_auth">연결 오류 (인증)</string>
<string name="smp_server_test_create_queue">대기열 만들기</string>
<string name="database_initialization_error_title">데이터베이스를 초기화할 수 없어요</string>
<string name="notifications_mode_service_desc">앱이 백그라운드에서 항상 실행되어요. 대신 메시지가 도착하자마자 바로 알림이 와요.</string>
<string name="notifications_mode_periodic_desc">10분마다 최대 1분간 새 메시지 확인</string>
<string name="notification_contact_connected">연결됨</string>
<string name="notification_preview_somebody">숨긴 대화 상대 :</string>
<string name="notification_preview_mode_contact">대화 상대 이름</string>
<string name="allow_verb">허용</string>
<string name="auth_confirm_credential">자격 증명 확인</string>
<string name="copy_verb">복사</string>
<string name="contact_connection_pending">연결 중…</string>
<string name="group_connection_pending">연결 중…</string>
<string name="attach">첨부파일</string>
<string name="icon_descr_cancel_file_preview">파일 미리보기 취소</string>
<string name="icon_descr_context">컨텍스트 아이콘</string>
<string name="icon_descr_server_status_connected">연결됨</string>
<string name="back">뒤로</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>새 연락처 추가</b> : 일회용 QR 코드 만들기</string>
<string name="cancel_verb">취소</string>
<string name="icon_descr_cancel_live_message">라이브 메시지 취소</string>
<string name="choose_file">파일 선택</string>
<string name="confirm_verb">확인</string>
<string name="connect_via_link_or_qr">링크 / QR 코드를 통해 연결</string>
<string name="copied">클립보드로 복사됨</string>
<string name="create_group">비밀 그룹 생성</string>
<string name="accept_contact_button">수락</string>
<string name="clear_chat_warning">모든 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요! 메시지는 나에게서만 삭제돼요.</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="connection_request_sent">연결 요청 완료</string>
<string name="connect_via_link">링크를 통해 연결</string>
<string name="smp_servers_preset_add">프리셋 서버 추가하기</string>
<string name="smp_servers_add">서버 추가…</string>
<string name="chat_console">채팅 콘솔</string>
<string name="smp_servers_check_address">서버 주소를 확인 후 다시 시도해 주세요.</string>
<string name="configure_ICE_servers">ICE 서버 설정</string>
<string name="contribute">기여</string>
<string name="network_settings">고급 네트워크 설정</string>
<string name="network_session_mode_user">채팅 프로필</string>
<string name="network_session_mode_entity">연결</string>
<string name="accept_requests">요청 수락</string>
<string name="app_version_code">앱 빌드 : %s</string>
<string name="appearance_settings">외관</string>
<string name="app_version_title">앱 버전</string>
<string name="app_version_name">앱 버전 : v%s</string>
<string name="accept_automatically">자동</string>
<string name="contact_requests">대화 상대의 요청</string>
<string name="core_version">코어 버전 : v%s</string>
<string name="callstatus_accepted">전화 받음</string>
<string name="bold">굵게</string>
<string name="callstatus_in_progress">전화 연결 중</string>
<string name="colored">색깔</string>
<string name="confirm_password">암호 확인</string>
<string name="callstatus_connecting">전화 연결 중</string>
<string name="create_profile_button">생성</string>
<string name="create_profile">프로필 생성</string>
<string name="callstatus_error">통화 오류</string>
<string name="callstate_connected">연결됨</string>
<string name="callstate_connecting">연결 중…</string>
<string name="create_your_profile">내 프로필 생성</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>배터리에 좋음</b>. 백그라운드 서비스는 10분마다 새 메시지를 확인합니다. 전화 및 긴급 메시지를 놓칠 수 있습니다.</string>
<string name="call_already_ended">전화가 이미 종료되었습니다!</string>
<string name="always_use_relay">항상 릴레이 사용</string>
<string name="icon_descr_audio_call">음성 전화</string>
<string name="settings_audio_video_calls">음성 &amp; 영상 전화</string>
<string name="call_on_lock_screen">잠금 화면에서의 전화</string>
<string name="status_contact_has_no_e2e_encryption">대화 상대와 종단간 암호화되지 않음</string>
<string name="answer_call">응답</string>
<string name="icon_descr_audio_on">소리 켜기</string>
<string name="icon_descr_audio_off">소리 끄기</string>
<string name="icon_descr_call_ended">통화 종료됨</string>
<string name="icon_descr_call_connecting">전화 연결 중</string>
<string name="auto_accept_images">이미지 자동 다운로드하기</string>
<string name="integrity_msg_bad_hash">잘못된 메시지 해시</string>
<string name="integrity_msg_bad_id">잘못된 메시지 아이디</string>
<string name="settings_section_title_calls">전화</string>
<string name="chat_is_running">채팅 기능이 작동하고 있어요</string>
<string name="settings_section_title_chats">채팅</string>
<string name="chat_database_imported">채팅 데이테베이스를 불러 왔어요</string>
<string name="impossible_to_recover_passphrase"><b>주의</b>: 비밀번호를 분실하면 복구나 비밀번호 변경을 할 수 없어요.</string>
<string name="change_database_passphrase_question">데이터베이스 암호를 바꾸겠습니까\?</string>
<string name="confirm_new_passphrase">새로운 암호 확인…</string>
<string name="chat_archive_section">채팅 기록 보관함</string>
<string name="rcv_group_event_changed_your_role">내 역할이 %s 역할로 변경됨.</string>
<string name="rcv_conn_event_switch_queue_phase_changing">주소 바꾸는 중…</string>
<string name="snd_conn_event_switch_queue_phase_changing">주소 바꾸는 중…</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s의 주소 바꾸는 중…</string>
<string name="rcv_group_event_member_connected">연결됨</string>
<string name="group_member_status_complete">완료</string>
<string name="group_member_status_connected">연결됨</string>
<string name="group_member_status_connecting">연결 중</string>
<string name="group_member_status_accepted">연결 중 (수락됨)</string>
<string name="group_member_status_announced">연결 중 (알림)</string>
<string name="group_member_status_intro_invitation">연결 중(초대 시작)</string>
<string name="group_member_status_creator">제작자</string>
<string name="invite_prohibited">상대를 초대할 수 없습니다</string>
<string name="clear_contacts_selection_button">지우기</string>
<string name="icon_descr_contact_checked">대화 상대 확인됨</string>
<string name="create_group_link">그룹 링크 생성</string>
<string name="button_create_group_link">링크 생성</string>
<string name="change_member_role_question">그룹 역할을 바꾸겠습니까\?</string>
<string name="info_row_connection">연결</string>
<string name="users_add">프로필 추가</string>
<string name="incognito_random_profile_description">대화 상대에게 랜덤으로 만들어진 익명 프로필이 보내져요</string>
<string name="cant_delete_user_profile">사용자 프로필을 삭제할 수 없습니다</string>
<string name="chat_preferences_always">항상</string>
<string name="chat_preferences_contact_allows">대화 상대가 허용했어요.</string>
<string name="contact_preferences">연락처 개별 설정</string>
<string name="allow_voice_messages_only_if">대화 상대도 허용한 경우에만 음성 메시지를 보낼 수 있습니다.</string>
<string name="allow_your_contacts_irreversibly_delete">모두에게서 메시지 영구 삭제 허용하기.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">대화 상대에게 자동 삭제되는 메시지 허용하기.</string>
<string name="allow_your_contacts_to_send_voice_messages">상대가 음성 메시지를 보내는 것을 허용하기.</string>
<string name="both_you_and_your_contacts_can_delete">나와 대화 상대 모두 메시지를 영구 삭제할 수 있어요.</string>
<string name="both_you_and_your_contact_can_send_voice">나와 대화 상대 모두 음성 메시지를 보낼 수 있어요.</string>
<string name="contacts_can_mark_messages_for_deletion">상대가 메시지에 삭제 표시를 할 수 있습니다. 그러나 삭제 표시된 메시지 내용은 여전히 볼 수 있습니다.</string>
<string name="allow_to_delete_messages">모두에게서 메시지 영구 삭제 허용하기.</string>
<string name="feature_cancelled_item">%s 취소됨</string>
<string name="v4_2_auto_accept_contact_requests">대화 상대의 요청 자동 수락</string>
<string name="v4_5_transport_isolation_descr">채팅 프로필(기본값) 또는 연결(베타).</string>
<string name="v4_4_verify_connection_security_desc">대화 상대와 보안 코드를 비교해 보세요.</string>
<string name="v4_6_chinese_spanish_interface">중국어 및 스페인어 인터페이스</string>
<string name="about_simplex">SimpleX에 대하여</string>
<string name="accept">수락</string>
<string name="share_one_time_link">일회용 초대 링크 만들기</string>
<string name="create_address">주소 생성</string>
<string name="chat_item_ttl_day">1일</string>
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX</xliff:g>에 대하여</string>
<string name="color_primary">강조 색상</string>
<string name="accept_call_on_lock_screen">응답</string>
<string name="accept_connection_request__question">연결 요청을 수락할까요\?</string>
<string name="accept_feature">수락</string>
<string name="network_enable_socks_info">SOCKS 프록시(포트 9050)를 통해 서버에 액세스할까요\? 이 설정을 활성화하기 전에 프록시를 시작해야 해요.</string>
<string name="smp_servers_add_to_another_device">다른 기기에 추가</string>
<string name="v4_3_improved_server_configuration_desc">QR 코드 스캔으로 서버 추가</string>
<string name="button_add_welcome_message">환영 메시지 추가</string>
<string name="group_member_role_admin">관리자</string>
<string name="v4_2_group_links_desc">관리자는 그룹 가입을 위한 링크를 만들 수 있어요.</string>
<string name="allow_to_send_disappearing">자동 삭제되는 메시지 허용하기.</string>
<string name="users_delete_all_chats_deleted">모든 채팅과 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요!</string>
<string name="allow_to_send_voice">음성 메시지 허용하기.</string>
<string name="allow_voice_messages_question">음성 메시지를 허용하겠습니까\?</string>
<string name="allow_disappearing_messages_only_if">상대도 허용하는 경우에만 자동 삭제되는 메시지를 사용할 수 있어요.</string>
<string name="allow_direct_messages">그룹 멤버에게 1:1 채팅 허용하기.</string>
<string name="all_group_members_will_remain_connected">모든 그룹 멤버는 연결 상태가 계속 유지돼요.</string>
<string name="allow_irreversible_message_deletion_only_if">상대도 허용한 경우에만 모두에게서 메시지 영구 삭제가 가능해요.</string>
<string name="all_your_contacts_will_remain_connected">모든 연락처와 연결 상태가 계속 유지돼요.</string>
<string name="notifications_mode_service">항상 켜기</string>
<string name="keychain_is_storing_securely">안드로이드 암호 저장소는 비밀번호를 안전하게 저장하는 데 사용되고 알림이 작동하도록 해요.</string>
<string name="keychain_allows_to_receive_ntfs">안드로이드 암호 저장소는 앱을 다시 시작하거나 비밀번호 변경을 하고 나서 비밀번호를 안전하게 저장하는 데 사용되고 알림이 작동되도록 해요.</string>
<string name="notifications_mode_off_desc">앱이 실행 중일 때만 알림을 받을 수 있고 백그라운드에서 실행되지 않아요.</string>
<string name="full_backup">앱 데이터 백업</string>
<string name="settings_section_title_icon">앱 아이콘</string>
<string name="incognito_random_profile_from_contact_description">링크를 보낸 사람한테 랜덤으로 만들어진 익명 프로필이 보내져요</string>
<string name="network_session_mode_user_description">별도로 분리된 TCP 연결(그리고 SOCKS 자격 증명)이 <b>각각의 채팅 프로필</b>에 사용될 거예요.</string>
<string name="network_session_mode_entity_description">별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 대화 상대 및 그룹 구성원</b>에게 사용될 거예요.
\n<b>참고</b>: 연결이 많은 경우 배터리 및 트래픽 소비가 엄청 높을 수 있고 일부 연결이 실패할 수 있어요.</string>
<string name="icon_descr_asked_to_receive">이미지 수신 요청됨</string>
<string name="v4_6_audio_video_calls">음성 및 영상 전화</string>
<string name="audio_call_no_encryption">음성 전화 (종단간 암호화 X)</string>
<string name="auth_unavailable">인증할 수 없어요</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_contact_can_send_disappearing">나와 대화 상대 모두 자동 삭제되는 메시지를 보낼 수 있어요.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR 코드 스캔</b>: QR 코드를 보여주는 사람과 연결할 수 있어요.</string>
<string name="cannot_access_keychain">데이터베이스 암호를 저장하고 있는 암호키 저장소에 접근할 수 없습니다</string>
<string name="onboarding_notifications_mode_service_desc"><b>배터리 많이 사용</b>! 백그라운드에서 항상 실행돼요. 메시지를 수신하자마자 알림이 떠요.</string>
<string name="callstatus_ended">통화 종료됨 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_calling">전화 중…</string>
<string name="icon_descr_call_progress">전화 연결 중</string>
<string name="icon_descr_cancel_link_preview">링크 미리보기 취소</string>
<string name="icon_descr_cancel_image_preview">이미지 미리보기 취소</string>
<string name="rcv_group_event_changed_member_role">%s 에서 %s로 역할이 변경됨</string>
<string name="chat_database_section">채팅 데이터베이스</string>
<string name="alert_title_cant_invite_contacts">대화 상대를 초대할 수 없습니다!</string>
<string name="change_verb">변경</string>
<string name="chat_archive_header">채팅 기록 보관함</string>
<string name="change_role">역할 바꾸기</string>
<string name="chat_database_deleted">채팅 데이터베이스를 삭제했어요</string>
<string name="chat_is_stopped">채팅 기능이 멈췄어요</string>
<string name="chat_is_stopped_indication">채팅 기능이 멈췄어요</string>
<string name="chat_preferences">채팅 설정</string>
<string name="chat_with_developers">개발자와 대화</string>
<string name="connect_via_link_verb">연결</string>
<string name="display_name_connecting">연결 중…</string>
<string name="icon_descr_close_button">닫기 버튼</string>
<string name="connect_button">연결</string>
<string name="group_member_status_introduced">연결 중 (도입)</string>
<string name="connection_error">연결 오류</string>
<string name="connection_local_display_name">연결 <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="connect_via_contact_link">링크를 통해 연결하겠습니까\?</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">대화 상대와 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요!</string>
<string name="status_contact_has_e2e_encryption">대화 상대와 종단간 암호화됨</string>
<string name="alert_title_contact_connection_pending">대화 상대와 아직 연결되지 않았어요!</string>
<string name="archive_created_on_ts"><xliff:g id="archive_ts">%1$s</xliff:g>에 생성 완료</string>
<string name="create_one_time_link">일회용 초대 링크 생성</string>
<string name="create_secret_group_title">비밀 그룹 생성</string>
<string name="accept_contact_incognito_button">익명 수락</string>
<string name="chat_item_ttl_month">1개월</string>
<string name="chat_item_ttl_week">1주</string>
<string name="a_plus_b">a + b</string>
<string name="deleted_description">삭제됨</string>
<string name="simplex_link_mode_description">설명</string>
<string name="smp_server_test_delete_queue">대기열 삭제</string>
<string name="delete_verb">삭제</string>
<string name="delete_message__question">메시지를 삭제할까요\?</string>
<string name="for_me_only">나에게서만 삭제</string>
<string name="delete_member_message__question">멤버의 메시지를 삭제할까요\?</string>
<string name="maximum_supported_file_size">현재 지원되는 최대 파일 크기는 <xliff:g id="maxFileSize">%1$s</xliff:g> 에요.</string>
<string name="image_decoding_exception_title">디코딩 오류</string>
<string name="button_delete_contact">대화 상대 삭제</string>
<string name="delete_contact_question">연락처를 삭제할까요\?</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 데스크톱: <b>QR 코드 스캔</b>을 통해 앱에서 표시된 QR 코드를 스캔해 주세요.</string>
<string name="delete_contact_menu_action">삭제</string>
<string name="delete_group_menu_action">삭제</string>
<string name="delete_pending_connection__question">대기 중인 연결을 삭제할까요\?</string>
<string name="clear_verification">인증 취소</string>
<string name="database_passphrase_and_export">데이터베이스 비밀번호 &amp; 내보내기</string>
<string name="smp_servers_delete_server">서버 삭제</string>
<string name="delete_address">주소 삭제</string>
<string name="delete_address__question">주소를 삭제할까요\?</string>
<string name="delete_image">이미지 삭제</string>
<string name="decentralized">탈중앙화</string>
<string name="settings_section_title_develop">개발</string>
<string name="settings_developer_tools">개발자 도구</string>
<string name="settings_section_title_device">기기</string>
<string name="database_passphrase">데이터베이스 비밀번호</string>
<string name="delete_files_and_media_for_all_users">모든 채팅 프로필 파일 삭제</string>
<string name="database_error">데이터베이스 에러</string>
<string name="passphrase_is_different">데이터베이스 비밀번호가 암호 저장소에 저장된 것과 일치하지 않아요.</string>
<string name="database_passphrase_is_required">채팅을 열려면 데이터베이스 비밀번호가 필요해요.</string>
<string name="delete_archive">보관된 채팅 삭제</string>
<string name="delete_chat_archive_question">보관된 채팅을 삭제할까요\?</string>
<string name="num_contacts_selected">%d 개의 연락처가 선택되었어요.</string>
<string name="info_row_database_id">데이터베이스 아이디</string>
<string name="users_delete_profile_for">다음 채팅 프로필 삭제</string>
<string name="theme_dark">어둡게</string>
<string name="delete_after">다음 기간 이후 자동 삭제</string>
<string name="above_then_preposition_continuation">위 다음 :</string>
<string name="delete_database">데이터베이스 삭제</string>
<string name="set_password_to_export_desc">데이터베이스는 임의의 비밀번호로 암호화되었어요. 내보내기 기능 사용 전 비밀번호를 변경해 주세요.</string>
<string name="delete_files_and_media_question">파일과 미디어를 삭제할까요\?</string>
<string name="current_passphrase">현재 비밀번호…</string>
<string name="database_encrypted">데이터베이스 암호화 완료!</string>
<string name="database_passphrase_will_be_updated">데이터베이스 비밀번호가 업데이트되요.</string>
<string name="encrypted_with_random_passphrase">데이터베이스는 임의의 비밀번호로 암호화되었고, 원하시면 비밀번호를 변경할 수 있어요.</string>
<string name="database_will_be_encrypted">데이터베이스는 암호화될 거예요.</string>
<string name="delete_messages">메시지 삭제</string>
<string name="delete_messages_after">다음 기간 이후 자동 삭제</string>
<string name="rcv_group_event_group_deleted">삭제된 그룹</string>
<string name="delete_link">링크 삭제</string>
<string name="delete_link_question">링크를 삭제할까요\?</string>
<string name="chat_preferences_default">기본값 (%s)</string>
<string name="ttl_day">%d일</string>
<string name="ttl_d">%d일</string>
<string name="ttl_days">%d일</string>
<string name="button_delete_group">그룹 삭제</string>
<string name="rcv_conn_event_switch_queue_phase_completed">주소 변경됨</string>
<string name="database_encryption_will_be_updated">데이터베이스 비밀번호가 업데이트되고 암호 저장소에 보관됩니다.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">데이터베이스는 암호화되고, 비밀번호는 암호 저장소에 보관될 거에요.</string>
<string name="users_delete_question">채팅 프로필을 삭제할까요\?</string>
<string name="delete_files_and_media_all">모든 파일 삭제</string>
<string name="delete_chat_profile_question">채팅 프로필을 삭제할까요\?</string>
<string name="full_deletion">모두에게서 삭제</string>
<string name="delete_group_question">그룹을 삭제할까요\?</string>
<string name="failed_to_create_user_duplicate_title">표시 이름이 중복되어요!</string>
<string name="smp_server_test_disconnect">연결 끊기</string>
<string name="auth_device_authentication_is_disabled_turning_off">기기 인증이 비활성화되어 SimpleX 잠금 기능이 작동하지 않아요.</string>
<string name="auth_disable_simplex_lock">SimpleX 잠금 비활성화</string>
<string name="icon_descr_server_status_disconnected">연결 끊김</string>
<string name="add_contact">일회용 초대 링크</string>
<string name="add_contact_or_create_group">새로운 채팅 시작</string>
<string name="display_name__field">표시 이름</string>
<string name="display_name_cannot_contain_whitespace">표시 이름에는 공백문자가 쓰일 수 없어요.</string>
<string name="display_name">표시 이름</string>
<string name="encrypted_audio_call">종단간 암호화된 음성 전화</string>
<string name="encrypted_video_call">종단간 암호화된 영상 전화</string>
<string name="no_call_on_lock_screen">비활성화</string>
<string name="status_e2e_encrypted">종단간 암호화</string>
<string name="integrity_msg_duplicate">중복된 메시지</string>
<string name="accept_feature_set_1_day">1일로 설정</string>
<string name="v4_4_disappearing_messages">자동 삭제되는 메시지</string>
<string name="total_files_count_and_size">전체 크기가 %s인 %d개의 파일</string>
<string name="conn_level_desc_direct">다이렉트</string>
<string name="disappearing_prohibited_in_this_chat">이 채팅에서는 자동 삭제되는 메시지를 사용할 수 없어요.</string>
<string name="disappearing_messages_are_prohibited">이 그룹에서는 자동 삭제되는 메시지를 사용할 수 없어요.</string>
<string name="ttl_m">%d분</string>
<string name="ttl_months">%d 개월</string>
<string name="ttl_min">%d 분</string>
<string name="ttl_month">%d 개월</string>
<string name="ttl_week">%d 주</string>
<string name="downgrade_and_open_chat">다운그레이드하고 채팅 열기</string>
<string name="direct_messages">1:1 메시지</string>
<string name="timed_messages">자동 삭제되는 메시지</string>
<string name="direct_messages_are_prohibited_in_chat">이 그룹에서는 멤버들의 1:1 채팅이 금지되어 있어요.</string>
<string name="ttl_s">%d초</string>
<string name="ttl_sec">%d 초</string>
<string name="ttl_h">%d시</string>
<string name="ttl_mth">%d개월</string>
<string name="ttl_w">%d주</string>
<string name="ttl_weeks">%d 주</string>
<string name="confirm_database_upgrades">데이터베이스 업그레이드 확인</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">기기 인증을 하고 있지 않아요. 기기 인증을 켜면 설정에서 SimpleX 잠금 기능을 사용할 수 있어요.</string>
<string name="ttl_hour">%d 시간</string>
<string name="ttl_hours">%d 시간</string>
<string name="mtr_error_different">앱/데이터베이스의 다른 마이그레이션: %s / %s</string>
<string name="v4_5_multiple_chat_profiles_descr">다른 이름, 아바타 그리고 전송 격리.</string>
<string name="dont_show_again">다시 보지 않기</string>
<string name="connected_to_server_to_receive_messages_from_contact">이 대화 상대로부터의 메시지를 수신할 서버와 연결되었어요.</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="contact_developers">앱 업데이트 후 개발자에게 연락해 주세요.</string>
<string name="connection_error_auth_desc">대화 상대가 나갔거나 초대 링크가 이미 사용된 경우가 아니면 버그일 수 있어요. 이 경우 개발자에게 알려주세요.
\n대화 상대에게 다른 초대 링크 만들도록 부탁하고 네트워크 연결이 안정적인지 확인하세요.</string>
<string name="auth_enable_simplex_lock">SimpleX 잠금 활성화</string>
<string name="auth_log_in_using_credential">자격 증명으로 로그인</string>
<string name="auth_open_chat_console">채팅 콘솔 열기</string>
<string name="auth_stop_chat">채팅 기능 중지하기</string>
<string name="auth_unlock">잠금 해제하기</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">앱을 사용하지 않는 지 30초가 지나면 다시 인증해야 해요.</string>
<string name="contact_wants_to_connect_with_you">님이 연결하고 싶어해요!</string>
<string name="callstate_starting">시작…</string>
<string name="callstate_waiting_for_answer">응답 대기 중…</string>
<string name="callstate_waiting_for_confirmation">확인 대기 중…</string>
<string name="alert_title_skipped_messages">읽지 않는 메시지</string>
<string name="alert_title_cant_invite_contacts_descr">이 그룹에서 익명 프로필을 사용하고 있어요. 원래의 내 프로필이 노출되는 걸 방지하기 위해 대화 상대 초대가 허용되지 않아요.</string>
<string name="button_remove_member">멤버 삭제하기</string>
<string name="chat_item_ttl_seconds">%s 초</string>
<string name="alert_message_group_invitation_expired">이 링크로 참여할 수 없어요. 이미 삭제된 링크에요.</string>
<string name="alert_message_no_group">존재하지 않는 그룹이에요.</string>
<string name="alert_title_no_group">그룹을 찾을 수 없어요!</string>
<string name="button_add_members">맴버 초대하기</string>
<string name="button_welcome_message">환영 메시지</string>
<string name="button_edit_group_profile">그룹 프로필 수정</string>
<string name="button_leave_group">그룹 나가기</string>
<string name="button_send_direct_message">1:1 채팅 시작하기</string>
<string name="conn_stats_section_title_servers">서버</string>
<string name="conn_level_desc_indirect">인다이렉트 (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="chat_preferences_you_allow">허용함</string>
<string name="chat_preferences_off">꺼짐</string>
<string name="chat_preferences_on">켜짐</string>
<string name="chat_preferences_no">아니요</string>
<string name="ask_your_contact_to_enable_voice">대화 상대에게 음성 메시지 기능을 활성화 해달라고 부탁해보세요.</string>
<string name="chat_help_tap_button">탭 버튼</string>
<string name="connection_you_accepted_will_be_cancelled">수락한 연결이 취소됩니다!</string>
<string name="chat_lock">SimpleX 잠금</string>
<string name="callstatus_rejected">거절된 전화</string>
<string name="callstate_ended">종료됨</string>
<string name="call_connection_peer_to_peer">P2P</string>
<string name="call_connection_via_relay">릴레이를 경유</string>
<string name="alert_title_group_invitation_expired">만료된 초대 링크에요!</string>
<string name="chat_preferences_yes"></string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g>님이 다음을 통해 연결하려고 해요 :</string>
<string name="allow_accepting_calls_from_lock_screen">설정에서 잠금 화면에서 바로 전화를 받을 수 있도록 설정할 수 있어요.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">연결을 완료하려면 대화 상대가 온라인 상태여야 해요.
\n연결 요청을 취소하고 대화 상대를 삭제할 수 있어요 (그리고 새 링크로 재시도).</string>
<string name="alert_text_skipped_messages_it_can_happen_when">다음과 같은 경우에 발생할 수 있어요.
\n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우
\n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우
\n3. 침해된 연결의 경우
\n서버 업데이트를 받으려면 설정에서 개발자에게 연락해 주세요.
\n저희 개발팀은 메시지 손실을 방지하기 위해 중복된 서버를 추가할 예정이에요.</string>
<string name="auth_simplex_lock_turned_on">SimpleX 잠금 켜짐</string>
<string name="callstate_received_answer">응답됨…</string>
<string name="callstate_received_confirmation">확인 받음…</string>
<string name="callstatus_missed">부재 중 전화</string>
<string name="chat_item_ttl_none">사용 안 함</string>
<string name="chat_with_the_founder">질문이나 아이디어 보내기</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(스캔하거나 클립보드에서 붙여넣기)</string>
<string name="contact_sent_large_file">대화 상대가 현재 지원되는 최대 크기(<xliff:g id="maxFileSize">%1$s</xliff:g>)보다 큰 파일을 보냈습니다.</string>
<string name="display_name_invited_to_connect">초대를 받았어요.</string>
<string name="failed_to_create_user_title">프로필 생성 오류!</string>
<string name="description_via_group_link_incognito">그룹 링크로 익명 채팅</string>
<string name="description_via_group_link">그룹 링크로 채팅</string>
<string name="description_via_one_time_link">일회용 링크로 채팅</string>
<string name="description_you_shared_one_time_link_incognito">일회용 익명 링크를 공유했어요.</string>
<string name="description_you_shared_one_time_link">일회용 링크를 공유했어요.</string>
<string name="description_via_contact_address_link_incognito">상대의 연락처 링크로 익명 연결</string>
<string name="description_via_contact_address_link">상대의 연락처 링크로 연결</string>
<string name="description_via_one_time_link_incognito">일회용 연락처로 익명 연결</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">SMP 서버 주소가 올바른 형식이고 줄로 구분되어 있고 중복이 없는지 확인해 주세요.</string>
<string name="error_saving_smp_servers">SMP 서버 저장 오류</string>
<string name="error_setting_network_config">네트워크 설정 업데이트 오류</string>
<string name="failed_to_active_user_title">프로필 변경 오류!</string>
<string name="failed_to_create_user_duplicate_desc">동일한 표시 이름을 가진 채팅 프로필이 있어요. 다른 이름을 선택해 주세요.</string>
<string name="failed_to_parse_chats_title">채팅 불러오기 실패</string>
<string name="failed_to_parse_chat_title">채팅 불러오기 실패</string>
<string name="error_adding_members">멤버 추가 오류</string>
<string name="error_joining_group">그룹 참여 오류</string>
<string name="error_sending_message">메시지 전송 오류</string>
<string name="error_creating_address">주소 생성 오류</string>
<string name="error_receiving_file">파일 다운로드 오류</string>
<string name="error_accepting_contact_request">상대 요청 수락 오류</string>
<string name="error_changing_address">주소 변경 오류</string>
<string name="error_deleting_contact">연락처 삭제 오류</string>
<string name="error_deleting_contact_request">대화 요청 삭제 오류</string>
<string name="error_deleting_group">그룹 삭제 오류</string>
<string name="error_deleting_pending_contact_connection">대기 중 대화 요청 삭제 오류</string>
<string name="error_smp_test_certificate">서버 주소의 인증서의 지문(fingerprint)이 잘못되었을 수도 있어요.</string>
<string name="error_smp_test_failed_at_step">테스트가 %s단계에서 실패했어요.</string>
<string name="error_smp_test_server_auth">서버는 대기열을 생성하고 비밀번호를 확인하려면 인증이 필요해요.</string>
<string name="enter_passphrase_notification_desc">알림을 받으려면 데이터베이스 암호를 입력해 주세요.</string>
<string name="enter_passphrase_notification_title">비밀번호가 필요해요.</string>
<string name="error_deleting_user">프로필 삭제 오류</string>
<string name="error_updating_user_privacy">사용자 개인정보 업데이트 오류</string>
<string name="database_initialization_error_desc">데이터베이스가 올바르게 작동하지 안하요. 자세히 알아보려면 탭하세요.</string>
<string name="edit_verb">수정하기</string>
<string name="delete_message_cannot_be_undone_warning">메시지가 삭제돼요. 삭제 후 복구할 수 없어요!</string>
<string name="delete_message_mark_deleted_warning">메시지가 삭제 표시될 거예요. 대화 상대는 여전히 삭제된 내용을 볼 수 있어요.</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICE 서버 주소가 올바른 형식이고 줄로 구분되고 중복이 없는지 확인해 주세요.</string>
<string name="enter_one_ICE_server_per_line">ICE 서버(한 줄에 하나씩)</string>
<string name="error_saving_ICE_servers">ICE 서버 저장 오류</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="exit_without_saving">저장하지 않고 나가기</string>
<string name="encrypt_database">암호화</string>
<string name="encrypted_database">암호화된 데이터베이스</string>
<string name="feature_off">꺼짐</string>
<string name="feature_enabled_for_you">나에게 켜짐</string>
<string name="feature_enabled_for_contact">대화 상대에게 켜짐</string>
<string name="feature_enabled">켜짐</string>
<string name="feature_received_prohibited">수신됨, 금지됨</string>
<string name="export_database">데이터베이스 내보내기</string>
<string name="enable_automatic_deletion_question">자동 삭제되는 메시지를 사용할까요\?</string>
<string name="error_changing_message_deletion">설정 변경 오류</string>
<string name="enable_automatic_deletion_message">이 작업은 되돌릴 수 없어요. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제돼요. 이는 몇 분 걸릴 수 있어요.</string>
<string name="error_with_info">오류: %s</string>
<string name="enter_correct_passphrase">올바른 비밀번호를 입력해 주세요.</string>
<string name="database_backup_can_be_restored">데이터베이스 비밀번호 변경이 완료되지 않았어요.</string>
<string name="database_restore_error">데이터베이스 오류 복구</string>
<string name="error_creating_link_for_group">그룹 링크 생성 오류</string>
<string name="error_updating_link_for_group">그룹 링크 업데이트 오류</string>
<string name="error_changing_role">역할 변경 오류</string>
<string name="error_removing_member">멤버 삭제 오류</string>
<string name="database_downgrade">데이터베이스 다운그레이드</string>
<string name="database_migrations">마이그레이션: %s</string>
<string name="delete_group_for_all_members_cannot_undo_warning">모든 멤버에게서 그룹이 삭제돼요. 삭제 후 복구할 수 없어요!</string>
<string name="delete_group_for_self_cannot_undo_warning">나에게서만 그룹이 삭제되요. 삭제 후 복구할 수 없어요!</string>
<string name="file_not_found">파일을 찾을 수 없음</string>
<string name="error_saving_user_password">사용자 비밀번호 저장 오류</string>
<string name="error_stopping_chat">채팅 정지하기 오류</string>
<string name="error_exporting_chat_database">채팅 데이터베이스 내보내기 오류</string>
<string name="database_is_not_encrypted">채팅 데이터베이스가 암호화되지 않았어요. 비밀번호를 설정하여 보호해 주세요.</string>
<string name="enter_passphrase">비밀번호를 입력해 주세요…</string>
<string name="enter_password_to_show">검색에 비밀번호 입력</string>
<string name="edit_image">이미지 수정하기</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">이 작업은 실행 취소될 수 없어요. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실돼요.</string>
<string name="error_importing_database">채팅 데이터베이스 가져오기 오류</string>
<string name="encrypt_database_question">데이터베이스를 암호화할까요\?</string>
<string name="developer_options">데이터베이스 ID 및 전송 격리 옵션.</string>
<string name="error_starting_chat">채팅 시작하기 오류</string>
<string name="error_encrypting_database">데이터베이스 암호화 오류</string>
<string name="enter_correct_current_passphrase">올바른 현재 비밀번호를 입력해 주세요.</string>
<string name="delete_chat_profile">채팅 프로필 삭제</string>
<string name="delete_profile">프로필 삭제</string>
<string name="database_downgrade_warning">경고: 일부 데이터가 손실될 수 있어요!</string>
<string name="database_upgrade">데이터베이스 업그레이드</string>
<string name="delete_files_and_media_desc">이 작업은 실행 취소될 수 없어요. 수신 및 전송된 모든 파일과 미디어가 삭제돼요. 저해상도 사진만 삭제되지 않아요.</string>
<string name="error_deleting_database">채팅 데이터베이스 삭제 오류</string>
<string name="error_deleting_link_for_group">그룹 링크 삭제 오류</string>
<string name="error_saving_file">파일 저장 오류</string>
<string name="error_saving_group_profile">그룹 프로필 저장 오류</string>
<string name="feature_offered_item">%s 제안</string>
<string name="feature_offered_item_with_param">%s 제안: %2s</string>
<string name="icon_descr_instant_notifications">즉시 알림</string>
<string name="hide_notification">숨기기</string>
<string name="hide_verb">숨기기</string>
<string name="for_everybody">모두에게</string>
<string name="icon_descr_edited">수정됨</string>
<string name="icon_descr_image_snd_complete">이미지 보냄</string>
<string name="full_name__field">이름 :</string>
<string name="hide_profile">프로필 숨기기</string>
<string name="icon_descr_flip_camera">카메라 전환</string>
<string name="icon_descr_hang_up">전화 끊기</string>
<string name="file_with_path">파일 : %s</string>
<string name="group_invitation_tap_to_join">탭하여 참여</string>
<string name="group_invitation_tap_to_join_incognito">탭하여 익명으로 참여</string>
<string name="group_member_status_left">나감</string>
<string name="group_member_role_member">멤버</string>
<string name="group_member_role_owner">소유자</string>
<string name="group_member_status_group_deleted">그룹 삭제됨</string>
<string name="group_member_status_invited">초대됨</string>
<string name="group_member_status_removed">강퇴됨</string>
<string name="icon_descr_expand_role">역할 선택지 펼치기</string>
<string name="files_and_media_section">파일 &amp; 미디어</string>
<string name="group_invitation_item_description">그룹으로 초대 <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="group_link">그룹 링크</string>
<string name="group_welcome_title">환영 메시지</string>
<string name="group_display_name_field">보여지는 그룹 이름</string>
<string name="group_full_name_field">그룹 이름 :</string>
<string name="group_is_decentralized">그룹은 완전히 탈중앙화되어 있으며 구성원만 그룹을 볼 수 있어요.</string>
<string name="group_unsupported_incognito_main_profile_sent">여기에서는 시크릿 모드가 지원되지 않아요. 기본 프로필이 그룹 멤버들에게 전송될 거예요.</string>
<string name="group_main_profile_sent">프로필이 그룹 구성원에게 전송될 거예요.</string>
<string name="group_profile_is_stored_on_members_devices">그룹 프로필은 서버가 아닌 멤버들의 기기에 저장되어요.</string>
<string name="group_preferences">그룹 설정</string>
<string name="group_members_can_send_disappearing">그룹 구성원은 자동 삭제되는 메시지를 보낼 수 있어요.</string>
<string name="group_members_can_send_dms">그룹 멤버들끼리 1:1 채팅을 할 수 있어요.</string>
<string name="icon_descr_add_members">멤버 초대하기</string>
<string name="icon_descr_group_inactive">비활성 그룹</string>
<string name="group_member_role_observer">관찰자</string>
<string name="group_info_member_you">나 : <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> 멤버</string>
<string name="file_saved">파일 저장됨</string>
<string name="file_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 파일이 저장되어요.</string>
<string name="file_will_be_received_when_contact_is_online">대화 상대가 온라인 상태일 때 파일이 전송되어요. 대화 상대가 온라인이 되기를 기다리거나 나중에 다시 확인해 주세요!</string>
<string name="icon_descr_record_voice_message">음성 메시지 녹화하기</string>
<string name="from_gallery_button">갤러리에서</string>
<string name="icon_descr_profile_image_placeholder">프로필 이미지 플레이스 홀더</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> 주소</string>
<string name="how_to">설명서</string>
<string name="how_to_use_your_servers">내 서버 사용법</string>
<string name="how_to_use_markdown">마크다운 사용법</string>
<string name="how_simplex_works"><xliff:g id="appName">SimpleX</xliff:g> 작동 방식</string>
<string name="group_invitation_expired">그룹 초대가 만료되었어요.</string>
<string name="group_members_can_delete">그룹 멤버는 보낸 메시지를 영구 삭제할 수 있어요.</string>
<string name="group_members_can_send_voice">그룹 멤버는 음성 메시지를 보낼 수 있어요.</string>
<string name="hidden_profile_password">숨긴 프로필 비밀번호</string>
<string name="full_name_optional__prompt">이름 (선택 사항)</string>
<string name="how_it_works">작동 방식</string>
<string name="hide_dev_options">숨기기 :</string>
<string name="cancel_file__question">파일 전송을 취소할까요\?</string>
<string name="file_transfer_will_be_cancelled_warning">파일 전송이 취소될 거예요. 이미 전송이 시작되었다면 중지될 거예요.</string>
<string name="icon_descr_sent_msg_status_sent">보냄</string>
<string name="group_preview_join_as">%s(으)로 참여</string>
<string name="group_preview_you_are_invited">그룹에 초대되었어요.</string>
<string name="icon_descr_received_msg_status_unread">잃지 않음</string>
<string name="icon_descr_sent_msg_status_send_failed">보내기 실패</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">비인증 전송</string>
<string name="icon_descr_help">도움말</string>
<string name="first_platform_without_user_ids">개인을 식별할 수 있는 어떠한 정보(임의의 숫자 포함)도 없는 첫 번째 플랫폼. 단순히 약속이 아니라 프로그램 설계상 완전한 익명성을 제공해요.</string>
<string name="icon_descr_call_rejected">거절된 전화</string>
<string name="icon_descr_call_pending_sent">대기 중인 전화</string>
<string name="icon_descr_call_missed">부재중 전화</string>
<string name="icon_descr_file">파일</string>
<string name="icon_descr_more_button">더 보기</string>
<string name="icon_descr_send_message">메시지 보내기</string>
<string name="icon_descr_server_status_error">오류</string>
<string name="how_to_use_simplex_chat">사용법</string>
<string name="icon_descr_email">이메일</string>
<string name="icon_descr_server_status_pending">대기 중</string>
<string name="icon_descr_settings">설정</string>
<string name="icon_descr_waiting_for_image">이미지 기다리는 중</string>
<string name="image_decoding_exception_desc">이미지를 디코딩할 수 없어요. 다른 이미지를 시도하거나 개발자에게 문의해 주세요.</string>
<string name="image_descr">이미지</string>
<string name="image_saved">갤러리에 사진 저장됨</string>
<string name="images_limit_desc">동시에 최대 10개까지만 이미지를 보낼 수 있어요.</string>
<string name="images_limit_title">이미지 수가 너무 많아요!</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">거절해도 상대에게 알림이 전송되지 않아요.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">직접 만날 수 없다면 <b>영상 통화에서 QR 코드를 보여주거나</b> 링크를 공유해 주세요.</string>
<string name="icon_descr_video_call">영상 전화</string>
<string name="icon_descr_video_off">영상 끄기</string>
<string name="icon_descr_speaker_on">스피커 켜기</string>
<string name="icon_descr_video_on">영상 켜기</string>
<string name="icon_descr_speaker_off">스피커 끄기</string>
<string name="import_database">데이터베이스 가져오기</string>
<string name="import_database_confirmation">가져오기</string>
<string name="incognito">익명 모드</string>
<string name="incognito_info_find">익명 채팅에 사용되는 프로필을 확인하려면 채팅 상단에 있는 연락처 또는 그룹 이름을 탭하세요.</string>
<string name="image_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 이미지가 수신될 거예요.</string>
<string name="image_descr_profile_image">프로필 이미지</string>
<string name="incognito_info_allows">하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요.</string>
<string name="immune_to_spam_and_abuse">스팸 및 남용에 면역</string>
<string name="ignore">무시하기</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser"><xliff:g id="appName">SimpleX Chat</xliff:g> 초대 링크를 받으면 브라우저에서 참여할 수 있어요 :</string>
<string name="image_descr_link_preview">링크 미리보기 이미지</string>
<string name="image_descr_qr_code">QR 코드</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">직접 만날 수 없다면 <b>영상 통화에서 QR 코드를 스캔</b>하거나 상대에게 초대 링크를 공유할 수 있어요.</string>
<string name="icon_descr_video_snd_complete">동영상 보내짐</string>
<string name="icon_descr_video_asked_to_receive">동영상 수신 요청됨</string>
<string name="icon_descr_waiting_for_video">동영상 기다리는 중</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> 로고</string>
<string name="image_will_be_received_when_contact_is_online">대화 상대가 온라인 상태일 때 이미지가 수신될 거예요. 기다리거나 나중에 확인하세요!</string>
<string name="import_database_question">채팅 데이터베이스를 가져올까요\?</string>
<string name="server_error">오류</string>
<string name="invalid_chat">유효하지 않는 채팅</string>
<string name="invalid_data">잘못된 데이터</string>
<string name="invalid_message_format">잘못된 메시지 형식</string>
<string name="invalid_connection_link">잘못된 연결 링크</string>
<string name="notifications">알림</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> 읽지 않은 메시지</string>
<string name="keychain_error">키체인 오류</string>
<string name="invite_to_group_button">그룹에 초대하기</string>
<string name="info_row_local_name">로컬 네임</string>
<string name="join_group_button">참여</string>
<string name="join_group_question">그룹에 참여할까요\?</string>
<string name="invite_prohibited_description">이 그룹에서는 기본 프로필을 사용하는 중인데 반해, 익명 프로필로 연락하고 있는 대화 상대를 초대하려고 하셨어요.</string>
<string name="initial_member_role">초기 역할</string>
<string name="info_row_group">그룹</string>
<string name="incompatible_database_version">호환되지 않는 데이터베이스 버전</string>
<string name="joining_group">그룹에 참여 중</string>
<string name="incognito_info_protects">익명 모드는 기본 프로필 이름과 사진과 같은 개인 정보를 보호해줘요. 새 대화 상대마다 새로운 랜덤 프로필이 만들어져요.</string>
<string name="is_verified">%s 은(는) 인증되었어요.</string>
<string name="italic">기울게</string>
<string name="incognito_info_share">익명 프로필 사용 중 초대받은 그룹에 참여하면, 그 그룹에서도 동일한 익명 프로필이 사용되어요.</string>
<string name="incognito_random_profile">내 랜덤 프로필</string>
<string name="incoming_audio_call">음성 전화 옴</string>
<string name="is_not_verified">%s은(는) 인증되지 않았어요.</string>
<string name="install_simplex_chat_for_terminal">터미널용 <xliff:g id="appNameFull">SimpleX Chat</xliff:g>를 설치하세요</string>
<string name="incoming_video_call">영상 전화 옴</string>
<string name="invalid_migration_confirmation">잘못된 마이그레이션 확인</string>
<string name="join_group_incognito_button">익명 모드로 참여</string>
<string name="invalid_QR_code">잘못된 QR 코드</string>
<string name="incorrect_code">잘못된 보안 코드!</string>
<string name="invalid_contact_link">잘못된 링크!</string>
<string name="marked_deleted_description">삭제됨으로 표시됨</string>
<string name="moderated_item_description">%s에 의해 조정됨</string>
<string name="live">라이브</string>
<string name="moderated_description">조정됨</string>
<string name="network_error_desc"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> 에서 네트워크 연결 상태를 확인한 후 다시 시도하세요.</string>
<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">정보를 보호하려면 SimpleX 잠금을 켜세요.
\n이 기능이 활성화하기 전에 인증을 완료하라는 메시지가 표시될 거예요.</string>
<string name="notification_new_contact_request">새로운 대화 요청</string>
<string name="notification_preview_mode_hidden">숨겨짐</string>
<string name="notification_preview_mode_contact_desc">대화 상대 이름만 표시</string>
<string name="notification_preview_new_message">새로운 메시지</string>
<string name="notifications_mode_off">앱이 열릴 때 실행</string>
<string name="notifications_mode_periodic">주기적으로 실행됨</string>
<string name="notification_display_mode_hidden_desc">연락처 이름 및 메시지 숨기기</string>
<string name="la_notice_turn_on">켜기</string>
<string name="message_delivery_error_desc">대화 상대가 나와의 연결을 삭제했을 가능성이 커요.</string>
<string name="message_delivery_error_title">메시지 전달 오류</string>
<string name="moderate_verb">조정</string>
<string name="moderate_message_will_be_deleted_warning">모든 멤버에게서 메시지가 삭제될 거예요.</string>
<string name="moderate_message_will_be_marked_warning">이 메시지는 모든 멤버에게 조정됨으로 표시될 거예요.</string>
<string name="no_details">세부 정보 없음</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 모바일: <b>모바일 앱에서 열기</b>를 누른 다음 앱에서 <b>연결</b>을 누르세요.</string>
<string name="mark_code_verified">확인됨으로 표시</string>
<string name="network_and_servers">네트워크 및 서버</string>
<string name="new_database_archive">새 데이터베이스 보관함</string>
<string name="new_member_role">새 멤버 역할</string>
<string name="no_contacts_to_add">추가할 연락처 없음</string>
<string name="no_contacts_selected">선택한 연락처 없음</string>
<string name="network_status">네트워크 상태</string>
<string name="language_system">시스템</string>
<string name="messages_section_title">메시지</string>
<string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string>
<string name="member_info_section_title_member">멤버</string>
<string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
<string name="network_options_revert">되돌리기</string>
<string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string>
<string name="leave_group_button">나가기</string>
<string name="large_file">큰 파일!</string>
<string name="network_settings_title">네트워크 설정</string>
<string name="network_use_onion_hosts_required_desc">연결하려면 Onion 호스트가 필요해요.</string>
<string name="network_option_ping_count">핑 횟수</string>
<string name="network_option_ping_interval">핑 간격</string>
<string name="network_option_protocol_timeout">프로토콜 타임아웃</string>
<string name="network_option_seconds_label"></string>
<string name="network_option_tcp_connection_timeout">TCP 연결 시간 초과</string>
<string name="muted_when_inactive">비활성 시 음소거!</string>
<string name="message_deletion_prohibited_in_chat">이 채팅에서는 메시지 영구 삭제가 허용되지 않아요.</string>
<string name="make_private_connection">비공개 연결하기</string>
<string name="no_received_app_files">수신 또는 전송된 파일 없음</string>
<string name="network_options_save">저장하기</string>
<string name="make_profile_private">프로필을 비공개로 설정하세요!</string>
<string name="live_message">라이브 메시지!</string>
<string name="mark_read">읽음으로 표시</string>
<string name="mark_unread">읽지 않음으로 표시</string>
<string name="mute_chat">음소거</string>
<string name="markdown_in_messages">메시지에 사용된 마크다운</string>
<string name="network_socks_toggle">SOCKS 프록시 사용 (포트 9050)</string>
<string name="network_disable_socks">직접적인 인터넷 연결을 사용할까요\?</string>
<string name="network_enable_socks">SOCKS 프록시를 사용할까요\?</string>
<string name="network_disable_socks_info">설정하면 메시징 서버에서 내 IP 주소와 내가 연결하려는 서버를 볼 수 있어요.</string>
<string name="network_use_onion_hosts">.onion 호스트 사용</string>
<string name="network_use_onion_hosts_no">아니요</string>
<string name="network_use_onion_hosts_prefer">사용 가능한 경우</string>
<string name="network_use_onion_hosts_required">필요함</string>
<string name="network_use_onion_hosts_prefer_desc">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
<string name="network_use_onion_hosts_no_desc">Onion 호스트가 사용되지 않을 거예요.</string>
<string name="network_session_mode_transport_isolation">전송 격리</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion 호스트가 사용되지 않을 거예요.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">연결하려면 Onion 호스트가 필요해요.</string>
<string name="next_generation_of_private_messaging">차세대 사생활 보호 메시징</string>
<string name="new_passphrase">새 비밀번호…</string>
<string name="network_option_enable_tcp_keep_alive">TCP 연결 유지 활성화</string>
<string name="new_in_version">%s의 새로운 기능</string>
<string name="markdown_help">마크다운 도움말</string>
<string name="many_people_asked_how_can_it_deliver">많은 사람들의 질문 : <i><xliff:g id="appName">SimpleX</xliff:g>에는 사용자 식별자가 없는데도 어떻게 메시지를 전달할 수 있어요\?</i></string>
<string name="leave_group_question">그룹에서 나갈까요\?</string>
<string name="mtr_error_no_down_migration">데이터베이스 버전이 앱보다 최신이지만 다음에 대한 다운 마이그레이션 없음: %s</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요!</string>
<string name="member_role_will_be_changed_with_notification">역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
<string name="network_options_reset_to_defaults">기본값으로 재설정</string>
<string name="notification_preview_mode_message">메시지 내용</string>
<string name="notification_preview_mode_message_desc">대화 상대 이름 및 메시지 표시</string>
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음).</string>
<string name="profile_will_be_sent_to_contact_sending_link">이 링크를 보낸 상대에게 프로필이 전송될 거예요.</string>
<string name="receiving_files_not_yet_supported">파일 수신은 아직 지원되지 않아요.</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요</string>
<string name="periodic_notifications">주기적 알림</string>
<string name="periodic_notifications_desc">주기적으로 새 메시지를 확인해요 — 하루에 몇 퍼센트의 배터리를 사용할 거예요. 푸시 알림을 사용하지 않아요 — 기기의 데이터가 서버로 전송되지 않아요.</string>
<string name="periodic_notifications_disabled">주기적 알림이 비활성화되었어요.</string>
<string name="ntf_channel_calls">SimpleX Chat 통화</string>
<string name="ntf_channel_messages">SimpleX Chat 메시지</string>
<string name="observer_cant_send_message_desc">그룹 관리자에게 문의해 주세요.</string>
<string name="observer_cant_send_message_title">메시지를 보낼 수 없습니다!</string>
<string name="ok"></string>
<string name="reject_contact_button">거절</string>
<string name="password_to_show">비밀번호 표시</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">사용자 디바이스에만 <b>2계층 종단 간 암호화</b> 로 전송된 사용자 프로필, 연락처, 그룹 및 메시지를 저장되어요.</string>
<string name="read_more_in_github">자세한 내용은 GitHub에서 확인해 주세요.</string>
<string name="privacy_and_security">개인 정보 및 보안</string>
<string name="notifications_will_be_hidden">알림은 앱이 중지되기 전까지만 전달될 거예요!</string>
<string name="only_you_can_send_disappearing">자동 삭제되는 메시지는 나만 보낼 수 있어요.</string>
<string name="prohibit_sending_disappearing_messages">자동 삭제되는 메시지 허용되지 않음.</string>
<string name="prohibit_sending_voice_messages">음성 메시지 허용되지 않음.</string>
<string name="prohibit_sending_disappearing">자동 삭제되는 메시지 허용되지 않음.</string>
<string name="old_database_archive">이전 데이터베이스 기록</string>
<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_left">나감</string>
<string name="rcv_group_event_user_deleted">강퇴됨</string>
<string name="rcv_group_event_member_deleted"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> 강퇴됨</string>
<string name="only_group_owners_can_change_prefs">그룹 소유자만 그룹 설정을 변경할 수 있어요.</string>
<string name="receiving_via">다음을 통해 수신</string>
<string name="only_your_contact_can_send_disappearing">대화 상대만 자동 삭제되는 메시지를 보낼 수 있어요.</string>
<string name="prohibit_direct_messages">멤버들 간의 1:1 채팅이 허용되지 않음.</string>
<string name="only_you_can_send_voice">나만 음성 메시지를 보낼 수 있어요.</string>
<string name="only_your_contact_can_send_voice">대화 상대만 음성 메시지를 보낼 수 있어요.</string>
<string name="prohibit_message_deletion">메시지 영구 삭제 허용되지 않음.</string>
<string name="prohibit_sending_voice">음성 메시지 허용되지 않음.</string>
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음).</string>
<string name="only_group_owners_can_enable_voice">그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요.</string>
<string name="one_time_link">일회성 초대 링크</string>
<string name="paste_button">붙여넣기</string>
<string name="profile_is_only_shared_with_your_contacts">프로필은 대화 상대들하고만 공유됩니다.</string>
<string name="privacy_redefined">프라이버시의 재정의</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">오픈 소스 프로토콜과 코드 - 누구나 자신만의 서버를 구축할 수 있어요.</string>
<string name="onboarding_notifications_mode_off">앱이 실행 중일 때</string>
<string name="read_more_in_github_with_link">ㅍ자세한 내용은 \u0020<font color="#0088ff">GitHub</font> 에서 확인해 주세요.</string>
<string name="relay_server_protects_ip">릴레이 서버는 IP 주소를 숨겨주지만, 통화 시간을 관찰 할 수 있어요.</string>
<string name="rcv_group_event_invited_via_your_group_link">그룹 링크로 초대</string>
<string name="onboarding_notifications_mode_subtitle">설정을 통해 나중에 변경할 수 있어요.</string>
<string name="onboarding_notifications_mode_title">비공개 알림</string>
<string name="reject">거절</string>
<string name="open_simplex_chat_to_accept_call"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>을 열어 전화 받기</string>
<string name="open_verb">열기</string>
<string name="protect_app_screen">앱 잠금</string>
<string name="personal_welcome"><xliff:g>%1$s</xliff:g>님, 환영합니다!</string>
<string name="only_stored_on_members_devices">(그룹 구성원에게만 저장됨)</string>
<string name="paste_connection_link_below_to_connect">아래 칸에 받은 링크를 붙여넣기하여 대화 상대와 연결해 주세요.</string>
<string name="rate_the_app">앱 평가하기</string>
<string name="onboarding_notifications_mode_periodic">주기적</string>
<string name="onboarding_notifications_mode_service">즉시</string>
<string name="paste_the_link_you_received">받은 링크 붙여넣기</string>
<string name="relay_server_if_necessary">릴레이 서버는 필요한 경우에만 사용되어요. 릴레이 서버가 사용되지 않으면 제3자가 내 IP 주소를 관찰할 수 있어요.</string>
<string name="open_chat">채팅 열기</string>
<string name="people_can_connect_only_via_links_you_share">공유한 링크를 통해서만 나에게 연결할 수 있어요.</string>
<string name="rcv_group_event_updated_group_profile">그룹 프로필 업데이트됨</string>
<string name="profile_password">프로필 비밀번호</string>
<string name="reveal_verb">보이기</string>
<string name="save_verb">저장하기</string>
<string name="scan_QR_code">QR 코드 스캔하기</string>
<string name="reply_verb">답장</string>
<string name="reset_verb">초기화</string>
<string name="save_servers_button">저장하기</string>
<string name="save_and_notify_contacts">저장하고 모든 대화 상대에게 알리기</string>
<string name="save_and_notify_group_members">저장하고 그룹 멤버들에게 알리기</string>
<string name="save_and_notify_contact">저장하고 대화 상대에게 알리기</string>
<string name="remove_passphrase">지우기</string>
<string name="save_archive">아카이브 저장하기</string>
<string name="save_color">색상 저장하기</string>
<string name="save_passphrase_in_keychain">암호 저장소에 비밀번호 저장하기</string>
<string name="restore_database">데이터베이스 백업 복원하기</string>
<string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요.</string>
<string name="restore_database_alert_title">데이터베이스 백업을 복원할까요\?</string>
<string name="restore_passphrase_not_found_desc">키스토어에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요.</string>
<string name="save_and_update_group_profile">저장하고 그룹 프로필 업데이트하기</string>
<string name="save_welcome_message_question">환영 메시지를 저장할까요\?</string>
<string name="save_group_profile">그룹 프로필 저장하기</string>
<string name="reset_color">색상 초기화</string>
<string name="remove_member_confirmation">강퇴하기</string>
<string name="scan_code">코드 스캔하기</string>
<string name="scan_code_from_contacts_app">대화 상대의 앱에서 보안 코드를 스캔해 주세요.</string>
<string name="saved_ICE_servers_will_be_removed">저장된 WebRTC ICE 서버가 제거될 거예요.</string>
<string name="save_passphrase_and_open_chat">비밀번호 저장하고 채팅 열기</string>
<string name="role_in_group">역할</string>
<string name="save_preferences_question">설정을 저장할까요\?</string>
<string name="save_profile_password">프로필 비밀번호 저장하기</string>
<string name="restart_the_app_to_use_imported_chat_database">가져온 채팅 데이터베이스를 사용하려면 앱을 다시 실행해 주세요.</string>
<string name="restart_the_app_to_create_a_new_chat_profile">새 프로필을 만드려면 앱을 다시 실행해 주세요.</string>
<string name="remove_passphrase_from_keychain">암호 저장소에서 비밀번호를 삭제할까요\?</string>
<string name="run_chat_section">채팅 기능 실행하기</string>
<string name="restore_database_alert_confirm">복원하기</string>
<string name="sender_cancelled_file_transfer">대화 상대가 파일 전송을 취소했어요.</string>
<string name="smp_server_test_secure_queue">보안 대기열</string>
<string name="smp_server_test_upload_file">파일 업로드하기</string>
<string name="smp_server_test_compare_file">파일 비교</string>
<string name="smp_server_test_delete_file">파일 삭제하기</string>
<string name="smp_server_test_download_file">파일 다운로드하기</string>
<string name="share_verb">공유</string>
<string name="send_live_message">라이브 메시지 보내기</string>
<string name="show_QR_code">QR코드 보기</string>
<string name="smp_server_test_create_file">파일 만들기</string>
<string name="sender_you_pronoun"></string>
<string name="sending_files_not_yet_supported">파일 보내기는 아직 지원되지 않아요.</string>
<string name="simplex_link_connection"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g>를 통해</string>
<string name="simplex_link_contact">SimpleX 연락처 주소</string>
<string name="simplex_link_group">SimpleX 그룹 링크</string>
<string name="simplex_link_invitation">SimpleX 일회용 초대 링크</string>
<string name="simplex_link_mode">SimpleX 링크</string>
<string name="simplex_link_mode_browser">브라우저를 통해</string>
<string name="simplex_link_mode_browser_warning">브라우저에서 링크를 열면 익명성 또는 보안에 문제가 생길 수 있어요. 신뢰할 수 없는 SimpleX 링크는 빨간색으로 표시되어요.</string>
<string name="simplex_link_mode_full">링크 전체</string>
<string name="sender_may_have_deleted_the_connection_request">보낸 사람이 연결 요청을 삭제했을 수 있어요.</string>
<string name="service_notifications_disabled">즉각적인 알림이 비활성화되었어요!</string>
<string name="settings_notification_preview_mode_title">미리보기 표시</string>
<string name="settings_notification_preview_title">알림 미리보기</string>
<string name="settings_notifications_mode_title">알림 서비스</string>
<string name="simplex_service_notification_text">메시지 받는 중…</string>
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX 채팅</xliff:g> 서비스</string>
<string name="share_file">파일 공유…</string>
<string name="share_image">이미지 공유…</string>
<string name="share_message">메시지 공유…</string>
<string name="send_live_message_desc">라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줘요.</string>
<string name="send_verb">보내기</string>
<string name="share_invitation_link">초대 링크 공유</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">연락하고자 하는 사람이 앱에서 QR 코드를 스캔할 수 있어요.</string>
<string name="security_code">보안 코드</string>
<string name="smp_servers_scan_qr">서버 QR코드 스캔</string>
<string name="smp_servers_test_some_failed">일부 서버가 테스트에 통과하지 못했습니다 :</string>
<string name="smp_servers_test_failed">서버 테스트 실패!</string>
<string name="section_title_welcome_message">환영 메시지</string>
<string name="share_link">링크 공유</string>
<string name="show_call_on_lock_screen">표시하기</string>
<string name="select_contacts">연락처 선택</string>
<string name="skip_inviting_button">멤버 초대 건너뛰기</string>
<string name="sending_via">다음을 통해 보내기</string>
<string name="stop_chat_confirmation">멈추기</string>
<string name="snd_group_event_changed_member_role">%s의 역할을 %s로 변경했어요.</string>
<string name="section_title_for_console">콘솔용</string>
<string name="should_be_at_least_one_visible_profile">적어도 하나의 숨겨지지 않은 사용자 프로필이 있어야 해요.</string>
<string name="set_group_preferences">그룹 설정 지정하기</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">%s의 주소를 바꿨어요</string>
<string name="snd_conn_event_switch_queue_phase_completed">주소를 바꿨어요</string>
<string name="snd_group_event_group_profile_updated">그룹 프로필 업데이트됨</string>
<string name="should_be_at_least_one_profile">적어도 하나의 사용자 프로필이 있어야 해요.</string>
<string name="smp_servers_test_server">서버 테스트하기</string>
<string name="smp_servers_use_server">서버 사용하기</string>
<string name="smp_servers_use_server_for_new_conn">새로운 대화에 사용</string>
<string name="smp_servers_invalid_address">잘못된 서버 주소에요!</string>
<string name="secret">비밀</string>
<string name="status_no_e2e_encryption">종단 간 암호화 안 됨</string>
<string name="settings_section_title_themes">테마</string>
<string name="snd_group_event_changed_role_for_yourself">내 역할이 %s로 변경되었어요.</string>
<string name="snd_group_event_user_left">나감</string>
<string name="settings_section_title_help">도움말</string>
<string name="settings_section_title_settings">설정</string>
<string name="send_link_previews">링크 미리보기 보내기</string>
<string name="settings_section_title_socks">SOCKS 프록시</string>
<string name="settings_section_title_support">SIMPLEX CHAT 도와주기</string>
<string name="settings_section_title_you"></string>
<string name="settings_experimental_features">실험적 기능</string>
<string name="show_dev_options">표시 :</string>
<string name="set_contact_name">연락처 이름 설정</string>
<string name="send_us_an_email">개발자에게 이메일 보내기</string>
<string name="smp_servers_enter_manually">서버를 수동으로 입력</string>
<string name="smp_servers_preset_server">미리 설정된 서버</string>
<string name="smp_servers_save">서버 저장하기</string>
<string name="smp_servers_test_servers">서버 테스트하기</string>
<string name="smp_servers_per_user">현재 채팅 프로필의 새로운 연결을 위한 서버</string>
<string name="smp_save_servers_question">서버를 저장할까요\?</string>
<string name="star_on_github">GitHub에서 별 주기</string>
<string name="show_developer_options">개발자 옵션 보기</string>
<string name="settings_section_title_messages">메시지 및 파일</string>
<string name="settings_section_title_incognito">익명 모드</string>
<string name="settings_section_title_experimenta">실험적</string>
<string name="set_password_to_export">내보낼 비밀번호 설정</string>
<string name="smp_servers">SMP 서버</string>
<string name="smp_servers_preset_address">미리 설정된 서버 주소</string>
<string name="smp_servers_your_server">내 서버</string>
<string name="smp_servers_your_server_address">내 서버 주소</string>
<string name="snd_group_event_member_deleted"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g>을(를) 강퇴했어요.</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">채팅 데이터베이스를 내보내기, 가져오기 또는 삭제 하려면 채팅 기능을 중지해 주세요. 채팅 기능이 중지된 동안에는 메시지를 주고받을 수 없어요.</string>
<string name="store_passphrase_securely">비밀번호를 모르면 변경하거나 찾을 수 없으므로 비밀번호를 안전하게 보관해 주세요.</string>
<string name="submit_passcode">제출하기</string>
<string name="store_passphrase_securely_without_recover">비밀번호를 모르면 채팅에 액세스할 수 없으니 비밀번호를 안전하게 보관해 주세요.</string>
<string name="stop_chat_question">채팅 기능을 중지할까요\?</string>
<string name="stop_chat_to_enable_database_actions">데이터베이스 작업을 할 수 있도록 채팅 기능을 중지하기</string>
<string name="switch_receiving_address">수신 주소 바꾸기</string>
</resources>
@@ -47,4 +47,312 @@
<string name="auth_unavailable">Tapatybės nustatymas neprieinamas</string>
<string name="impossible_to_recover_passphrase"><b>Turėkite omenyje</b>: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</string>
<string name="cancel_verb">Atsisakyti</string>
<string name="callstate_connecting">jungiamasi…</string>
<string name="connect_via_link_verb">Prisijungti</string>
<string name="server_connected">prisijungta</string>
<string name="server_connecting">jungiamasi</string>
<string name="display_name_connection_established">ryšys užmegztas</string>
<string name="display_name_connecting">jungiasi…</string>
<string name="connection_error">Ryšio klaida</string>
<string name="smp_server_test_connect">Prisijungti</string>
<string name="notification_contact_connected">Prisijungė</string>
<string name="connect_button">Prisijungti</string>
<string name="group_member_status_connected">prisijungė</string>
<string name="contact_connection_pending">jungiasi…</string>
<string name="group_connection_pending">jungiasi…</string>
<string name="icon_descr_server_status_connected">Prisijungta</string>
<string name="confirm_verb">Patvirtinti</string>
<string name="configure_ICE_servers">Konfigūruoti ICE serverius</string>
<string name="callstate_connected">prisijungta</string>
<string name="rcv_group_event_member_connected">prisijungė</string>
<string name="info_row_connection">Ryšys</string>
<string name="confirm_database_upgrades">Patvirtinti duomenų bazių naujinimus</string>
<string name="group_member_status_connecting">jungiasi</string>
<string name="network_session_mode_entity">Ryšys</string>
<string name="database_passphrase_will_be_updated">Duomenų bazės šifravimo slaptafrazė bus atnaujinta.</string>
<string name="core_version">Branduolio versija: v%s</string>
<string name="delete_address__question">Ištrinti adresą\?</string>
<string name="save_preferences_question">Įrašyti nuostatas\?</string>
<string name="create_profile">Sukurti profilį</string>
<string name="callstatus_rejected">atmestas skambutis</string>
<string name="callstate_ended">užbaigtas</string>
<string name="onboarding_notifications_mode_title">Privatūs pranešimai</string>
<string name="ignore">Nepaisyti</string>
<string name="icon_descr_flip_camera">Apversti kamerą</string>
<string name="icon_descr_call_rejected">Atmestas skambutis</string>
<string name="privacy_and_security">Privatumas ir saugumas</string>
<string name="settings_section_title_device">ĮRENGINYS</string>
<string name="settings_section_title_help">PAGALBA</string>
<string name="encrypt_database">Šifruoti</string>
<string name="remove_passphrase">Šalinti</string>
<string name="button_delete_group">Ištrinti grupę</string>
<string name="v4_2_group_links">Grupių nuorodos</string>
<string name="server_error">klaida</string>
<string name="simplex_link_mode_description">Aprašas</string>
<string name="error_saving_smp_servers">Klaida įrašant SMP serverius</string>
<string name="error_setting_network_config">Klaida atnaujinant tinklo konfigūraciją</string>
<string name="failed_to_create_user_title">Klaida kuriant profilį!</string>
<string name="failed_to_parse_chat_title">Nepavyko įkelti pokalbio</string>
<string name="failed_to_parse_chats_title">Nepavyko įkelti pokalbių</string>
<string name="failed_to_active_user_title">Klaida perjungiant profilį!</string>
<string name="error_sending_message">Klaida siunčiant žinutę</string>
<string name="error_creating_address">Klaida kuriant adresą</string>
<string name="error_joining_group">Klaida prisijungiant prie grupės</string>
<string name="error_receiving_file">Klaida gaunant failą</string>
<string name="error_changing_address">Klaida keičiant adresą</string>
<string name="error_deleting_contact">Klaida ištrinant adresatą</string>
<string name="error_deleting_group">Klaida ištrinant grupę</string>
<string name="smp_server_test_disconnect">Atsijungti</string>
<string name="error_deleting_user">Klaida ištrinant naudotojo profilį</string>
<string name="notification_display_mode_hidden_desc">Slėpti adresatą ir žinutę</string>
<string name="copy_verb">Kopijuoti</string>
<string name="reply_verb">Atsakyti</string>
<string name="delete_message__question">Ištrinti žinutę\?</string>
<string name="icon_descr_file">Failas</string>
<string name="maximum_supported_file_size">Šiuo metu didžiausias palaikomas failo dydis yra <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="file_not_found">Failas nerastas</string>
<string name="file_saved">Failas įrašytas</string>
<string name="icon_descr_server_status_disconnected">Atsijungta</string>
<string name="icon_descr_server_status_error">Klaida</string>
<string name="ask_your_contact_to_enable_voice">Paprašykite adresato, kad įjungtų balso žinučių siuntimą.</string>
<string name="reset_verb">Atstatyti</string>
<string name="delete_group_menu_action">Ištrinti</string>
<string name="image_descr_qr_code">QR kodas</string>
<string name="icon_descr_help">pagalba</string>
<string name="icon_descr_email">El. paštas</string>
<string name="create_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string>
<string name="scan_code">Skenuoti kodą</string>
<string name="database_passphrase_and_export">Duomenų bazės slaptafrazė ir eksportavimas</string>
<string name="smp_servers_delete_server">Ištrinti serverį</string>
<string name="error_saving_ICE_servers">Klaida įrašant ICE serverius</string>
<string name="save_servers_button">Įrašyti</string>
<string name="files_and_media_section">Failai ir medija</string>
<string name="delete_files_and_media_all">Ištrinti visus failus</string>
<string name="delete_files_and_media_question">Ištrinti failus ir mediją\?</string>
<string name="delete_archive">Ištrinti archyvą</string>
<string name="delete_chat_archive_question">Ištrinti pokalbio archyvą\?</string>
<string name="snd_group_event_group_profile_updated">grupės profilis atnaujintas</string>
<string name="info_row_group">Grupė</string>
<string name="users_delete_question">Ištrinti pokalbio profilį\?</string>
<string name="network_options_revert">Sugrąžinti</string>
<string name="network_options_save">Įrašyti</string>
<string name="feature_enabled">įjungta</string>
<string name="delete_after">Ištrinti po</string>
<string name="error_updating_user_privacy">Klaida atnaujinant naudotojo privatumą</string>
<string name="simplex_service_notification_text">Gaunamos žinutės…</string>
<string name="hide_notification">Slėpti</string>
<string name="save_verb">Įrašyti</string>
<string name="delete_verb">Ištrinti</string>
<string name="edit_verb">Taisyti</string>
<string name="hide_verb">Slėpti</string>
<string name="reveal_verb">Atskleisti</string>
<string name="for_me_only">Ištrinti man</string>
<string name="for_everybody">Visiems</string>
<string name="icon_descr_edited">taisyta</string>
<string name="observer_cant_send_message_desc">Susisiekite su grupės administratoriumi.</string>
<string name="button_delete_contact">Ištrinti adresatą</string>
<string name="delete_contact_question">Ištrinti adresatą\?</string>
<string name="create_group">Sukurti slaptą grupę</string>
<string name="from_gallery_button">Iš galerijos</string>
<string name="toast_permission_denied">Leidimas atmestas!</string>
<string name="reject_contact_button">Atmesti</string>
<string name="incorrect_code">Neteisingas saugumo kodas!</string>
<string name="smp_servers_save">Įrašyti serverius</string>
<string name="smp_save_servers_question">Įrašyti serverius\?</string>
<string name="create_address">Sukurti adresą</string>
<string name="delete_address">Ištrinti adresą</string>
<string name="error_saving_user_password">Klaida įrašant naudotojo slaptažodį</string>
<string name="create_profile_button">Sukurti</string>
<string name="how_it_works">Kaip tai veikia</string>
<string name="incoming_audio_call">Gaunamas garso skambutis</string>
<string name="incoming_video_call">Gaunamas vaizdo skambutis</string>
<string name="reject">Atmesti</string>
<string name="no_call_on_lock_screen">Išjungti</string>
<string name="settings_experimental_features">Eksperimentinės ypatybės</string>
<string name="database_passphrase">Duomenų bazės slaptafrazė</string>
<string name="delete_database">Ištrinti duomenų bazę</string>
<string name="export_database">Eksportuoti duomenų bazę</string>
<string name="import_database">Importuoti duomenų bazę</string>
<string name="set_password_to_export_desc">Duomenų bazė yra šifruota naudojant atsitiktinę slaptafrazę. Prieš eksportuodami duomenų bazę, pakeiskite slaptafrazę.</string>
<string name="error_exporting_chat_database">Klaida eksportuojant pokalbio duomenų bazę</string>
<string name="error_stopping_chat">Klaida sustabdant pokalbį</string>
<string name="import_database_question">Importuoti pokalbio duomenų bazę\?</string>
<string name="current_passphrase">Dabartinė slaptafrazė…</string>
<string name="encrypted_with_random_passphrase">Duomenų bazė yra šifruota naudojant atsitiktinę slaptafrazę, kurią galite pakeisti.</string>
<string name="encrypt_database_question">Šifruoti duomenų bazę\?</string>
<string name="error_with_info">Klaida: %s</string>
<string name="button_edit_group_profile">Taisyti grupės profilį</string>
<string name="group_link">Grupės nuoroda</string>
<string name="button_create_group_link">Sukurti nuorodą</string>
<string name="info_row_database_id">Duomenų bazės ID</string>
<string name="error_changing_role">Klaida keičiant vaidmenį</string>
<string name="conn_level_desc_direct">tiesioginis</string>
<string name="conn_level_desc_indirect">netiesioginis (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="create_secret_group_title">Sukurti slaptą grupę</string>
<string name="error_saving_group_profile">Klaida įrašant grupės profilį</string>
<string name="dont_show_again">Daugiau neberodyti</string>
<string name="theme_dark">Tamsus</string>
<string name="reset_color">Atstatyti spalvas</string>
<string name="save_color">Įrašyti spalvą</string>
<string name="group_preferences">Grupės nuostatos</string>
<string name="full_deletion">Ištrinti visiems</string>
<string name="direct_messages">Tiesioginės žinutės</string>
<string name="v4_4_disappearing_messages">Išnykstančios žinutės</string>
<string name="timed_messages">Išnykstančios žinutės</string>
<string name="v4_4_french_interface">Sąsaja prancūzų kalba</string>
<string name="v4_3_improved_privacy_and_security">Patobulintas privatumas ir saugumas</string>
<string name="num_contacts_selected">Pažymėta adresatų: %d</string>
<string name="delete_chat_profile_question">Ištrinti pokalbio profilį\?</string>
<string name="error_importing_database">Klaida importuojant pokalbio duomenų bazę</string>
<string name="delete_messages_after">Ištrinti žinutes po</string>
<string name="database_will_be_encrypted">Duomenų bazė bus šifruota.</string>
<string name="alert_title_no_group">Grupė nerasta!</string>
<string name="error_deleting_link_for_group">Klaida ištrinant grupės nuorodą</string>
<string name="error_updating_link_for_group">Klaida atnaujinant grupės nuorodą</string>
<string name="database_upgrade">Duomenų bazės naujinimas</string>
<string name="incompatible_database_version">Nesuderinama duomenų bazės versija</string>
<string name="group_member_status_group_deleted">grupė ištrinta</string>
<string name="save_and_update_group_profile">Įrašyti ir atnaujinti grupės profilį</string>
<string name="delete_messages">Ištrinti žinutes</string>
<string name="error_changing_message_deletion">Klaida keičiant nustatymą</string>
<string name="error_encrypting_database">Klaida šifruojant duomenų bazę</string>
<string name="encrypted_database">Šifruota duomenų bazė</string>
<string name="restore_database_alert_confirm">Atkurti</string>
<string name="create_group_link">Sukurti grupės nuorodą</string>
<string name="delete_link">Ištrinti nuorodą</string>
<string name="delete_link_question">Ištrinti nuorodą\?</string>
<string name="error_creating_link_for_group">Klaida kuriant grupės nuorodą</string>
<string name="how_simplex_works">Kaip <xliff:g id="appName">SimpleX</xliff:g> veikia</string>
<string name="database_encrypted">Duomenų bazė šifruota!</string>
<string name="group_member_status_creator">kūrėjas</string>
<string name="delete_group_question">Ištrinti grupę\?</string>
<string name="user_hide">Slėpti</string>
<string name="error_saving_file">Klaida įrašant failą</string>
<string name="hide_dev_options">Slėpti:</string>
<string name="database_error">Duomenų bazės klaida</string>
<string name="file_with_path">Failas: %s</string>
<string name="profile_password">Profilio slaptažodis</string>
<string name="remove_member_confirmation">Šalinti</string>
<string name="v4_3_improved_server_configuration">Patobulinta serverio konfigūracija</string>
<string name="delete_chat_profile">Ištrinti pokalbio profilį</string>
<string name="delete_profile">Ištrinti profilį</string>
<string name="share_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string>
<string name="delete_contact_menu_action">Ištrinti</string>
<string name="save_and_notify_contact">Įrašyti ir pranešti adresatui</string>
<string name="enable_automatic_deletion_question">Įjungti automatinį žinučių ištrynimą\?</string>
<string name="error_deleting_database">Klaida ištrinant pokalbio duomenų bazę</string>
<string name="error_starting_chat">Klaida pradedant pokalbį</string>
<string name="hide_profile">Slėpti profilį</string>
<string name="import_database_confirmation">Importuoti</string>
<string name="save_and_notify_contacts">Įrašyti ir pranešti adresatams</string>
<string name="callstate_starting">pradedama…</string>
<string name="use_chat">Naudoti pokalbį</string>
<string name="icon_descr_speaker_off">Išjungti garsiakalbį</string>
<string name="icon_descr_speaker_on">Įjungti garsiakalbį</string>
<string name="alert_title_skipped_messages">Praleistos žinutės</string>
<string name="settings_section_title_settings">NUSTATYMAI</string>
<string name="theme_system">Sistemos</string>
<string name="unknown_message_format">nežinomas žinutės formatas</string>
<string name="simplex_link_contact">SimpleX adresato adresas</string>
<string name="simplex_link_group">SimpleX grupės nuoroda</string>
<string name="simplex_link_invitation">SimpleX vienkartinis pakvietimas</string>
<string name="simplex_link_mode">SimpleX nuorodos</string>
<string name="settings_notification_preview_mode_title">Rodyti peržiūrą</string>
<string name="auth_unlock">Atrakinti</string>
<string name="share_verb">Bendrinti</string>
<string name="tap_to_start_new_chat">Bakstelėkite, norėdami pradėti naują pokalbį</string>
<string name="share_file">Bendrinti failą…</string>
<string name="icon_descr_settings">Nustatymai</string>
<string name="show_QR_code">Rodyti QR kodą</string>
<string name="share_invitation_link">Bendrinti pakvietimo nuorodą</string>
<string name="smp_servers">SMP serveriai</string>
<string name="use_simplex_chat_servers__question">Naudoti <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serverius\?</string>
<string name="network_disable_socks">Naudoti tiesioginį interneto ryšį\?</string>
<string name="update_network_settings_confirmation">Atnaujinti</string>
<string name="update_network_settings_question">Atnaujinti tinklo nustatymus\?</string>
<string name="ntf_channel_messages">SimpleX Chat žinutės</string>
<string name="ntf_channel_calls">SimpleX Chat skambučiai</string>
<string name="notification_preview_mode_message_desc">Rodyti adresatą ir žinutę</string>
<string name="notification_preview_mode_contact_desc">Rodyti tik adresatą</string>
<string name="auth_stop_chat">Stabdyti pokalbį</string>
<string name="share_message">Bendrinti žinutę…</string>
<string name="use_camera_button">Naudoti kamerą</string>
<string name="thank_you_for_installing_simplex">Dėkojame, kad įdiegėte <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="smp_servers_use_server">Naudoti serverį</string>
<string name="network_socks_toggle">Naudoti SOCKS įgaliotąjį serverį (prievadas 9050)</string>
<string name="network_enable_socks">Naudoti SOCKS įgaliotąjį serverį\?</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="share_link">Bendrinti nuorodą</string>
<string name="tap_to_activate_profile">Bakstelėkite, norėdami aktyvuoti profilį.</string>
<string name="language_system">Sistemos</string>
<string name="v4_6_audio_video_calls_descr">Bluetooth palaikymas ir kiti patobulinimai.</string>
<string name="group_invitation_tap_to_join">Bakstelėkite, norėdami prisijungti</string>
<string name="show_call_on_lock_screen">Rodyti</string>
<string name="stop_chat_confirmation">Stabdyti</string>
<string name="upgrade_and_open_chat">Naujinti ir atverti pokalbį</string>
<string name="switch_verb">Perjungti</string>
<string name="icon_descr_received_msg_status_unread">neskaityta</string>
<string name="show_dev_options">Rodyti:</string>
<string name="stop_chat_question">Stabdyti pokalbį\?</string>
<string name="update_database">Atnaujinti</string>
<string name="unknown_error">Nežinoma klaida</string>
<string name="add_contact_or_create_group">Pradėti naują pokalbį</string>
<string name="callstate_waiting_for_answer">laukiama, kol bus atsiliepta…</string>
<string name="callstate_waiting_for_confirmation">laukiama patvirtinimo…</string>
<string name="icon_descr_video_off">Išjungti vaizdą</string>
<string name="icon_descr_video_on">Įjungti vaizdą</string>
<string name="your_privacy">Jūsų privatumas</string>
<string name="settings_section_title_you">JŪS</string>
<string name="wrong_passphrase_title">Neteisinga slaptafrazė!</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="sender_you_pronoun">jūs</string>
<string name="description_via_group_link">per grupės nuorodą</string>
<string name="description_via_contact_address_link">per adresato adreso nuorodą</string>
<string name="description_via_one_time_link">per vienkartinę nuorodą</string>
<string name="simplex_link_connection">per <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode_browser">Per naršyklę</string>
<string name="waiting_for_file">Laukiama failo</string>
<string name="voice_message_with_duration">Balso žinutė (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="voice_message_send_text">Balso žinutė…</string>
<string name="view_security_code">Rodyti saugumo kodą</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> adresas</string>
<string name="your_SMP_servers">Jūsų SMP serveriai</string>
<string name="your_ICE_servers">Jūsų ICE serveriai</string>
<string name="users_add">Pridėti profilį</string>
<string name="users_delete_all_chats_deleted">Visi pokalbiai ir žinutės bus ištrinti to neįmanoma bus atšaukti!</string>
<string name="welcome">Sveiki!</string>
<string name="attach">Pridėti</string>
<string name="observer_cant_send_message_title">Jūs negalite siųsti žinučių!</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> logotipas</string>
<string name="your_settings">Jūsų nustatymai</string>
<string name="smp_servers_your_server">Jūsų serveris</string>
<string name="smp_servers_your_server_address">Jūsų serverio adresas</string>
<string name="smp_servers_add_to_another_device">Pridėti į kitą įrenginį</string>
<string name="using_simplex_chat_servers">Naudojami <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serveriai.</string>
<string name="network_settings">Išplėstiniai tinklo nustatymai</string>
<string name="your_current_profile">Jūsų dabartinis profilis</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Jūsų profilis yra saugomas jūsų įrenginyje ir bendrinamas tik su jūsų adresatais. <xliff:g id="appName">SimpleX</xliff:g> serveriai negali matyti jūsų profilio.</string>
<string name="icon_descr_video_call">vaizdo skambutis</string>
<string name="your_calls">Jūsų skambučiai</string>
<string name="webrtc_ice_servers">WebRTC ICE serveriai</string>
<string name="your_ice_servers">Jūsų ICE serveriai</string>
<string name="snd_group_event_user_left">jūs išėjote</string>
<string name="group_member_role_admin">administratorius</string>
<string name="incognito_random_profile">Jūsų atsitiktinis profilis</string>
<string name="chat_preferences_you_allow">Jūs leidžiate</string>
<string name="your_preferences">Jūsų nuostatos</string>
<string name="v4_3_voice_messages">Balso žinutės</string>
<string name="voice_messages">Balso žinutės</string>
<string name="chat_preferences_yes">taip</string>
<string name="you_joined_this_group">Jūs prisijungėte prie šios grupės</string>
<string name="database_downgrade_warning">Įspėjimas: galite prarasti tam tikrus duomenis!</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Jūs nustosite gauti žinutes iš šios grupės. Pokalbio istorija bus išsaugota.</string>
<string name="group_info_member_you">jūs: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="personal_welcome">Sveiki, <xliff:g>%1$s</xliff:g>!</string>
<string name="your_chats">Jūsų pokalbiai</string>
<string name="wrong_passphrase">Neteisinga duomenų bazės slaptafrazė</string>
<string name="icon_descr_video_snd_complete">Vaizdo įrašas išsiųstas</string>
<string name="voice_message">Balso žinutė</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> komanda</string>
<string name="whats_new">Kas naujo</string>
</resources>

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