Compare commits

..

177 Commits

Author SHA1 Message Date
spaced4ndy
061d2c25ed core: clean up file descriptions older than server expiration 2023-04-20 17:05:02 +04: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
Evgeny Poberezkin
05c4a6c682 android: support for ARMv7a and Android 8+ (#2038)
* add armv7a

* disable armv6l, that is lacking SMP atomics

* Add Android 8 setting (API Ver 26)

* Drop x86_64-linux, this makes no sense with `pkgs' = androidPkgs`.

* Drop mis-labled x86_64-linux:lib:support (it was aarch64-android)

* Drop x86_64-android, these do not exist in nixpkgs

The ones set up were aarch64-android anyway (pkgs' = androidPkgs)

* android: support Android 8+, armeabi-v7a (32 bit) (#2012)

* test

* stubs for allowing to launch the app

* more stubs and minSdk lowered to 26

* replaced functions that supported on higher API levels with other functions

* animated images on lower API levels and write permission

* updated abi filter and scripts for downloading libs

* changed compression script for multiple apks

* cmake changes

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>

* update haskell.nix ref

* bump hackage

* bump haskell.nix (again)

* build-android: add armv7

* flake.nix: remove local nixconf

This change to flake.nix breaks build-android.sh script by forcing user
to input y/n. AFAIK, this cannot be automated and I rather not include
workarounds like piping "yes n | nix build ...".

* build-android.sh: update nix version

* flake.{nix,lock}: testing

* flake.{nix,lock}: restore to original

* update android/prepare script to use zip archives

* update gradle file

* android: 4.6-beta.0 (104)

* android: abi filter for bundle (#2075)

* android: abi filter for bundle

* removed log

---------

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

---------

Co-authored-by: Moritz Angermann <moritz.angermann@gmail.com>
Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
Co-authored-by: shum <shum@liber.li>
2023-03-25 21:51:27 +00:00
Evgeny Poberezkin
b2aec6d6a7 4.6: Android 107, iOS 134 2023-03-25 17:40:37 +00:00
Evgeny Poberezkin
09c4609b6c website: translations (#2077)
* Translated using Weblate (Dutch)

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

* Translated using Weblate (Spanish)

Currently translated at 14.2% (30 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 14.2% (30 of 211 strings)

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

* Translated using Weblate (Dutch)

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

* Translated using Weblate (Spanish)

Currently translated at 14.6% (31 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 14.6% (31 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 15.1% (32 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 15.1% (32 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 15.6% (33 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 15.6% (33 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 26.5% (56 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 26.5% (56 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 28.9% (61 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 28.9% (61 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 30.3% (64 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 30.3% (64 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 31.2% (66 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 32.7% (69 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 32.7% (69 of 211 strings)

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

* Translated using Weblate (Dutch)

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

* Translated using Weblate (Spanish)

Currently translated at 14.2% (30 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 14.2% (30 of 211 strings)

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

* Translated using Weblate (Dutch)

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

* Translated using Weblate (Spanish)

Currently translated at 14.6% (31 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 14.6% (31 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 15.1% (32 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 15.1% (32 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 15.6% (33 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 15.6% (33 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 26.5% (56 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 26.5% (56 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 28.9% (61 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 28.9% (61 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 30.3% (64 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 30.3% (64 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 31.2% (66 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 32.7% (69 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 32.7% (69 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/

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: 4 Bi 5aYzVk 93FCVjWLWxh44XH3984teVSfjwFYmUGUrbvnHwGirk9 <userfifteen.seventeen@mailfence.com>
2023-03-25 17:24:40 +00:00
Evgeny Poberezkin
f6d2aa7aae mobile: translations (#2076)
* Translated using Weblate (Chinese (Simplified))

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

* Translated using Weblate (Chinese (Simplified))

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

* Translated using Weblate (Czech)

Currently translated at 99.2% (933 of 940 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

* Translated using Weblate (Chinese (Simplified))

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

* 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 (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 (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 (Russian)

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

* 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 (Italian)

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (940 of 940 strings)

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

* ios: import/export localizations

---------

Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
2023-03-25 17:21:38 +00:00
Evgeny Poberezkin
92facf58f7 android: fix layout of moderated item 2023-03-25 17:02:15 +00:00
Evgeny Poberezkin
15c36c5a84 android: fix JSON parsing for errors not defined in the UI 2023-03-25 16:27:39 +00:00
Evgeny Poberezkin
cea0543e98 android: make search field always visible in user profiles view 2023-03-25 16:10:46 +00:00
Evgeny Poberezkin
c0bbe77788 android: fix deleting active and hidden user profile, fix incorrect log 2023-03-25 15:25:52 +00:00
Evgeny Poberezkin
9f8cbe140d android: minor lint fixes 2023-03-25 10:25:17 +00: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
314 changed files with 38336 additions and 5341 deletions

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

1
.gitignore vendored
View File

@@ -75,3 +75,4 @@ website/package-lock.json
# Ignore test files
website/.cache
website/test/stubs-layout-cache/_includes/*.js
apps/android/app/release

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.).

View File

@@ -9,15 +9,12 @@ android {
defaultConfig {
applicationId "chat.simplex.app"
minSdk 29
minSdk 26
targetSdk 32
versionCode 106
versionName "4.6-beta.2"
versionCode 116
versionName "5.0-beta.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters 'arm64-v8a'
}
vectorDrawables {
useSupportLibrary true
}
@@ -77,10 +74,38 @@ android {
jniLibs.useLegacyPackaging = compression_level != "0"
}
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
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'
} else {
splits {
abi {
enable true
reset()
if (isRelease) {
include 'arm64-v8a', 'armeabi-v7a'
} else {
include 'arm64-v8a', 'armeabi-v7a'
universalApk false
}
}
}
}
}
dependencies {
@@ -129,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'
@@ -136,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
}
}
@@ -156,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 = ""
@@ -196,6 +224,8 @@ tasks.register("compressApk") {
if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') {
new File(outputDir, "app-release.apk").renameTo(new File(outputDir, "simplex.apk"))
new File(outputDir, "app-armeabi-v7a-release.apk").renameTo(new File(outputDir, "simplex-armv7a.apk"))
new File(outputDir, "app-arm64-v8a-release.apk").renameTo(new File(outputDir, "simplex.apk"))
}
// View all gradle properties set

View File

@@ -53,10 +53,6 @@ add_library( support SHARED IMPORTED )
set_target_properties( support PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so)
add_library( crypto SHARED IMPORTED )
set_target_properties( crypto PROPERTIES IMPORTED_LOCATION
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libcrypto.so)
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
@@ -64,7 +60,7 @@ set_target_properties( crypto PROPERTIES IMPORTED_LOCATION
target_link_libraries( # Specifies the target library.
app-lib
simplex support crypto
simplex support
# Links the target library to the log library
# included in the NDK.

View File

@@ -7,6 +7,17 @@ void hs_init(int * argc, char **argv[]);
void setLineBuffering(void);
int pipe_std_to_socket(const char * name);
extern void __svfscanf(void){};
extern void __vfwscanf(void){};
extern void __memset_chk_fail(void){};
extern void __strcpy_chk_generic(void){};
extern void __strcat_chk_generic(void){};
extern void __libc_globals(void){};
extern void __rel_iplt_start(void){};
// Android 9 only, not 13
extern void reallocarray(void){};
JNIEXPORT jint JNICALL
Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE);
@@ -24,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);
@@ -33,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);

View File

@@ -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()

View File

@@ -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) }

View File

@@ -326,4 +326,4 @@ class SimplexService: Service() {
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
}
}
}

View File

@@ -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() {

View File

@@ -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)

View File

@@ -8,4 +8,4 @@ val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
)

View File

@@ -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
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -106,4 +106,4 @@ class CallManager(val chatModel: ChatModel) {
chatModel.controller.ntfManager.cancelCallNotification()
}
}
}
}

View File

@@ -92,7 +92,7 @@ fun ActiveCallView(chatModel: ChatModel) {
am.registerAudioDeviceCallback(audioCallback, null)
val pm = (SimplexApp.context.getSystemService(Context.POWER_SERVICE) as PowerManager)
val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, "proximityLock")
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, SimplexApp.context.packageName + ":proximityLock")
} else {
null
}

View File

@@ -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(

View File

@@ -48,4 +48,4 @@ class SoundPlayer {
companion object {
val shared = SoundPlayer()
}
}
}

View File

@@ -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 = { _ -> },

View File

@@ -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) {

View File

@@ -7,17 +7,15 @@ import android.Manifest
import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.graphics.ImageDecoder.DecodeException
import android.graphics.*
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.*
@@ -33,7 +31,6 @@ 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 chat.simplex.app.*
@@ -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,14 +180,16 @@ 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? ->
if (uri != null) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
val bitmap: Bitmap? = getBitmapFromUri(uri)
if (bitmap != null) {
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
}
}
}
val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
@@ -199,50 +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 source = ImageDecoder.createSource(context.contentResolver, uri)
val drawable = try {
ImageDecoder.decodeDrawable(source)
} catch (e: DecodeException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.image_decoding_exception_title),
text = generalGetString(R.string.image_decoding_exception_desc)
)
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}")
null
}
var bitmap: Bitmap? = if (drawable != null) ImageDecoder.decodeBitmap(source) else null
if (drawable is AnimatedImageDrawable) {
// 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))
@@ -250,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()
@@ -273,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
}
@@ -398,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)
@@ -437,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]))
}
}
}
}
@@ -479,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)
}
}
@@ -601,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
)
@@ -640,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 -> {}
}
@@ -773,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)
@@ -798,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()
}

View File

@@ -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

View File

@@ -8,10 +8,12 @@ import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Build
import android.text.InputType
import android.util.Log
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.*
import android.widget.EditText
import android.widget.TextView
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -50,6 +52,7 @@ import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.*
import java.lang.reflect.Field
@Composable
fun SendMsgView(
@@ -72,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) }
@@ -152,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
}
)
}
@@ -224,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)
@@ -240,7 +241,17 @@ private fun NativeKeyboard(
editText.background = drawable
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
editText.setText(cs.message)
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) }
if (Build.VERSION.SDK_INT >= 29) {
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) }
} else {
try {
val f: Field = TextView::class.java.getDeclaredField("mCursorDrawableRes")
f.isAccessible = true
f.set(editText, R.drawable.edit_text_cursor)
} catch (e: Exception) {
Log.e(chat.simplex.app.TAG, e.stackTraceToString())
}
}
editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
editText

View File

@@ -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")
}
}

View File

@@ -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),

View File

@@ -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 = {},

View File

@@ -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,

View File

@@ -154,4 +154,4 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
// }
// }
//}
//}

View File

@@ -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),

View File

@@ -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 -> {}

View File

@@ -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)
}
)
}
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -1,5 +1,7 @@
package chat.simplex.app.views.chat.item
import android.Manifest
import android.os.Build
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -20,11 +22,11 @@ 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.*
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.datetime.Clock
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
@@ -40,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,
@@ -101,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) {
@@ -128,14 +127,20 @@ 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)
ItemAction(stringResource(R.string.save_verb), Icons.Outlined.SaveAlt, onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage -> saveImage(context, cItem.file)
is MsgContent.MCFile -> saveFileLauncher.launch(cItem.file?.fileName)
is MsgContent.MCVoice -> saveFileLauncher.launch(cItem.file?.fileName)
is MsgContent.MCImage -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
saveImage(context, cItem.file)
} else {
writePermissionState.launchPermissionRequest()
}
}
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
else -> {}
}
showMenu.value = false
@@ -158,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)
}
@@ -170,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),
@@ -214,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)
}
}
@@ -234,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)
@@ -260,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,
@@ -297,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),
@@ -322,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)
@@ -373,6 +407,7 @@ fun PreviewChatItemView() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
@@ -393,6 +428,7 @@ fun PreviewChatItemViewDeletedContent() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},

View File

@@ -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 != "") {

View File

@@ -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
)
}
}

View File

@@ -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
)

View File

@@ -1,8 +1,7 @@
package chat.simplex.app.views.chat.item
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
@@ -31,10 +30,12 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
MarkedDeletedText(String.format(generalGetString(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
} else {
MarkedDeletedText(generalGetString(R.string.marked_deleted_description))
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 {
MarkedDeletedText(generalGetString(R.string.marked_deleted_description))
}
}
CIMetaView(ci, timedMessagesTTL)
}

View File

@@ -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))

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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)
},

View File

@@ -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)
}
}

View File

@@ -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 = {},
)
}
}
}

View File

@@ -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(

View File

@@ -8,7 +8,6 @@ import SectionView
import android.content.Context
import android.content.res.Configuration
import android.net.Uri
import android.os.FileUtils
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
@@ -40,6 +39,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.*
import kotlinx.coroutines.*
import kotlinx.datetime.*
import org.apache.commons.io.IOUtils
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
@@ -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))
@@ -620,7 +621,7 @@ private fun saveArchiveFromUri(context: Context, importedArchiveUri: Uri): Strin
if (inputStream != null && archiveName != null) {
val archivePath = "${context.cacheDir}/$archiveName"
val destFile = File(archivePath)
FileUtils.copy(inputStream, FileOutputStream(destFile))
IOUtils.copy(inputStream, FileOutputStream(destFile))
archivePath
} else {
Log.e(TAG, "saveArchiveFromUri null inputStream")
@@ -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))

View File

@@ -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
)
})
}
}

View File

@@ -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()
}
}

View File

@@ -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,

View File

@@ -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()
}

View File

@@ -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)
}
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -34,7 +34,7 @@ fun DefaultTopAppBar(
if (!showSearch) {
title?.invoke()
} else {
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), onSearchValueChanged)
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), alwaysVisible = false, onSearchValueChanged)
}
},
backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight,
@@ -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
)
}
}

View File

@@ -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()
}

View File

@@ -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,
)
}
}

View File

@@ -6,7 +6,6 @@ import android.content.*
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.PackageManager
import android.graphics.*
import android.graphics.ImageDecoder.DecodeException
import android.net.Uri
import android.provider.MediaStore
import android.util.Base64
@@ -19,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
@@ -114,7 +112,7 @@ fun base64ToBitmap(base64ImageString: String): Bitmap {
class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() {
@CallSuper
override fun createIntent(context: Context, input: Void?): Intent {
tmpFile = File.createTempFile("image", ".bmp", context.filesDir)
tmpFile = File.createTempFile("image", ".bmp", File(getAppFilesDirectory(SimplexApp.context)))
// Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system
tmpFile?.deleteOnExit()
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!)
@@ -175,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 {
@@ -205,17 +214,10 @@ fun GetImageBottomSheet(
val context = LocalContext.current
val processPickedImage = { uri: Uri? ->
if (uri != null) {
val source = ImageDecoder.createSource(context.contentResolver, uri)
try {
val bitmap = ImageDecoder.decodeBitmap(source)
val bitmap = getBitmapFromUri(uri)
if (bitmap != null) {
imageBitmap.value = uri
onImageChange(bitmap)
} catch (e: DecodeException) {
Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}")
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.image_decoding_exception_title),
text = generalGetString(R.string.image_decoding_exception_desc)
)
}
}
}
@@ -256,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) {

View File

@@ -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)

View File

@@ -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) {

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
@@ -29,15 +30,18 @@ import kotlinx.coroutines.delay
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) {
fun SearchTextField(modifier: Modifier, placeholder: String, alwaysVisible: Boolean, onValueChange: (String) -> Unit) {
var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) {
focusRequester.requestFocus()
delay(200)
keyboard?.show()
if (!alwaysVisible) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
delay(200)
keyboard?.show()
}
}
DisposableEffect(Unit) {
@@ -87,7 +91,14 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
Text(placeholder)
},
trailingIcon = if (searchText.text.isNotEmpty()) {{
IconButton({ searchText = TextFieldValue(""); onValueChange("") }) {
IconButton({
if (alwaysVisible) {
keyboard?.hide()
focusManager.clearFocus()
}
searchText = TextFieldValue("");
onValueChange("")
}) {
Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
}
}} else null,

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.helpers
import android.Manifest
import android.content.*
import android.net.Uri
import android.provider.MediaStore
@@ -81,6 +82,7 @@ fun imageMimeType(fileName: String): String {
}
}
/** Before calling, make sure the user allows to write to external storage [Manifest.permission.WRITE_EXTERNAL_STORAGE] */
fun saveImage(cxt: Context, ciFile: CIFile?) {
val filePath = getLoadedFilePath(cxt, ciFile)
val fileName = ciFile?.fileName

View File

@@ -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)

View File

@@ -9,6 +9,8 @@ import android.content.res.Configuration
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
@@ -33,10 +35,12 @@ import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider
import androidx.core.text.HtmlCompat
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import org.apache.commons.io.IOUtils
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
@@ -234,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"
}
@@ -322,6 +332,14 @@ fun getFileName(context: Context, uri: Uri): String? {
}
}
fun getAppFilePath(context: Context, uri: Uri): String? {
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
getAppFilePath(context, cursor.getString(nameIndex))
}
}
fun getFileSize(context: Context, uri: Uri): Long? {
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
@@ -330,9 +348,48 @@ fun getFileSize(context: Context, uri: Uri): Long? {
}
}
fun getBitmapFromUri(uri: Uri, withAlertOnException: Boolean = true): Bitmap? {
return if (Build.VERSION.SDK_INT >= 28) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
try {
ImageDecoder.decodeBitmap(source)
} catch (e: android.graphics.ImageDecoder.DecodeException) {
Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}")
if (withAlertOnException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.image_decoding_exception_title),
text = generalGetString(R.string.image_decoding_exception_desc)
)
}
null
}
} else {
BitmapFactory.decodeFile(getAppFilePath(SimplexApp.context, uri))
}
}
fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable? {
return if (Build.VERSION.SDK_INT >= 28) {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
try {
ImageDecoder.decodeDrawable(source)
} catch (e: android.graphics.ImageDecoder.DecodeException) {
if (withAlertOnException) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.image_decoding_exception_title),
text = generalGetString(R.string.image_decoding_exception_desc)
)
}
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}")
null
}
} else {
Drawable.createFromPath(getAppFilePath(SimplexApp.context, uri))
}
}
fun saveImage(context: Context, uri: Uri): String? {
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source)
val bitmap = getBitmapFromUri(uri) ?: return null
return saveImage(context, bitmap)
}
@@ -403,7 +460,7 @@ fun saveFileFromUri(context: Context, uri: Uri): String? {
if (inputStream != null && fileToSave != null) {
val destFileName = uniqueCombine(context, fileToSave)
val destFile = File(getAppFilePath(context, destFileName))
FileUtils.copy(inputStream, FileOutputStream(destFile))
IOUtils.copy(inputStream, FileOutputStream(destFile))
destFileName
} else {
Log.e(chat.simplex.app.TAG, "Util.kt saveFileFromUri null inputStream")
@@ -439,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()]
@@ -486,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)
@@ -543,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()
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)))
})
}

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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 = {}
)

View File

@@ -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() {

View File

@@ -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)
}
}
}

View File

@@ -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))
}

View File

@@ -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)
)
}
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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 "简体中文"
)

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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 = { {} },

View File

@@ -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

View File

@@ -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)
)
}

View File

@@ -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()}")

View File

@@ -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()

View File

@@ -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()

View File

@@ -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,27 +57,18 @@ 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) } } },
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
showSettingsModalWithSearch = { modalView ->
ModalManager.shared.showCustomModal { close ->
val search = mutableStateOf("")
var showSearch by remember { mutableStateOf(false) }
val search = rememberSaveable { mutableStateOf("") }
ModalView(
{ if (showSearch) { showSearch = false } else close() },
{ close() },
if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight,
endButtons = {
if (!showSearch) {
IconButton({ showSearch = true }) {
Icon(Icons.Outlined.Search, stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary)
}
} else {
BackHandler { showSearch = false }
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go)) { search.value = it }
}
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), alwaysVisible = true) { search.value = it }
},
content = { modalView(chatModel, search) })
}
@@ -134,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,
@@ -184,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()
@@ -215,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)
}
}
@@ -311,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(
@@ -326,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)
}
}
}
@@ -378,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),
@@ -390,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),
@@ -403,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) {
@@ -532,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)
}
)
}
@@ -552,9 +535,8 @@ fun PreviewSettingsLayout() {
encrypted = false,
incognito = remember { mutableStateOf(false) },
incognitoPref = SharedPreference({ false }, {}),
developerTools = SharedPreference({ false }, {}),
userDisplayName = "Alice",
setPerformLA = {},
setPerformLA = { _, _ -> },
showModal = { {} },
showSettingsModal = { {} },
showSettingsModalWithSearch = { },

View File

@@ -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 = { _, _, _ -> }
)
}

View File

@@ -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)
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)
},
)
}
}

View File

@@ -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))
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<solid android:color="@color/highOrLowLight" />
<size android:width="1dp" />
</shape>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

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