Compare commits

...

237 Commits

Author SHA1 Message Date
Evgeny Poberezkin
c7f5443920 fix 2023-04-07 11:01:33 +01:00
Evgeny Poberezkin
ccff79aa59 core: allow messages with contact requests 2023-04-07 10:15:19 +01: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
Evgeny Poberezkin
d0cf550b51 4.6-beta.2: Android 133, iOS 106 2023-03-25 08:28:29 +00:00
Evgeny Poberezkin
a86725480f Translated using Weblate (Dutch) (#2073)
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/

Co-authored-by: John m <jvanmanen@gmail.com>
2023-03-24 22:51:50 +00:00
Evgeny Poberezkin
9ad22e1f6d mobile: translations (#2062)
* Translated using Weblate (Russian)

Currently translated at 99.8% (1007 of 1009 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 96.8% (977 of 1009 strings)

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

* Translated using Weblate (Russian)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 98.1% (990 of 1009 strings)

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

* Translated using Weblate (Russian)

Currently translated at 99.8% (1007 of 1009 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 96.8% (977 of 1009 strings)

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

* Translated using Weblate (Russian)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 98.1% (990 of 1009 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 98.4% (993 of 1009 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 99.2% (1001 of 1009 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 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 (French)

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 98.6% (995 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 (Simplified))

Currently translated at 96.4% (906 of 939 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (1009 of 1009 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% (939 of 939 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.1% (1000 of 1009 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Spanish)

Currently translated at 96.0% (969 of 1009 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% (1009 of 1009 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% (939 of 939 strings)

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

* Added translation using Weblate (Finnish)

* Added translation using Weblate (Finnish)

* Translated using Weblate (German)

Currently translated at 96.6% (975 of 1009 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% (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% (939 of 939 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 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 (German)

Currently translated at 100.0% (939 of 939 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 4.7% (48 of 1009 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 5.6% (53 of 939 strings)

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

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

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (1009 of 1009 strings)

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

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

Currently translated at 100.0% (939 of 939 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (German)

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

* Translated using Weblate (Russian)

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

* Translated using Weblate (French)

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

* Translated using Weblate (Chinese (Simplified))

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

* Translated using Weblate (Chinese (Traditional))

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

* Translated using Weblate (Spanish)

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

* Translated using Weblate (Dutch)

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

* Translated using Weblate (Czech)

Currently translated at 99.3% (1001 of 1008 strings)

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

* ios: import/export localizations

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: random r <epsilin@yopmail.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: Float <float.hu+@gmail.com>
2023-03-24 22:50:09 +00:00
Stanislav Dmitrenko
a266bcbae7 android: hidden and muted user profiles (#2069)
* android: hidden and muted user profiles

* swap buttons

* smaller delay

* remove unused type

* some fixes of issues

* small visual changes

* removed delay

* re-appeared calls

* update icons and colors

* disable all notifications for muted users

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-24 21:48:34 +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
Evgeny Poberezkin
b665dce383 ios: show muted user profiles in user menu, do not show badge on messages in hidden profiles (#2068) 2023-03-23 23:09:37 +00:00
Evgeny Poberezkin
f349f124d8 4.6-beta.1: Android 105, iOS 132 2023-03-23 22:02:45 +00:00
Evgeny Poberezkin
8212d7a00e mobile: fix "delete for me" moderating the received item in group (#2067) 2023-03-23 18:47:55 +00:00
Stanislav Dmitrenko
36bcb1b26e android: downgrade target sdk (#2065) 2023-03-23 16:04:53 +00:00
spaced4ndy
8d6fe2be99 core: restore stateTVar imports 2023-03-23 17:29:04 +04:00
Evgeny Poberezkin
d9571c70f2 update script to unpack ios libs 2023-03-23 10:06:13 +00:00
spaced4ndy
babbca48f8 Merge branch 'master' into xftp 2023-03-23 13:58:23 +04:00
Stanislav Dmitrenko
8c4e2e57f9 android: Show lockscreen faster (#1822)
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-23 09:51:23 +00:00
Evgeny Poberezkin
8308651f44 android: 4.6-beta.0 (104) - for f-droid, the build is in armv7a branch 2023-03-22 22:40:35 +00:00
Evgeny Poberezkin
ad881bd46a ios: 4.6 (131) 2023-03-22 22:10:31 +00:00
Evgeny Poberezkin
c037eb2d24 Revert "mobile: hide observer role from UI (to be reverted after v4.5.4 is released)"
This reverts commit 37d0bc2f14.
2023-03-22 21:38:12 +00:00
Evgeny Poberezkin
8b4353deba ios: 4.6 (130) 2023-03-22 21:35:57 +00:00
Evgeny Poberezkin
a2be0d35fb readme: Spanish translation 2023-03-22 20:50:50 +00:00
Evgeny Poberezkin
1909bdc702 android: update string 2023-03-22 20:14:52 +00:00
Evgeny Poberezkin
63909defaf website: translations (#2061)
* Translated using Weblate (Arabic)

Currently translated at 96.2% (203 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 96.2% (203 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 97.1% (205 of 211 strings)

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

* Added translation using Weblate (Ukrainian)

* Added translation using Weblate (Chinese (Simplified))

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

Currently translated at 11.8% (25 of 211 strings)

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

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

* Deleted translation using Weblate (Norwegian Bokmål)

* 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 (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 13.7% (29 of 211 strings)

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

---------

Co-authored-by: jonnysemon <johndevand@tutanota.com>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: 4 Bi 5aYzVk 93FCVjWLWxh44XH3984teVSfjwFYmUGUrbvnHwGirk9 <userfifteen.seventeen@mailfence.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
2023-03-22 19:42:54 +00:00
Evgeny Poberezkin
8544984b17 core: 4.6.0.0 2023-03-22 19:37:02 +00:00
Stanislav Dmitrenko
563984c0df android: strings for user privacy settings (#2060) 2023-03-22 19:34:43 +00:00
Evgeny Poberezkin
6500ee5fc9 mobile app translations (#2029)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 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 71.7% (692 of 965 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% (965 of 965 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 99.6% (904 of 907 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% (965 of 965 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% (907 of 907 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (907 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 1.2% (11 of 907 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 71.7% (692 of 965 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% (965 of 965 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 99.6% (904 of 907 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% (965 of 965 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% (907 of 907 strings)

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

* Added translation using Weblate (Ukrainian)

* Added translation using Weblate (Ukrainian)

* Added translation using Weblate (Lithuanian)

* Added translation using Weblate (Lithuanian)

* Translated using Weblate (Spanish)

Currently translated at 84.8% (819 of 965 strings)

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

* Translated using Weblate (Lithuanian)

Currently translated at 0.8% (8 of 965 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 0.9% (9 of 907 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Spanish)

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

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

* Deleted translation using Weblate (Norwegian Bokmål)

* Deleted translation using Weblate (Polish)

* Deleted translation using Weblate (Norwegian Bokmål)

* Deleted translation using Weblate (Polish)

* Deleted translation using Weblate (Bulgarian)

* Deleted translation using Weblate (Bulgarian)

* Translated using Weblate (German)

Currently translated at 99.2% (900 of 907 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (967 of 967 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 44.8% (434 of 967 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Spanish)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Spanish)

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

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

* ios: add Spanish

* ios: import localizations

* export localizations

* fix typo

* android: add Spanish

---------

Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: Nick Lai <nick20080808@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: Moo <hazap@hotmail.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Luis Morillo Najarro <luis_cnnvd@hotmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: Pedro Licio <amaralrj@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
2023-03-22 19:33:24 +00:00
spaced4ndy
2a9c138a23 xftp: set xftp config (#2059) 2023-03-22 22:20:12 +04:00
Evgeny Poberezkin
61d8fa02d4 ios: export localizations 2023-03-22 17:50:41 +00:00
Evgeny Poberezkin
0fac2187f0 mobile: what's new in 4.6 (#2058)
* ios: what's new in 4.6

* android: what's new in 4.6
2023-03-22 17:45:55 +00:00
Evgeny Poberezkin
1db61be860 ios: remove unused type 2023-03-22 15:59:48 +00:00
Evgeny Poberezkin
06a0dbd0f2 core, iOS: hidden and muted user profiles (#2025)
* core, ios: profile privacy design

* migration

* core: user profile privacy

* update nix dependencies

* update simplexmq

* import stateTVar

* update core library

* update UI

* update hide/show user profile

* update API, UI, fix test

* update api, UI, test

* update api call

* fix api

* update UI for hidden profiles

* filter notifications on hidden/muted profiles when inactive, alerts

* updates

* update schema, test, icon
2023-03-22 15:58:01 +00:00
spaced4ndy
47c6daf0cc xftp: set app tmp directory (#2054) 2023-03-22 18:48:38 +04:00
Evgeny Poberezkin
bcdf502ce6 core: update simplexmq 2023-03-22 09:10:54 +00:00
Stanislav Dmitrenko
f9e2f4931a android: group welcome message (#2042) 2023-03-21 23:00:20 +00:00
Stanislav Dmitrenko
8929d15df0 ios: ability to specify welcome message in a group (#2041)
* ios: ability to specify welcome message in a group

* update state in model
2023-03-21 15:15:48 +00:00
spaced4ndy
60d6a47bdb xftp: delete agent rcv files on completion, error, item delete (#2040) 2023-03-21 15:21:14 +04:00
Stanislav Dmitrenko
2f529535b1 android: turn of screen in call (#2037) 2023-03-20 21:17:13 +00:00
Stanislav Dmitrenko
90c9eae283 android: night mode splash screen (#2036) 2023-03-20 18:04:48 +00:00
Stanislav Dmitrenko
added6105b android: relay server footer (#2035) 2023-03-20 17:12:15 +00:00
Stanislav Dmitrenko
2df39b5e24 android: catch exceptions while opening a URL (#2034) 2023-03-20 16:18:41 +00:00
Stanislav Dmitrenko
3477dd9400 android: system and in-app language selector (#2033)
* android: system language selector

* in-app language selector

* refactor

* refactor

* different value for Chinese

* change language order/names

* different translation

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-20 15:47:09 +00:00
Evgeny Poberezkin
5282551f3d Merge branch 'stable' 2023-03-19 22:59:36 +00:00
Evgeny Poberezkin
dcadaaf29b docs: readme (#2031) 2023-03-19 22:58:17 +00:00
Evgeny Poberezkin
e9d6baa6ba docs: translations (#2030)
* docs: translations

* update table

* update heading/links

* update table

* update table

* update heading
2023-03-19 20:28:39 +00:00
zenobit
6ae052a7a1 Added translated docs to czech (#1963)
* Added translated docs to czech

* Dates reverted, Added link to CZ WEBRTC.md
2023-03-19 18:10:32 +00:00
Stanislav Dmitrenko
9f750c2516 android: audio session management in calls (#2026)
* android: audio session management in calls

* audio session compatibility with old APIs

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-19 17:20:43 +00:00
Evgeny Poberezkin
1fe46834f2 mobile: add Chinese interface language 2023-03-19 17:17:27 +00:00
Evgeny Poberezkin
3db85c7d37 mobile app translations (#2028)
* Translated using Weblate (Russian)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Dutch)

Currently translated at 99.8% (902 of 903 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% (903 of 903 strings)

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

* Translated using Weblate (Russian)

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

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Dutch)

Currently translated at 99.8% (902 of 903 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% (903 of 903 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 59.1% (571 of 965 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 2.9% (27 of 903 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% (965 of 965 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% (903 of 903 strings)

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

* ios: remove unused translations

* ios: import/export localizations

---------

Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: John m <jvanmanen@gmail.com>
2023-03-19 13:35:51 +00:00
Evgeny Poberezkin
1e4f1b8891 ios: export localizations 2023-03-19 12:53:41 +00:00
Evgeny Poberezkin
0fcd6d40ee Merge pull request #2001 from simplex-chat/callkit
iOS: native calls using WebRTC library and CallKit
2023-03-19 12:25:44 +00:00
Evgeny Poberezkin
aaa4ffe789 Merge branch 'master' into callkit 2023-03-19 12:14:34 +00:00
Evgeny Poberezkin
8c4720d0cb ios: link to set app interface language 2023-03-19 12:14:10 +00:00
Evgeny Poberezkin
94b97f6097 cli: /db export command 2023-03-19 11:49:30 +00:00
Evgeny Poberezkin
85800d96c8 ios: import/export localizations 2023-03-18 18:02:34 +00:00
Evgeny Poberezkin
3b4c06111a ios: update core library 2023-03-18 17:54:40 +00:00
Evgeny Poberezkin
548d695a82 mobile app translations (#2027)
* Translated using Weblate (Dutch)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 92.6% (884 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 69.1% (660 of 954 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 72.7% (694 of 954 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 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 26.1% (249 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.1% (1 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 27.1% (260 of 959 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Arabic)

Currently translated at 1.1% (11 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 35.8% (344 of 959 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Arabic)

Currently translated at 2.5% (25 of 965 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 4.8% (43 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.6% (6 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 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% (893 of 893 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 32.5% (314 of 965 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 32.5% (314 of 965 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 1.7% (16 of 893 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% (965 of 965 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% (893 of 893 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 45.5% (440 of 965 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% (954 of 954 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% (893 of 893 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% (954 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 92.6% (884 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 69.1% (660 of 954 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 72.7% (694 of 954 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 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 26.1% (249 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.1% (1 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 27.1% (260 of 959 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Arabic)

Currently translated at 1.1% (11 of 959 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (959 of 959 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 35.8% (344 of 959 strings)

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

* Translated using Weblate (German)

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

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

* Translated using Weblate (Arabic)

Currently translated at 2.5% (25 of 965 strings)

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

* Translated using Weblate (Arabic)

Currently translated at 4.8% (43 of 893 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (965 of 965 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 0.6% (6 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

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

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (965 of 965 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% (893 of 893 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 32.5% (314 of 965 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 32.5% (314 of 965 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 1.7% (16 of 893 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% (965 of 965 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% (893 of 893 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 45.5% (440 of 965 strings)

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

---------

Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Pedro Licio <amaralrj@gmail.com>
Co-authored-by: Pedro Licio <pedro@agenciaregex.com>
Co-authored-by: Float <float.hu+@gmail.com>
Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: jonnysemon <johndevand@tutanota.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: Nick Lai <nick20080808@gmail.com>
Co-authored-by: M Sarmad Qadeer <msarmadqadeer@gmail.com>
Co-authored-by: No name <CertainBot@users.noreply.hosted.weblate.org>
Co-authored-by: Luis Morillo Najarro <luis_cnnvd@hotmail.com>
2023-03-18 17:46:50 +00:00
Evgeny Poberezkin
c986a4b88b website: enable Italian and Dutch translations 2023-03-18 17:20:08 +00:00
Evgeny Poberezkin
09940ccf8d website translations (#1960)
* Added translation using Weblate (Arabic)

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

Currently translated at 20.3% (43 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 52.6% (111 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 61.6% (130 of 211 strings)

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

* Added translation using Weblate (Arabic)

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

Currently translated at 20.3% (43 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 52.6% (111 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 61.6% (130 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 69.1% (146 of 211 strings)

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

* Translated using Weblate (Czech)

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

* Translated using Weblate (Arabic)

Currently translated at 76.7% (162 of 211 strings)

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

* Translated using Weblate (Dutch)

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

* Translated using Weblate (Dutch)

Currently translated at 50.2% (106 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 95.7% (202 of 211 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 96.6% (204 of 211 strings)

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

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

* Added translation using Weblate (Italian)

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

* Added translation using Weblate (Spanish)

* remove "coming soon"

---------

Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
Co-authored-by: jonnysemon <johndevand@tutanota.com>
Co-authored-by: ManeraKai <manerakai@protonmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: random r <epsilin@yopmail.com>
2023-03-18 17:12:25 +00:00
Evgeny Poberezkin
cfc323862f update simplexmq 2023-03-18 16:28:07 +00:00
Evgeny Poberezkin
d8cc867099 update simplexmq 2023-03-18 16:18:59 +00:00
Evgeny Poberezkin
dce8a1dff9 update simplexmq 2023-03-18 13:43:01 +00:00
Evgeny Poberezkin
5bc9e014c2 update simplexmq 2023-03-18 13:26:54 +00:00
Evgeny Poberezkin
b0c9ba05f3 Merge branch 'master' into xftp 2023-03-18 11:00:30 +00:00
Evgeny Poberezkin
8a2876fca9 core: uncomment simplexmq QQ git reference, ios: update core 2023-03-18 11:00:19 +00:00
Evgeny Poberezkin
00d5f3b769 Merge branch 'master' into xftp 2023-03-18 09:59:25 +00:00
Evgeny Poberezkin
17f39ec6a0 Merge branch 'stable' 2023-03-18 09:59:08 +00:00
Evgeny Poberezkin
498ffe8a71 4.5.4: Android 103, iOS 128 2023-03-18 09:57:19 +00:00
Evgeny Poberezkin
858f0f2650 Merge branch 'master' into xftp 2023-03-18 08:38:27 +00:00
Evgeny Poberezkin
66ea2d5d71 Merge branch 'stable' 2023-03-18 08:38:10 +00:00
Evgeny Poberezkin
3dd5b5d835 core: 4.5.4.2 (add stateTVar imports) 2023-03-18 07:59:43 +00:00
Evgeny Poberezkin
9127b1bbc6 Merge pull request #2022 from simplex-chat/ep/v454
v4.5.4: add support for observer role
2023-03-17 17:14:43 +00:00
Evgeny Poberezkin
1657bcf97d core: 4.5.4.1 2023-03-17 15:02:41 +00:00
Evgeny Poberezkin
428db2f8f4 core: fix unused contact deletion (#2023)
* core: failing test for leaving and deleting the group joined via link

* fix test

* merge logic

* fix

* add condition

* refactor

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

* compiles

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-03-17 16:03:19 +04:00
Evgeny Poberezkin
a8fa9b5e58 Merge branch 'master' into xftp 2023-03-17 09:58:36 +00:00
Evgeny Poberezkin
4cc59d9fbd core: 4.5.4.0 2023-03-16 23:30:28 +00:00
Evgeny Poberezkin
c50306709b mobile: do not show "observer" overlay when user leaves group 2023-03-16 23:29:47 +00:00
Evgeny Poberezkin
37d0bc2f14 mobile: hide observer role from UI (to be reverted after v4.5.4 is released) 2023-03-16 22:58:00 +00:00
Stanislav Dmitrenko
2fda0454e3 android: group link role, add observer role (#1981)
* android: group link role, add observer role

* padding

* disabled tint for buttons

* proper layout for long display name

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-16 22:20:42 +00:00
Evgeny Poberezkin
be19af62d9 ios: group link role, add observer role (#1978)
* ios: group link role, add observer role

* prevent observers from sending in UI, clear compose state on role change
2023-03-16 22:20:20 +00:00
Evgeny Poberezkin
f915eb2a20 core: initial group member role when joining via link (#1975)
* core: initial group member role when joining via link

* fix tests

* set role when joining group via link, enable observer test

* show group link when role changes

* amend test

* check role is member or observer when creating a link
2023-03-16 22:19:51 +00:00
Evgeny Poberezkin
2bc1236a2c terminal: update help, remove user ID from terminal /smp test command (#1973)
* terminal: update help, remove user ID from terminal /smp test command

* update mobile api

* update help
2023-03-16 22:19:22 +00:00
Evgeny Poberezkin
9db1924268 ios: optionally show callkit calls in recents and update settings (#2021)
* ios: optionally show callkit calls in recents and update settings

* refactor, fix call error when starting from recents
2023-03-16 22:08:58 +00:00
Evgeny Poberezkin
7a9f220290 ios: do not suspend chat when switching to another callkit call (#2020) 2023-03-16 20:19:53 +00:00
Evgeny Poberezkin
8145387f77 ios: CallKit changed reporting logic (#2019)
* ios: CallKit changed reporting logic

* refactor, suspend chat after call when app is in background

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2023-03-16 19:57:43 +00:00
Evgeny Poberezkin
063440e735 ios: remove sheets in ActiveCallView (does not work when call accepted from background via callkit) 2023-03-16 17:18:25 +00:00
Evgeny Poberezkin
6724de09c9 ios: dismiss sheets on IncomingCallView, send notification if reportNewIncomingVoIPPushPayload fails 2023-03-16 16:59:05 +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
Evgeny Poberezkin
809cc1f234 ios: different speaker buttons on call screen 2023-03-16 08:46:13 +00:00
spaced4ndy
12200a74ff core: XFTP file transfer test (#2009) 2023-03-16 10:49:57 +04:00
Stanislav Dmitrenko
2643ea9066 ios: reverted some changes related to lockScreen (#2011)
* Revert "ios: CallKit enhancements (#2010)"

This reverts commit 840df89ca6.

* Revert "ios: CallKit integrated with app lock and screen protect (#2007)"

This reverts commit 0404b020e6.

* ios: reverted some changes related to lockScreen

* undo delay

* better support of appLock + call

* refactor

* refactor 2

* refactor 3

* refactor 4

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-15 21:09:33 +00:00
Stanislav Dmitrenko
840df89ca6 ios: CallKit enhancements (#2010)
* ios: CallKit enhancements

* better checks
2023-03-15 15:32:27 +00:00
Stanislav Dmitrenko
0404b020e6 ios: CallKit integrated with app lock and screen protect (#2007)
* ios: CallKit integrated with app lock and screen protect

* better lock mechanics

* background color

* logs

* refactor, revert auth changes

* additional state variable to allow connecting call

* fix lock screen, public logs

* show callkit option without dev tools

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-15 10:21:21 +00:00
spaced4ndy
fda41817e9 core: XFTP accept; provide save path to agent (#2005) 2023-03-14 21:51:35 +04:00
Stanislav Dmitrenko
f48cabcc0a ios: CallKit double call in background fix (#2004) 2023-03-14 15:28:34 +00:00
Stanislav Dmitrenko
f123a905d5 app icon in CallKit screen (#2003) 2023-03-14 15:19:54 +00: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
Stanislav Dmitrenko
9ec6911005 ios: CallKit integration (#1969)
* ios: CallKit integration

* notifying CallKit about outgoing call

* changes

* switching calls with CallKit

* string

* add NSE filtering entitlement

* add NSE build scheme

* remove some call limitations

* calls enhancments

* fixed calls on lockscreen

* don't display useless notification

* fix app state

* ability to answer on call from chat item via CallKit

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-14 08:12:40 +00: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
darkmaster
c4c93f881d docs: fixes listening port (server) [FR] (#2000) 2023-03-13 23:57:37 +00: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
Evgeny Poberezkin
13706c4f64 website: remove "coming soon" from released features 2023-03-12 23:44:16 +00:00
Evgeny Poberezkin
f2f4b26c35 core: update agent protocol to parameterize by entity type (#1988)
* core: update agent protocol to parameterize by entity type

* update simplexmq
2023-03-10 17:23:04 +00:00
Evgeny Poberezkin
1b7b9da07c ios: update core library 2023-03-09 23:08:53 +00:00
Evgeny Poberezkin
2817306659 core: types to support xftp (#1971)
* core: types to support xftp

* migration, amend types

* update protocol / types

* update protocol, types

* update schema, simplexmq
2023-03-09 11:01:22 +00:00
Stanislav Dmitrenko
5f587c2104 android: group link role, add observer role (#1981)
* android: group link role, add observer role

* padding

* disabled tint for buttons

* proper layout for long display name

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-07 22:27:28 +00:00
M Sarmad Qadeer
f5670c39da website: add support for overlay hash in URL (#1974)
* website: add support for overlay hash in URL

* website: update the overlay hashes

* website: fix the ui of donate button in join simplex section

* website: make the text selectable of unique & explained swiper

* scroll to popup context

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-06 22:25:54 +00:00
Stanislav Dmitrenko
c0105d135c android: UI to moderate messages to other members (#1982)
* android: UI to moderate messages to other members

* do not show moderate button on moderated, show alert

* changed item

* limiting number of lines in header

* limit text height
2023-03-06 21:58:44 +00:00
Evgeny Poberezkin
f1a9814faa ios: UI to moderate messages of other members (#1980)
* ios: UI to moderate messages of other members

* split moderate action

* do not show moderate button on moderated, show alert
2023-03-06 21:57:58 +00:00
Evgeny Poberezkin
8f0e7512be ios: group link role, add observer role (#1978)
* ios: group link role, add observer role

* prevent observers from sending in UI, clear compose state on role change
2023-03-06 13:54:43 +00:00
Evgeny Poberezkin
7d49209f79 core: initial group member role when joining via link (#1975)
* core: initial group member role when joining via link

* fix tests

* set role when joining group via link, enable observer test

* show group link when role changes

* amend test

* check role is member or observer when creating a link
2023-03-06 09:51:42 +00:00
Evgeny Poberezkin
b2e285c2c7 terminal: update help, remove user ID from terminal /smp test command (#1973)
* terminal: update help, remove user ID from terminal /smp test command

* update mobile api

* update help
2023-03-04 22:33:17 +00:00
Stanislav Dmitrenko
54020250dc ios: native WebRTC (#1933)
* ios: native WebRTC

* add video showing

* make async function better working with main thread

* wrapped code in main actor, just in case

* small change

* a little better

* enable relay

* removed unused code

* allow switching calls

* testing

* enable encryption

* testing more

* another test

* one more test

* fix remote unencrypted video

* deleted unused code related to PixelBuffer

* added MediaEncryption playground

* better playground

* better playground

* fixes

* use new encryption api

* media encryption works

* small changes

* added lib dependency

* use commit reference for lib instead of version

* video format, PIP size

* remove sample.js

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-03-02 13:17:01 +00:00
Evgeny Poberezkin
01acbb970a blog: typos 2023-03-01 20:03:06 +00:00
Evgeny Poberezkin
36cad35d46 blog: SimpleX File Transfer Protocol (XFTP) (#1965)
* blog: SimpleX File Transfer Protocol (XFTP)

* update blog

* simplify quick start

* update blog
2023-03-01 19:29:59 +00:00
spacedandy
41c9c84139 4.5.3: iOS 127 2023-03-01 19:38:50 +04:00
spacedandy
9e9ca521b0 4.5.3: Android 102 2023-03-01 18:00:49 +04:00
spaced4ndy
1927862871 android: show "export prohibited" alert on enabling app data backup with random db password set (#1962) 2023-03-01 14:36:08 +04:00
spacedandy
c80eaf8550 core: 4.5.3.1 2023-03-01 13:23:34 +04:00
Evgeny Poberezkin
9e6a35bac3 mobile: add Czech, fix translations (#1961)
* ios: import localizations

* re-export localizations

* add Czech language

* fix Czech strings

* add Czech to android
2023-03-01 08:52:56 +00:00
Evgeny Poberezkin
62ffcf94a6 translations (#1956)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 73.6% (658 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (954 of 954 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 99.7% (891 of 893 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% (893 of 893 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 78.2% (699 of 893 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 82.5% (737 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.5% (862 of 893 strings)

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

* Added translation using Weblate (Arabic)

* Added translation using Weblate (Arabic)

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 73.6% (658 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (954 of 954 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 99.7% (891 of 893 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% (893 of 893 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 78.2% (699 of 893 strings)

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

* Translated using Weblate (French)

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

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

* Translated using Weblate (German)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 82.5% (737 of 893 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 96.5% (862 of 893 strings)

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

* Added translation using Weblate (Arabic)

* Added translation using Weblate (Arabic)

* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 59.2% (565 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (893 of 893 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 0.6% (6 of 954 strings)

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

---------

Co-authored-by: Aaron H <niximi333@gmail.com>
Co-authored-by: John m <jvanmanen@gmail.com>
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: mlanp <github@lang.xyz>
Co-authored-by: M Sarmad Qadeer <msarmadqadeer@gmail.com>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Pedro Licio <pedro@agenciaregex.com>
2023-03-01 08:15:32 +00:00
Evgeny Poberezkin
2b77920dcd teminal: option to log errors and service messages to file, closes #1516 (#1957)
* teminal: option to log errors and service messages to file, closes #1516

* rename function
2023-02-28 23:26:08 +00:00
darkmaster
38b7e4d4a4 docs: fixes listening port (server) (#1958) 2023-02-27 20:15:08 +00:00
Evgeny Poberezkin
3e4d4f04ef ios: allow pasting profile image 2023-02-27 17:46:10 +00:00
Evgeny Poberezkin
f6f3d17383 ios: do not show notifications on update events in inactive profiles (#1959) 2023-02-27 16:20:54 +00:00
Evgeny Poberezkin
d5f6b76ec5 core: increase default queue sizes to 1024, fix broadcast bot not to lock when queue is smaller than the number of contacts (#1955) 2023-02-26 16:36:11 +00:00
zenobit
ae75be56ea blog: string fix (#1952) 2023-02-25 17:53:31 +00:00
Evgeny Poberezkin
50b90c4814 core: use 12 bytes IV for WebRTC frame encryption with AES-GCM (#1951)
* core: use 12 bytes IV for WebRTC frame encryption with AES-GCM

* refactor
2023-02-25 17:52:23 +00:00
+shyfire131
6eddb5f30f fix comment syntax in cabal.project.mac (#1948)
Co-authored-by: +shyfire131 <shyfire131@shyfire131.net>
2023-02-24 21:04:46 +00:00
Evgeny Poberezkin
cee8f3a4b6 website internationalization (#1922)
* website Internationalization (#1904)

* added devcontainer config

* internationalization under dev

* internationalization of _data done

* overlays internationalization done

* improved routing

* updated gitignore

* remove .devcontainer

* internationalization in progess
deleted all the intermediate files
added translation of few sections

* remaining website strings are added to translation

* done internationalization
- fully converted to i18n plugin
- wrote bash script for creating a combined translations.json

* internationalization UI done

* a quick fix

* remove jq installation

* Added translation using Weblate (German)

* Translated using Weblate (French)

Currently translated at 100.0% (212 of 212 strings)

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

* Translated using Weblate (German)

Currently translated at 42.9% (91 of 212 strings)

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

* fixes of web Internationalization (#1925)

* a quick fix

* blog route for all languages is shifted to root blog route

* wrote merge_translations.js

* remove language label from dropdown

* update language names

* refactor scripts

* remove catch from script

* Added translation using Weblate (Dutch)

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (Dutch)

Currently translated at 33.0% (70 of 212 strings)

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

* website: internationalization fixes (#1931)

* added devcontainer config

* internationalization under dev

* internationalization of _data done

* overlays internationalization done

* improved routing

* updated gitignore

* remove .devcontainer

* internationalization in progess
deleted all the intermediate files
added translation of few sections

* remaining website strings are added to translation

* done internationalization
- fully converted to i18n plugin
- wrote bash script for creating a combined translations.json

* internationalization UI done

* a quick fix

* blog route for all languages is shifted to root blog route

* wrote merge_translations.js

* remove flag from blog

* updated nav stylings & logo links

* add enabled key

* updated nav dropdown styling

* gave specific lang to i18n plugin.
overlay translations are now working.

* enable nl & nb_NO

* updated nav stylings

* updated contact.js

---------

Co-authored-by: M Sarmad Qadeer <msarmadqadeer@gmail.com>

* Translated using Weblate (German)

Currently translated at 47.6% (101 of 212 strings)

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

* fixed internationalization issues

* updated strings, refactor contact.js

* updated nav stylings for mobile

* a bit smaller padding on mobile

* Added translation using Weblate (Czech)

* Translated using Weblate (Czech)

Currently translated at 100.0% (212 of 212 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (212 of 212 strings)

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

* Translated using Weblate (German)

Currently translated at 100.0% (212 of 212 strings)

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

* enabled languages

* check/correct (#1949)

---------

Co-authored-by: M Sarmad Qadeer <MSarmadQadeer@gmail.com>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: Ophiushi <41908476+ishi-sama@users.noreply.github.com>
2023-02-24 21:03:57 +00:00
Evgeny Poberezkin
a2e5733be6 core: update/fix webrtc frame encryption function to return error (#1950)
* core: update/fix webrtc frame encryption function to return error

* ios: update C header

* more tests
2023-02-24 20:55:59 +00:00
Evgeny Poberezkin
5075657c02 ios: update core library 2023-02-23 23:36:39 +00:00
Evgeny Poberezkin
0450b1ace2 mobile: welcome/about page (#1946)
* mobile: welcome/about page

* scroll view (fixes trimmed texts)

* layout

* bigger frame

* minHeight, to allow scroling with large font

---------

Co-authored-by: spacedandy <8711996+spaced4ndy@users.noreply.github.com>
2023-02-21 15:31:41 +00:00
Evgeny Poberezkin
0ebf1da05d core: WebRTC frames encryption (#1942)
* core: WebRTC frames encryption

* test
2023-02-19 23:51:50 +00:00
Evgeny Poberezkin
07ad3edbc2 4.5.3-beta.0: Android 101, iOS 126 2023-02-19 13:42:32 +00:00
Evgeny Poberezkin
b40ed2a7f3 core: 4.5.3.0 2023-02-19 10:09:22 +00:00
Evgeny Poberezkin
29b074607c mobile: add Dutch interface (#1941)
* iOS: add Dutch language

* ios: re-export localizations

* enable Dutch in Android
2023-02-19 08:47:26 +00:00
Evgeny Poberezkin
258a157e44 translations (#1938)
* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 50.8% (485 of 953 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% (953 of 953 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 0.3% (3 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 57.0% (544 of 953 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 51.6% (460 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.1% (144 of 953 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 15.2% (145 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 14.3% (128 of 891 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 15.1% (135 of 891 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 14.3% (137 of 953 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% (953 of 953 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 25.1% (224 of 891 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 15.1% (144 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 59.4% (567 of 953 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 53.5% (477 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.5% (148 of 953 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 29.2% (261 of 891 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 15.6% (149 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 31.6% (282 of 891 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% (953 of 953 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 36.2% (323 of 891 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% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 7.4% (71 of 953 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 41.5% (370 of 891 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% (953 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 53.9% (481 of 891 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 57.5% (513 of 891 strings)

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

* Added translation using Weblate (Bulgarian)

* Added translation using Weblate (Bulgarian)

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 77.2% (688 of 891 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 83.9% (748 of 891 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% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.9% (152 of 953 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 7.9% (76 of 954 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 99.8% (953 of 954 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% (891 of 891 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 99.8% (953 of 954 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% (891 of 891 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 99.8% (953 of 954 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% (891 of 891 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% (954 of 954 strings)

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

* Added translation using Weblate (Norwegian Bokmål)

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (German)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 67.1% (641 of 954 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.7% (150 of 954 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 99.3% (948 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 61.4% (586 of 954 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 55.5% (495 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 27.2% (260 of 954 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 42.5% (406 of 954 strings)

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

* Translated using Weblate (Croatian)

Currently translated at 2.0% (18 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 66.2% (632 of 954 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 61.3% (547 of 891 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Italian)

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

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 50.8% (485 of 953 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% (953 of 953 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 0.3% (3 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 57.0% (544 of 953 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 51.6% (460 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.1% (144 of 953 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 15.2% (145 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 14.3% (128 of 891 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 15.1% (135 of 891 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 14.3% (137 of 953 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% (953 of 953 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 25.1% (224 of 891 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 15.1% (144 of 953 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 59.4% (567 of 953 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 53.5% (477 of 891 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.5% (148 of 953 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 29.2% (261 of 891 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 15.6% (149 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 31.6% (282 of 891 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% (953 of 953 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 36.2% (323 of 891 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% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Czech)

Currently translated at 100.0% (953 of 953 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 7.4% (71 of 953 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 41.5% (370 of 891 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% (953 of 953 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 53.9% (481 of 891 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 57.5% (513 of 891 strings)

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

* Added translation using Weblate (Bulgarian)

* Added translation using Weblate (Bulgarian)

* Translated using Weblate (Dutch)

Currently translated at 100.0% (953 of 953 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 77.2% (688 of 891 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 83.9% (748 of 891 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% (953 of 953 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 15.9% (152 of 953 strings)

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

* Translated using Weblate (Dutch)

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

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

* Translated using Weblate (Italian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 7.9% (76 of 954 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 99.8% (953 of 954 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% (891 of 891 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 99.8% (953 of 954 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% (891 of 891 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 99.8% (953 of 954 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% (891 of 891 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% (954 of 954 strings)

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

* Added translation using Weblate (Norwegian Bokmål)

* Added translation using Weblate (Norwegian Bokmål)

* Translated using Weblate (German)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (French)

Currently translated at 100.0% (891 of 891 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 67.1% (641 of 954 strings)

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

* Translated using Weblate (Hindi)

Currently translated at 15.7% (150 of 954 strings)

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

* Translated using Weblate (Japanese)

Currently translated at 99.3% (948 of 954 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 61.4% (586 of 954 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 55.5% (495 of 891 strings)

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

* Translated using Weblate (Chinese (Traditional))

Currently translated at 27.2% (260 of 954 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 42.5% (406 of 954 strings)

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

* Translated using Weblate (Croatian)

Currently translated at 2.0% (18 of 891 strings)

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

* Translated using Weblate (Chinese (Simplified))

Currently translated at 66.2% (632 of 954 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 61.3% (547 of 891 strings)

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

* Translated using Weblate (Russian)

Currently translated at 100.0% (954 of 954 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (891 of 891 strings)

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

---------

Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: xqhjay <137847720@qq.com>
Co-authored-by: sith-on-mars <groguko36@pm.me>
Co-authored-by: John m <jvanmanen@gmail.com>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: sith-on-mars <groguko36@tuta.io>
Co-authored-by: Bdd55oo <giggzuv9z.eofjx@aleeas.com>
Co-authored-by: Raman <translations.0l5zc@simplelogin.com>
Co-authored-by: LegendaryInfernalFox <0387agimeno@e-itaca.es>
Co-authored-by: mlanp <github@lang.xyz>
Co-authored-by: Ophiushi <Ophiushi@users.noreply.hosted.weblate.org>
Co-authored-by: tomato potato <4ryo49@protonmail.com>
Co-authored-by: Mehmed <pajazetovicmeho@gmail.com>
2023-02-19 08:23:32 +00:00
Evgeny Poberezkin
92d9a1f9f2 ios: export localizations 2023-02-18 19:32:25 +00:00
Evgeny Poberezkin
e5009a58df core: update simplexmq v4.4.1 2023-02-18 19:04:59 +00:00
Evgeny Poberezkin
35a1ce4903 core: separate core options to use in bots (#1937)
* core: separate core options to use in bots

* ci: install pkg-config for mac
2023-02-18 17:39:16 +00:00
Evgeny Poberezkin
7c4c627ee9 terminal: support multiline messages (as JSON strings) (#1936)
* terminal: support for multiline messages

* fix

* fix tests
2023-02-18 15:16:50 +00:00
Evgeny Poberezkin
b7575ec01d core: encrypt/decrypt WebRTC frames (#1935)
* core: encrypt/decrypt WebRTC frames

* swift API

* add decrypt stub

* change name

* remove unused type

* move functions

* update cabal file

* copy bytes from encrypted string
2023-02-16 20:25:37 +00:00
Evgeny Poberezkin
a0351d6f99 apps: update chat bots, readme (#1928)
* apps: update chat bots, readme

* CLI readme

* broadcast bot

* delete messages from non-publishers, better replies, support forwarding low-res images and links

* typo

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

* change

---------

Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com>
2023-02-14 07:57:27 +00:00
459 changed files with 61818 additions and 7201 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
@@ -91,8 +91,12 @@ jobs:
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
echo " flags: +openssl" >> cabal.project.local
- name: Install pkg-config for Mac
if: matrix.os == 'macos-latest'
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
@@ -108,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

5
.gitignore vendored
View File

@@ -45,10 +45,12 @@ tests/tmp
tests/tmp*
logs/
*.devcontainer
# for website
website/node_modules/
website/src/blog/
website/translations.json
website/src/_data/supported_languages.json
website/src/img/images/
website/src/images/
# Generated files
@@ -73,3 +75,4 @@ website/package-lock.json
# Ignore test files
website/.cache
website/test/stubs-layout-cache/_includes/*.js
apps/android/app/release

214
README.md
View File

@@ -1,15 +1,29 @@
| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/README.md) |
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
[![build](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml/badge.svg?branch=stable)](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml)
[![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases)
[![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases)
[![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)
| 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%">
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
[<img src="./images/trail-of-bits.jpg" height="100">](http://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) &nbsp;&nbsp;&nbsp; [<img src="./images/privacy-guides.jpg" height="80">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) &nbsp;&nbsp;&nbsp; [<img src="./images/kuketz-blog.jpg" height="80">](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/)
## Welcome to SimpleX Chat!
1. 📲 [Install the app](#install-the-app).
2. ↔️ [Connect to the team](#connect-to-the-team-via-the-app) and [join user groups](#join-user-groups).
3. 🤝 [Make a private connection](#make-a-private-connection) with a friend.
4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat).
5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations).
[Learn more about SimpleX Chat](#contents).
## Install the app
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apple_store.svg" alt="iOS app" height="42">](https://apps.apple.com/us/app/simplex-chat/id1605771084)
&nbsp;
[![Android app](https://github.com/simplex-chat/.github/blob/master/profile/images/google_play.svg)](https://play.google.com/store/apps/details?id=chat.simplex.app)
@@ -24,9 +38,92 @@
- 🔐 Double ratchet end-to-end encryption, with additional encryption layer.
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows.
- 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows.
**NEW**: Security audit by [Trail of Bits](https://www.trailofbits.com/about), the [new website](https://simplex.chat) and v4.2 released! [See the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
## Connect to the team via the app
- to ask any questions
- to suggest any improvements
- to share anything relevant
## 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)
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:
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
## Make a private connection
You need to share a link with your friend or scan a QR code from their phone, in person or during a video call, to make a connection and start messaging.
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
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.
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 |[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/)||
|🇷🇺 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/)|[![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!
## Contribute
We would love to have you join the development! You can help us with:
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- contributing to SimpleX Chat knowledge-base.
- developing features - please connect to us via chat so we can help you get started.
## Help us with donations
Huge thank you to everybody who donated to SimpleX Chat!
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, would make a big difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
Thank you,
Evgeny
SimpleX Chat founder
## Contents
@@ -38,16 +135,11 @@
- [Users own SimpleX network](#users-own-simplex-network)
- [Frequently asked questions](#frequently-asked-questions)
- [News and updates](#news-and-updates)
- [Make a private connection](#make-a-private-connection)
- [Quick installation of a terminal app](#zap-quick-installation-of-a-terminal-app)
- [SimpleX Platform design](#simplex-platform-design)
- [Privacy: technical details and limitations](#privacy-technical-details-and-limitations)
- [For developers](#for-developers)
- [Roadmap](#roadmap)
- [Join a user group](#join-a-user-group)
- [Translate the apps](#translate-the-apps)
- [Contribute](#contribute)
- [Help us with donations](#help-us-with-donations)
- [Disclaimers, Security contact, License](#disclaimers)
## Why privacy matters
@@ -88,26 +180,22 @@ 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).
[All updates](./blog)
## Make a private connection
You need to share a link or scan a QR code (in person or during a video call) to make a connection and start messaging.
The channel through which you share the link does not have to be secure - it is enough that you can confirm who sent you the message and that your SimpleX connection is established.
<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app1.png" alt="Make a private connection" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app2.png" alt="Conversation" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/arrow.png" height="360"> <img src="https://github.com/simplex-chat/.github/blob/master/profile/images/app3.png" alt="Video call" height="360">
## :zap: Quick installation of a terminal app
```sh
@@ -202,17 +290,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.).
@@ -225,70 +317,6 @@ If you are considering developing with SimpleX platform please get in touch for
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- High capacity multi-node SMP relays.
## Join a user group
You can join an English-speaking group if you want to ask any questions: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D)
There are also several groups in languages other than English, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users. We do not always answer questions there, so please ask them in one of the English-speaking groups.
- [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking).
- [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking).
- [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking).
- [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
Join via the app to share what's going on and ask any questions!
## Translate the apps
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps are translated to many other languages. Join our translators to help SimpleX grow faster!
Current interface languages:
- English (development language)
- German: [@mlanp](https://github.com/mlanp)
- French: [@ishi_sama](https://github.com/ishi-sama)
- Italian: [@unbranched](https://github.com/unbranched)
- Russian: project team
Languages in progress: Chinese, Hindi, Czech, Japanese, Dutch and [many others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
## Contribute
We would love to have you join the development! You can contribute to SimpleX Chat with:
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- developing features - please connect to us via chat so we can help you get started.
## Help us with donations
Huge thank you to everybody who donated to SimpleX Chat!
We are prioritizing users privacy and security - it would be impossible without your support.
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
Your donations help us raise more funds any amount, even the price of the cup of coffee, would make a big difference for us.
It is possible to donate via:
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
Thank you,
Evgeny
SimpleX Chat founder
## Disclaimers
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.

View File

@@ -9,15 +9,12 @@ android {
defaultConfig {
applicationId "chat.simplex.app"
minSdk 29
minSdk 26
targetSdk 32
versionCode 100
versionName "4.5.2"
versionCode 111
versionName "4.6.1-beta.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters 'arm64-v8a'
}
vectorDrawables {
useSupportLibrary true
}
@@ -77,9 +74,26 @@ android {
jniLibs.useLegacyPackaging = compression_level != "0"
}
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
if (isRelease) {
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", "ru", "de", "fr", "it")
android.defaultConfig.resConfigs("en", "cs", "de", "es", "fr", "it", "nl", "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
}
}
}
}
}
@@ -129,6 +143,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 +153,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 +166,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 +213,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

@@ -31,6 +31,7 @@
android:extractNativeLibs="${extract_native_libs}"
android:supportsRtl="true"
android:theme="@style/Theme.SimpleX">
<!-- android:localeConfig="@xml/locales_config"-->
<!-- Main activity -->
<activity

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,21 +35,24 @@ 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);
extern char *chat_parse_markdown(const char *str);
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);
@@ -85,3 +99,13 @@ Java_chat_simplex_app_SimplexAppKt_chatParseServer(JNIEnv *env, __unused jclass
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_app_SimplexAppKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) {
const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE);
const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt));
(*env)->ReleaseStringUTFChars(env, pwd, _pwd);
(*env)->ReleaseStringUTFChars(env, salt, _salt);
return res;
}

View File

@@ -3,9 +3,7 @@ package chat.simplex.app
import android.app.Application
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.os.*
import android.os.SystemClock.elapsedRealtime
import android.util.Log
import android.view.WindowManager
@@ -41,9 +39,8 @@ import chat.simplex.app.views.database.DatabaseErrorView
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
class MainActivity: FragmentActivity() {
companion object {
@@ -68,6 +65,7 @@ class MainActivity: FragmentActivity() {
super.onCreate(savedInstanceState)
// testJson()
val m = vm.chatModel
applyAppLocale(m.controller.appPrefs.appLanguage)
// When call ended and orientation changes, it re-process old intent, it's unneeded.
// Only needed to be processed on first creation of activity
if (savedInstanceState == null) {
@@ -110,8 +108,8 @@ class MainActivity: FragmentActivity() {
processExternalIntent(intent, vm.chatModel)
}
override fun onStart() {
super.onStart()
override fun onResume() {
super.onResume()
val enteredBackgroundVal = enteredBackground.value
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
runAuthenticate()
@@ -130,6 +128,7 @@ class MainActivity: FragmentActivity() {
override fun onStop() {
super.onStop()
VideoPlayer.stopAll()
enteredBackground.value = elapsedRealtime()
}
@@ -161,25 +160,31 @@ class MainActivity: FragmentActivity() {
} else {
userAuthorized.value = false
ModalManager.shared.closeModals()
authenticate(
generalGetString(R.string.auth_unlock),
generalGetString(R.string.auth_log_in_using_credential),
this@MainActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
userAuthorized.value = true
is LAResult.Error, LAResult.Failed ->
laFailed.value = true
LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.appPrefs.performLA.set(false)
laUnavailableTurningOffAlert()
// To make Main thread free in order to allow to Compose to show blank view that hiding content underneath of it faster on slow devices
CoroutineScope(Dispatchers.Default).launch {
delay(50)
withContext(Dispatchers.Main) {
authenticate(
generalGetString(R.string.auth_unlock),
generalGetString(R.string.auth_log_in_using_credential),
this@MainActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
userAuthorized.value = true
is LAResult.Error, LAResult.Failed ->
laFailed.value = true
LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.appPrefs.performLA.set(false)
laUnavailableTurningOffAlert()
}
}
}
}
)
}
)
}
}
}
@@ -262,14 +267,6 @@ fun MainPage(
setPerformLA: (Boolean) -> Unit,
showLANotice: () -> Unit
) {
// this with LaunchedEffect(userAuthorized.value) fixes bottom sheet visibly collapsing after authentication
var chatsAccessAuthorized by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(userAuthorized.value) {
if (chatModel.controller.appPrefs.performLA.get()) {
delay(500L)
}
chatsAccessAuthorized = userAuthorized.value == true
}
var showChatDatabaseError by rememberSaveable {
mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null)
}
@@ -328,7 +325,7 @@ fun MainPage(
}
}
onboarding == null || userCreated == null -> SplashView()
!chatsAccessAuthorized -> {
userAuthorized.value != true -> {
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
authView()
} else {
@@ -405,8 +402,9 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId")
if (chatId != null) {
withBGApi {
if (userId != null && userId != chatModel.currentUser.value?.userId) {
chatModel.controller.changeActiveUser(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
chatModel.clearOverlays.value = true
@@ -417,8 +415,9 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
NtfManager.ShowChatsAction -> {
Log.d(TAG, "processNotificationIntent: ShowChatsAction")
withBGApi {
if (userId != null && userId != chatModel.currentUser.value?.userId) {
chatModel.controller.changeActiveUser(userId)
awaitChatStartedIfNeeded(chatModel)
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
chatModel.controller.changeActiveUser(userId, null)
}
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
@@ -508,6 +507,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,22 +26,26 @@ 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
external fun chatParseMarkdown(str: String): String
external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
class SimplexApp: Application(), LifecycleEventObserver {
lateinit var chatController: ChatController
var isAppOnForeground: Boolean = false
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
val defaultLocale: Locale = Locale.getDefault()
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

@@ -94,6 +94,32 @@ class ChatModel(val controller: ChatController) {
val filesToDelete = mutableSetOf<File>()
val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get())
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value
} else {
users.firstOrNull { it.user.userId == userId }?.user
}
private fun getUserIndex(user: User): Int =
users.indexOfFirst { it.user.userId == user.userId }
fun updateUser(user: User) {
val i = getUserIndex(user)
if (i != -1) {
users[i] = users[i].copy(user = user)
}
if (currentUser.value?.userId == user.userId) {
currentUser.value = user
}
}
fun removeUser(user: User) {
val i = getUserIndex(user)
if (i != -1 && users[i].user.userId != currentUser.value?.userId) {
users.removeAt(i)
}
}
fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null
fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id }
fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId }
@@ -166,10 +192,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)
}
}
}
}
@@ -196,19 +225,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
}
}
@@ -422,13 +451,19 @@ data class User(
val localDisplayName: String,
val profile: LocalProfile,
val fullPreferences: FullChatPreferences,
val activeUser: Boolean
val activeUser: Boolean,
val showNtfs: Boolean,
val viewPwdHash: UserPwdHash?
): NamedChat {
override val displayName: String get() = profile.displayName
override val fullName: String get() = profile.fullName
override val image: String? get() = profile.image
override val localAlias: String = ""
val hidden: Boolean = viewPwdHash != null
val showNotifications: Boolean = activeUser || showNtfs
companion object {
val sampleData = User(
userId = 1,
@@ -436,11 +471,19 @@ data class User(
localDisplayName = "alice",
profile = LocalProfile.sampleData,
fullPreferences = FullChatPreferences.sampleData,
activeUser = true
activeUser = true,
showNtfs = true,
viewPwdHash = null,
)
}
}
@Serializable
data class UserPwdHash(
val hash: String,
val salt: String
)
@Serializable
data class UserInfo(
val user: User,
@@ -486,6 +529,24 @@ data class Chat (
val chatItems: List<ChatItem>,
val chatStats: ChatStats = ChatStats(),
) {
val userCanSend: Boolean
get() = when (chatInfo) {
is ChatInfo.Direct -> true
is ChatInfo.Group -> {
val m = chatInfo.groupInfo.membership
m.memberActive && m.memberRole >= GroupMemberRole.Member
}
else -> false
}
val userIsObserver: Boolean get() = when(chatInfo) {
is ChatInfo.Group -> {
val m = chatInfo.groupInfo.membership
m.memberActive && m.memberRole == GroupMemberRole.Observer
}
else -> false
}
val id: String get() = chatInfo.id
@Serializable
@@ -963,11 +1024,13 @@ class GroupMemberRef(
@Serializable
enum class GroupMemberRole(val memberRole: String) {
@SerialName("member") Member("member"), // order matters in comparisons
@SerialName("observer") Observer("observer"), // order matters in comparisons
@SerialName("member") Member("member"),
@SerialName("admin") Admin("admin"),
@SerialName("owner") Owner("owner");
val text: String get() = when (this) {
Observer -> generalGetString(R.string.group_member_role_observer)
Member -> generalGetString(R.string.group_member_role_member)
Admin -> generalGetString(R.string.group_member_role_admin)
Owner -> generalGetString(R.string.group_member_role_owner)
@@ -1218,9 +1281,24 @@ data class ChatItem (
when (content) {
is CIContent.SndDeleted -> true
is CIContent.RcvDeleted -> true
is CIContent.SndModerated -> true
is CIContent.RcvModerated -> true
else -> false
}
fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? {
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
val m = chatInfo.groupInfo.membership
if (m.memberRole >= GroupMemberRole.Admin && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) {
chatInfo.groupInfo to chatDir.groupMember
} else {
null
}
} else {
null
}
}
private val showNtfDir: Boolean get() = !chatDir.sent
val showNotification: Boolean get() =
@@ -1344,7 +1422,7 @@ data class ChatItem (
file = null
)
}
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
const val TEMP_LIVE_CHAT_ITEM_ID = -2L
@@ -1636,18 +1714,31 @@ 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.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> false
is CIFileStatus.RcvTransfer -> false
is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true
}
val cancellable: Boolean = when (fileStatus) {
is CIFileStatus.SndStored -> fileProtocol != FileProtocol.XFTP // TODO true - enable when XFTP send supports cancel
is CIFileStatus.SndTransfer -> fileProtocol != FileProtocol.XFTP // TODO true
is CIFileStatus.SndComplete -> false
is CIFileStatus.SndCancelled -> false
is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> true
is CIFileStatus.RcvTransfer -> true
is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> false
}
companion object {
@@ -1658,21 +1749,27 @@ 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;
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("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()
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@@ -1683,6 +1780,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()
@@ -1736,6 +1834,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")
})
@@ -1759,6 +1862,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)
@@ -1794,6 +1902,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")

View File

@@ -80,7 +80,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {
notifyMessageReceived(
displayNotification(
user = user,
chatId = cInfo.id,
displayName = cInfo.displayName,
@@ -91,7 +91,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
}
fun notifyContactConnected(user: User, contact: Contact) {
notifyMessageReceived(
displayNotification(
user = user,
chatId = contact.id,
displayName = contact.displayName,
@@ -101,11 +101,11 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {
if (!cInfo.ntfsEnabled) return
notifyMessageReceived(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
}
fun notifyMessageReceived(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
if (!user.showNotifications) return
Log.d(TAG, "notifyMessageReceived $chatId")
val now = Clock.System.now().toEpochMilliseconds()
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)

View File

@@ -133,18 +133,24 @@ class AppPreferences(val context: Context) {
val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false)
val connectViaLinkTab = mkStrPreference(SHARED_PREFS_CONNECT_VIA_LINK_TAB, ConnectViaLinkTab.SCAN.name)
val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false)
val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true)
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true)
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb())
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val xftpSendEnabled = mkBoolPreference(SHARED_PREFS_XFTP_SEND_ENABLED, false)
private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference(
get = fun() = sharedPreferences.getInt(prefName, default),
@@ -214,6 +220,7 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
@@ -231,14 +238,18 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_INCOGNITO = "Incognito"
private const val SHARED_PREFS_CONNECT_VIA_LINK_TAB = "ConnectViaLinkTab"
private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown"
private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice"
private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert"
private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase"
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_XFTP_SEND_ENABLED = "XFTPSendEnabled"
}
}
@@ -259,11 +270,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatModel.incognito.value = appPrefs.incognito.get()
}
private fun currentUserId(funcName: String): Long {
val userId = chatModel.currentUser.value?.userId
if (userId == null) {
val error = "$funcName: no current user"
Log.e(TAG, error)
throw Exception(error)
}
return userId
}
suspend fun startChat(user: User) {
Log.d(TAG, "user: $user")
try {
if (chatModel.chatRunning.value == true) return
apiSetNetworkConfig(getNetCfg())
apiSetTempFolder(getTempFilesDirectory(appContext))
apiSetFilesFolder(getAppFilesDirectory(appContext))
apiSetXFTPConfig(getXFTPCfg())
val justStarted = apiStartChat()
val users = listUsers()
chatModel.users.clear()
@@ -271,7 +295,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
if (justStarted) {
chatModel.currentUser.value = user
chatModel.userCreated.value = true
apiSetFilesFolder(getAppFilesDirectory(appContext))
apiSetIncognito(chatModel.incognito.value)
getUserChatData()
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
@@ -290,21 +313,26 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun changeActiveUser(toUserId: Long) {
suspend fun changeActiveUser(toUserId: Long, viewPwd: String?) {
try {
changeActiveUser_(toUserId)
changeActiveUser_(toUserId, viewPwd)
} catch (e: Exception) {
Log.e(TAG, "Unable to set active user: ${e.stackTraceToString()}")
AlertManager.shared.showAlertMsg(generalGetString(R.string.failed_to_active_user_title), e.stackTraceToString())
}
}
suspend fun changeActiveUser_(toUserId: Long) {
chatModel.currentUser.value = apiSetActiveUser(toUserId)
suspend fun changeActiveUser_(toUserId: Long, viewPwd: String?) {
val currentUser = apiSetActiveUser(toUserId, viewPwd)
chatModel.currentUser.value = currentUser
val users = listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
getUserChatData()
val invitation = chatModel.callInvitations.values.firstOrNull { inv -> inv.user.userId == toUserId }
if (invitation != null) {
chatModel.callManager.reportNewIncomingCall(invitation.copy(user = currentUser))
}
}
suspend fun getUserChatData() {
@@ -401,15 +429,33 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
throw Exception("failed to list users ${r.responseType} ${r.details}")
}
suspend fun apiSetActiveUser(userId: Long): User {
val r = sendCmd(CC.ApiSetActiveUser(userId))
suspend fun apiSetActiveUser(userId: Long, viewPwd: String?): User {
val r = sendCmd(CC.ApiSetActiveUser(userId, viewPwd))
if (r is CR.ActiveUser) return r.user
Log.d(TAG, "apiSetActiveUser: ${r.responseType} ${r.details}")
throw Exception("failed to set the user as active ${r.responseType} ${r.details}")
}
suspend fun apiDeleteUser(userId: Long, delSMPQueues: Boolean) {
val r = sendCmd(CC.ApiDeleteUser(userId, delSMPQueues))
suspend fun apiHideUser(userId: Long, viewPwd: String): User =
setUserPrivacy(CC.ApiHideUser(userId, viewPwd))
suspend fun apiUnhideUser(userId: Long, viewPwd: String): User =
setUserPrivacy(CC.ApiUnhideUser(userId, viewPwd))
suspend fun apiMuteUser(userId: Long): User =
setUserPrivacy(CC.ApiMuteUser(userId))
suspend fun apiUnmuteUser(userId: Long): User =
setUserPrivacy(CC.ApiUnmuteUser(userId))
private suspend fun setUserPrivacy(cmd: CC): User {
val r = sendCmd(cmd)
if (r is CR.UserPrivacy) return r.updatedUser
else throw Exception("Failed to change user privacy: ${r.responseType} ${r.details}")
}
suspend fun apiDeleteUser(userId: Long, delSMPQueues: Boolean, viewPwd: String?) {
val r = sendCmd(CC.ApiDeleteUser(userId, delSMPQueues, viewPwd))
if (r is CR.CmdOk) return
Log.d(TAG, "apiDeleteUser: ${r.responseType} ${r.details}")
throw Exception("failed to delete the user ${r.responseType} ${r.details}")
@@ -432,12 +478,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
private suspend fun apiSetTempFolder(tempFolder: String) {
val r = sendCmd(CC.SetTempFolder(tempFolder))
if (r is CR.CmdOk) return
throw Error("failed to set temp folder: ${r.responseType} ${r.details}")
}
private suspend fun apiSetFilesFolder(filesFolder: String) {
val r = sendCmd(CC.SetFilesFolder(filesFolder))
if (r is CR.CmdOk) return
throw Error("failed to set files folder: ${r.responseType} ${r.details}")
}
suspend fun apiSetXFTPConfig(cfg: XFTPFileConfig?) {
val r = sendCmd(CC.ApiSetXFTPConfig(cfg))
if (r is CR.CmdOk) return
throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}")
}
suspend fun apiSetIncognito(incognito: Boolean) {
val r = sendCmd(CC.SetIncognito(incognito))
if (r is CR.CmdOk) return
@@ -470,10 +528,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun apiGetChats(): List<Chat> {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiGetChats: no current user")
return emptyList()
}
val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return emptyList() }
val r = sendCmd(CC.ApiGetChats(userId))
if (r is CR.ApiChats) return r.chats
Log.e(TAG, "failed getting the list of chats: ${r.responseType} ${r.details}")
@@ -517,11 +572,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun apiDeleteMemberChatItem(groupId: Long, groupMemberId: Long, itemId: Long): Pair<ChatItem, ChatItem?>? {
val r = sendCmd(CC.ApiDeleteMemberChatItem(groupId, groupMemberId, itemId))
if (r is CR.ChatItemDeleted) return r.deletedChatItem.chatItem to r.toChatItem?.chatItem
Log.e(TAG, "apiDeleteMemberChatItem bad response: ${r.responseType} ${r.details}")
return null
}
private suspend fun getUserSMPServers(): Pair<List<ServerCfg>, List<String>>? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "getUserSMPServers: no current user")
return null
}
val userId = kotlin.runCatching { currentUserId("getUserSMPServers") }.getOrElse { return null }
val r = sendCmd(CC.APIGetUserSMPServers(userId))
if (r is CR.UserSMPServers) return r.smpServers to r.presetSMPServers
Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}")
@@ -529,10 +588,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun setUserSMPServers(smpServers: List<ServerCfg>): Boolean {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "setUserSMPServers: no current user")
return false
}
val userId = kotlin.runCatching { currentUserId("setUserSMPServers") }.getOrElse { return false }
val r = sendCmd(CC.APISetUserSMPServers(userId, smpServers))
return when (r) {
is CR.CmdOk -> true
@@ -548,8 +604,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun testSMPServer(smpServer: String): SMPTestFailure? {
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("testSMPServer: no current user") }
val r = sendCmd(CC.TestSMPServer(userId, smpServer))
val userId = currentUserId("testSMPServer")
val r = sendCmd(CC.APITestSMPServer(userId, smpServer))
return when (r) {
is CR.SmpTestResult -> r.smpTestFailure
else -> {
@@ -560,14 +616,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun getChatItemTTL(): ChatItemTTL {
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("getChatItemTTL: no current user") }
val userId = currentUserId("getChatItemTTL")
val r = sendCmd(CC.APIGetChatItemTTL(userId))
if (r is CR.ChatItemTTL) return ChatItemTTL.fromSeconds(r.chatItemTTL)
throw Exception("failed to get chat item TTL: ${r.responseType} ${r.details}")
}
suspend fun setChatItemTTL(chatItemTTL: ChatItemTTL) {
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("setChatItemTTL: no current user") }
val userId = currentUserId("setChatItemTTL")
val r = sendCmd(CC.APISetChatItemTTL(userId, chatItemTTL.seconds))
if (r is CR.CmdOk) return
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
@@ -751,10 +807,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun apiListContacts(): List<Contact>? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiListContacts: no current user")
return null
}
val userId = kotlin.runCatching { currentUserId("apiListContacts") }.getOrElse { return null }
val r = sendCmd(CC.ApiListContacts(userId))
if (r is CR.ContactsList) return r.contacts
Log.e(TAG, "apiListContacts bad response: ${r.responseType} ${r.details}")
@@ -762,10 +815,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun apiUpdateProfile(profile: Profile): Profile? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiUpdateProfile: no current user")
return null
}
val userId = kotlin.runCatching { currentUserId("apiUpdateProfile") }.getOrElse { return null }
val r = sendCmd(CC.ApiUpdateProfile(userId, profile))
if (r is CR.UserProfileNoChange) return profile
if (r is CR.UserProfileUpdated) return r.toProfile
@@ -795,10 +845,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun apiCreateUserAddress(): String? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiCreateUserAddress: no current user")
return null
}
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
val r = sendCmd(CC.ApiCreateMyAddress(userId))
return when (r) {
is CR.UserContactLinkCreated -> r.connReqContact
@@ -812,10 +859,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun apiDeleteUserAddress(): Boolean {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiDeleteUserAddress: no current user")
return false
}
val userId = kotlin.runCatching { currentUserId("apiDeleteUserAddress") }.getOrElse { return false }
val r = sendCmd(CC.ApiDeleteMyAddress(userId))
if (r is CR.UserContactLinkDeleted) return true
Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}")
@@ -823,10 +867,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
private suspend fun apiGetUserAddress(): UserContactLinkRec? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiGetUserAddress: no current user")
return null
}
val userId = kotlin.runCatching { currentUserId("apiGetUserAddress") }.getOrElse { return null }
val r = sendCmd(CC.ApiShowMyAddress(userId))
if (r is CR.UserContactLink) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
@@ -838,10 +879,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
suspend fun userAddressAutoAccept(autoAccept: AutoAccept?): UserContactLinkRec? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "userAddressAutoAccept: no current user")
return null
}
val userId = kotlin.runCatching { currentUserId("userAddressAutoAccept") }.getOrElse { return null }
val r = sendCmd(CC.ApiAddressAutoAccept(userId, autoAccept))
if (r is CR.UserContactLinkUpdated) return r.contactLink
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
@@ -960,11 +998,27 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiNewGroup(p: GroupProfile): GroupInfo? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiNewGroup: no current user")
return null
suspend fun cancelFile(user: User, fileId: Long) {
val chatItem = apiCancelFile(fileId)
if (chatItem != null) {
chatItemSimpleUpdate(user, chatItem)
}
}
suspend fun apiCancelFile(fileId: Long): AChatItem? {
val r = sendCmd(CC.CancelFile(fileId))
return when (r) {
is CR.SndFileCancelled -> r.chatItem
is CR.RcvFileCancelled -> r.chatItem
else -> {
Log.d(TAG, "apiCancelFile bad response: ${r.responseType} ${r.details}")
null
}
}
}
suspend fun apiNewGroup(p: GroupProfile): GroupInfo? {
val userId = kotlin.runCatching { currentUserId("apiNewGroup") }.getOrElse { return null }
val r = sendCmd(CC.ApiNewGroup(userId, p))
if (r is CR.GroupCreated) return r.groupInfo
Log.e(TAG, "apiNewGroup bad response: ${r.responseType} ${r.details}")
@@ -1060,9 +1114,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiCreateGroupLink(groupId: Long): String? {
return when (val r = sendCmd(CC.APICreateGroupLink(groupId))) {
is CR.GroupLinkCreated -> r.connReqContact
suspend fun apiCreateGroupLink(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? {
return when (val r = sendCmd(CC.APICreateGroupLink(groupId, memberRole))) {
is CR.GroupLinkCreated -> r.connReqContact to r.memberRole
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r)
@@ -1072,6 +1126,18 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiGroupLinkMemberRole(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? {
return when (val r = sendCmd(CC.APIGroupLinkMemberRole(groupId, memberRole))) {
is CR.GroupLink -> r.connReqContact to r.memberRole
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiGroupLinkMemberRole", generalGetString(R.string.error_updating_link_for_group), r)
}
null
}
}
}
suspend fun apiDeleteGroupLink(groupId: Long): Boolean {
return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) {
is CR.GroupLinkDeleted -> true
@@ -1084,9 +1150,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiGetGroupLink(groupId: Long): String? {
suspend fun apiGetGroupLink(groupId: Long): Pair<String, GroupMemberRole>? {
return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) {
is CR.GroupLink -> r.connReqContact
is CR.GroupLink -> r.connReqContact to r.memberRole
else -> {
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
null
@@ -1185,7 +1251,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
val contactRequest = r.contactRequest
val cInfo = ChatInfo.ContactRequest(contactRequest)
if (active(r.user)) {
chatModel.addChat(Chat(chatInfo = cInfo, chatItems = listOf()))
if (chatModel.hasChat(contactRequest.id)) {
chatModel.updateChatInfo(cInfo)
} else {
chatModel.addChat(Chat(chatInfo = cInfo, chatItems = listOf()))
}
}
ntfManager.notifyContactRequestReceived(r.user, cInfo)
}
@@ -1271,7 +1341,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
ntfManager.cancelNotificationsForChat(cInfo.id)
ntfManager.notifyMessageReceived(
ntfManager.displayNotification(
r.user,
cInfo.id,
cInfo.displayName,
@@ -1347,6 +1417,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileSndCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileComplete -> {
@@ -1362,8 +1436,25 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
removeFile(appContext, fileName)
}
}
is CR.CallInvitation ->
is CR.SndFileRcvCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileCompleteXFTP -> {
chatItemSimpleUpdate(r.user, r.chatItem)
val cItem = r.chatItem.chatItem
val mc = cItem.content.msgContent
val fileName = cItem.file?.fileName
if (
mc is MsgContent.MCFile
&& fileName != null
) {
removeFile(appContext, fileName)
}
}
is CR.CallInvitation -> {
chatModel.callManager.reportNewIncomingCall(r.callInvitation)
}
is CR.CallOffer -> {
// TODO askConfirmation?
// TODO check encryption is compatible
@@ -1658,6 +1749,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
fun getXFTPCfg(): XFTPFileConfig? {
val prefXFTPSendEnabled = appPrefs.xftpSendEnabled.get()
return if (prefXFTPSendEnabled) XFTPFileConfig(minFileSize = 0) else null
}
fun getNetCfg(): NetCfg {
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
val socksProxy = if (useSocksProxy) ":9050" else null
@@ -1729,11 +1825,17 @@ sealed class CC {
class ShowActiveUser: CC()
class CreateActiveUser(val profile: Profile): CC()
class ListUsers: CC()
class ApiSetActiveUser(val userId: Long): CC()
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean): CC()
class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC()
class ApiHideUser(val userId: Long, val viewPwd: String): CC()
class ApiUnhideUser(val userId: Long, val viewPwd: String): CC()
class ApiMuteUser(val userId: Long): CC()
class ApiUnmuteUser(val userId: Long): CC()
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
class StartChat(val expire: Boolean): CC()
class ApiStopChat: CC()
class SetTempFolder(val tempFolder: String): CC()
class SetFilesFolder(val filesFolder: String): CC()
class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC()
class SetIncognito(val incognito: Boolean): CC()
class ApiExportArchive(val config: ArchiveConfig): CC()
class ApiImportArchive(val config: ArchiveConfig): CC()
@@ -1744,6 +1846,7 @@ sealed class CC {
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
class ApiNewGroup(val userId: Long, val groupProfile: GroupProfile): CC()
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC()
@@ -1752,12 +1855,13 @@ sealed class CC {
class ApiLeaveGroup(val groupId: Long): CC()
class ApiListMembers(val groupId: Long): CC()
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
class APICreateGroupLink(val groupId: Long): CC()
class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC()
class APIGetUserSMPServers(val userId: Long): CC()
class APISetUserSMPServers(val userId: Long, val smpServers: List<ServerCfg>): CC()
class TestSMPServer(val userId: Long, val smpServer: String): CC()
class APITestSMPServer(val userId: Long, val smpServer: String): CC()
class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC()
class APIGetChatItemTTL(val userId: Long): CC()
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
@@ -1797,6 +1901,7 @@ sealed class CC {
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
class ReceiveFile(val fileId: Long, val inline: Boolean?): CC()
class CancelFile(val fileId: Long): CC()
class ShowVersion(): CC()
val cmdString: String get() = when (this) {
@@ -1804,11 +1909,17 @@ sealed class CC {
is ShowActiveUser -> "/u"
is CreateActiveUser -> "/create user ${profile.displayName} ${profile.fullName}"
is ListUsers -> "/users"
is ApiSetActiveUser -> "/_user $userId"
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}"
is ApiSetActiveUser -> "/_user $userId${maybePwd(viewPwd)}"
is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}"
is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}"
is ApiMuteUser -> "/_mute user $userId"
is ApiUnmuteUser -> "/_unmute user $userId"
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
is StartChat -> "/_start subscribe=on expire=${onOff(expire)}"
is ApiStopChat -> "/_stop"
is SetTempFolder -> "/_temp_folder $tempFolder"
is SetFilesFolder -> "/_files_folder $filesFolder"
is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off"
is SetIncognito -> "/incognito ${onOff(incognito)}"
is ApiExportArchive -> "/_db export ${json.encodeToString(config)}"
is ApiImportArchive -> "/_db import ${json.encodeToString(config)}"
@@ -1819,6 +1930,7 @@ sealed class CC {
is ApiSendMessage -> "/_send ${chatRef(type, id)} live=${onOff(live)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
is ApiNewGroup -> "/_group $userId ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId"
@@ -1827,12 +1939,13 @@ sealed class CC {
is ApiLeaveGroup -> "/_leave #$groupId"
is ApiListMembers -> "/_members #$groupId"
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}"
is APICreateGroupLink -> "/_create link #$groupId"
is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()}"
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId"
is APIGetUserSMPServers -> "/_smp $userId"
is APISetUserSMPServers -> "/_smp $userId ${smpServersStr(smpServers)}"
is TestSMPServer -> "/smp test $userId $smpServer"
is APITestSMPServer -> "/_smp test $userId $smpServer"
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
is APIGetChatItemTTL -> "/_ttl $userId"
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
@@ -1872,6 +1985,7 @@ sealed class CC {
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
is ReceiveFile -> if (inline == null) "/freceive $fileId" else "/freceive $fileId inline=${onOff(inline)}"
is CancelFile -> "/fcancel $fileId"
is ShowVersion -> "/version"
}
@@ -1881,10 +1995,16 @@ sealed class CC {
is CreateActiveUser -> "createActiveUser"
is ListUsers -> "listUsers"
is ApiSetActiveUser -> "apiSetActiveUser"
is ApiHideUser -> "apiHideUser"
is ApiUnhideUser -> "apiUnhideUser"
is ApiMuteUser -> "apiMuteUser"
is ApiUnmuteUser -> "apiUnmuteUser"
is ApiDeleteUser -> "apiDeleteUser"
is StartChat -> "startChat"
is ApiStopChat -> "apiStopChat"
is SetTempFolder -> "setTempFolder"
is SetFilesFolder -> "setFilesFolder"
is ApiSetXFTPConfig -> "apiSetXFTPConfig"
is SetIncognito -> "setIncognito"
is ApiExportArchive -> "apiExportArchive"
is ApiImportArchive -> "apiImportArchive"
@@ -1895,6 +2015,7 @@ sealed class CC {
is ApiSendMessage -> "apiSendMessage"
is ApiUpdateChatItem -> "apiUpdateChatItem"
is ApiDeleteChatItem -> "apiDeleteChatItem"
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
is ApiNewGroup -> "apiNewGroup"
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
@@ -1904,11 +2025,12 @@ sealed class CC {
is ApiListMembers -> "apiListMembers"
is ApiUpdateGroupProfile -> "apiUpdateGroupProfile"
is APICreateGroupLink -> "apiCreateGroupLink"
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
is APIDeleteGroupLink -> "apiDeleteGroupLink"
is APIGetGroupLink -> "apiGetGroupLink"
is APIGetUserSMPServers -> "apiGetUserSMPServers"
is APISetUserSMPServers -> "apiSetUserSMPServers"
is TestSMPServer -> "testSMPServer"
is APITestSMPServer -> "testSMPServer"
is APISetChatItemTTL -> "apiSetChatItemTTL"
is APIGetChatItemTTL -> "apiGetChatItemTTL"
is APISetNetworkConfig -> "/apiSetNetworkConfig"
@@ -1948,6 +2070,7 @@ sealed class CC {
is ApiChatRead -> "apiChatRead"
is ApiChatUnread -> "apiChatUnread"
is ReceiveFile -> "receiveFile"
is CancelFile -> "cancelFile"
is ShowVersion -> "showVersion"
}
@@ -1961,13 +2084,26 @@ sealed class CC {
val obfuscated: CC
get() = when (this) {
is ApiStorageEncryption -> ApiStorageEncryption(DBEncryptionConfig(obfuscate(config.currentKey), obfuscate(config.newKey)))
is ApiSetActiveUser -> ApiSetActiveUser(userId, obfuscateOrNull(viewPwd))
is ApiHideUser -> ApiHideUser(userId, obfuscate(viewPwd))
is ApiUnhideUser -> ApiUnhideUser(userId, obfuscate(viewPwd))
is ApiDeleteUser -> ApiDeleteUser(userId, delSMPQueues, obfuscateOrNull(viewPwd))
else -> this
}
private fun obfuscate(s: String): String = if (s.isEmpty()) "" else "***"
private fun obfuscateOrNull(s: String?): String? =
if (s != null) {
obfuscate(s)
} else {
null
}
private fun onOff(b: Boolean): String = if (b) "on" else "off"
private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd)
companion object {
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
@@ -1996,6 +2132,9 @@ sealed class ChatPagination {
@Serializable
class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgContent: MsgContent)
@Serializable
class XFTPFileConfig(val minFileSize: Long)
@Serializable
class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? = null, val parentTempDirectory: String? = null)
@@ -2820,6 +2959,13 @@ class APIResponse(val resp: CR, val corr: String? = null) {
resp = CR.ApiChat(user, chat),
corr = data["corr"]?.toString()
)
} else if (type == "chatCmdError") {
val userObject = resp["user_"]?.jsonObject
val user = runCatching<User?> { json.decodeFromJsonElement(userObject!!) }.getOrNull()
return APIResponse(
resp = CR.ChatCmdError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))),
corr = data["corr"]?.toString()
)
}
} catch (e: Exception) {
Log.e(TAG, "Error while parsing chat(s): " + e.stackTraceToString())
@@ -2876,6 +3022,7 @@ sealed class CR {
@Serializable @SerialName("chatCleared") class ChatCleared(val user: User, val chatInfo: ChatInfo): CR()
@Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR()
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile): CR()
@Serializable @SerialName("userPrivacy") class UserPrivacy(val user: User, val updatedUser: User): CR()
@Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val user: User, val toContact: Contact): CR()
@Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val user: User, val toConnection: PendingContactConnection): CR()
@Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val user: User, val fromContact: Contact, val toContact: Contact): CR()
@@ -2925,20 +3072,24 @@ sealed class CR {
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR()
@Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: User, val groupInfo: GroupInfo): CR()
// receiving file events
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: User, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: User, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR()
// sending file events
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndGroupFileCancelled") class SndGroupFileCancelled(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR()
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
@Serializable @SerialName("callOffer") class CallOffer(val user: User, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
@Serializable @SerialName("callAnswer") class CallAnswer(val user: User, val contact: Contact, val answer: WebRTCSession): CR()
@@ -2946,11 +3097,11 @@ sealed class CR {
@Serializable @SerialName("callEnded") class CallEnded(val user: User, val contact: Contact): CR()
@Serializable @SerialName("newContactConnection") class NewContactConnection(val user: User, val connection: PendingContactConnection): CR()
@Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: User, val connection: PendingContactConnection): CR()
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo): CR()
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR()
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
@Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR()
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user: User?, val chatError: ChatError): CR()
@Serializable @SerialName("chatError") class ChatRespError(val user: User?, val chatError: ChatError): CR()
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR()
@Serializable @SerialName("chatError") class ChatRespError(val user_: User?, val chatError: ChatError): CR()
@Serializable class Response(val type: String, val json: String): CR()
@Serializable class Invalid(val str: String): CR()
@@ -2979,6 +3130,7 @@ sealed class CR {
is ChatCleared -> "chatCleared"
is UserProfileNoChange -> "userProfileNoChange"
is UserProfileUpdated -> "userProfileUpdated"
is UserPrivacy -> "userPrivacy"
is ContactAliasUpdated -> "contactAliasUpdated"
is ConnectionAliasUpdated -> "connectionAliasUpdated"
is ContactPrefsUpdated -> "contactPrefsUpdated"
@@ -3034,11 +3186,15 @@ sealed class CR {
is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart"
is RcvFileComplete -> "rcvFileComplete"
is RcvFileCancelled -> "rcvFileCancelled"
is RcvFileSndCancelled -> "rcvFileSndCancelled"
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
is SndFileCancelled -> "sndFileCancelled"
is SndFileComplete -> "sndFileComplete"
is SndFileRcvCancelled -> "sndFileRcvCancelled"
is SndFileStart -> "sndFileStart"
is SndGroupFileCancelled -> "sndGroupFileCancelled"
is SndFileProgressXFTP -> "sndFileProgressXFTP"
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer"
is CallAnswer -> "callAnswer"
@@ -3080,6 +3236,7 @@ sealed class CR {
is ChatCleared -> withUser(user, json.encodeToString(chatInfo))
is UserProfileNoChange -> withUser(user, noDetails())
is UserProfileUpdated -> withUser(user, json.encodeToString(toProfile))
is UserPrivacy -> withUser(user, json.encodeToString(updatedUser))
is ContactAliasUpdated -> withUser(user, json.encodeToString(toContact))
is ConnectionAliasUpdated -> withUser(user, json.encodeToString(toConnection))
is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}")
@@ -3129,18 +3286,22 @@ sealed class CR {
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is GroupRemoved -> withUser(user, json.encodeToString(groupInfo))
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact")
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact")
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
is RcvFileComplete -> withUser(user, json.encodeToString(chatItem))
is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem))
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
is SndFileCancelled -> json.encodeToString(chatItem)
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem))
is SndFileStart -> withUser(user, json.encodeToString(chatItem))
is SndGroupFileCancelled -> withUser(user, json.encodeToString(chatItem))
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
@@ -3148,10 +3309,12 @@ sealed class CR {
is CallEnded -> withUser(user, "contact: ${contact.id}")
is NewContactConnection -> withUser(user, json.encodeToString(connection))
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
is VersionInfo -> json.encodeToString(versionInfo)
is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" +
"chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" +
"agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}"
is CmdOk -> withUser(user, noDetails())
is ChatCmdError -> withUser(user, chatError.string)
is ChatRespError -> withUser(user, chatError.string)
is ChatCmdError -> withUser(user_, chatError.string)
is ChatRespError -> withUser(user_, chatError.string)
is Response -> json
is Invalid -> str
}
@@ -3223,11 +3386,13 @@ sealed class ChatError {
is ChatErrorAgent -> "agent ${agentError.string}"
is ChatErrorStore -> "store ${storeError.string}"
is ChatErrorDatabase -> "database ${databaseError.string}"
is ChatErrorInvalidJSON -> "invalid json ${json}"
}
@Serializable @SerialName("error") class ChatErrorChat(val errorType: ChatErrorType): ChatError()
@Serializable @SerialName("errorAgent") class ChatErrorAgent(val agentError: AgentErrorType): ChatError()
@Serializable @SerialName("errorStore") class ChatErrorStore(val storeError: StoreError): ChatError()
@Serializable @SerialName("errorDatabase") class ChatErrorDatabase(val databaseError: DatabaseError): ChatError()
@Serializable @SerialName("invalidJSON") class ChatErrorInvalidJSON(val json: String): ChatError()
}
@Serializable

View File

@@ -17,6 +17,7 @@ enum class DefaultTheme {
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 DarkColorPalette = darkColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files

View File

@@ -83,6 +83,8 @@ fun TerminalLayout(
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = sendCommand,
sendLiveMessage = null,

View File

@@ -13,12 +13,14 @@ class CallManager(val chatModel: ChatModel) {
Log.d(TAG, "CallManager.reportNewIncomingCall")
with (chatModel) {
callInvitations[invitation.contact.id] = invitation
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
activeCallInvitation.value = invitation
controller.ntfManager.notifyCallInvitation(invitation)
} else {
val contact = invitation.contact
controller.ntfManager.notifyMessageReceived(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
if (invitation.user.showNotifications) {
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
activeCallInvitation.value = invitation
controller.ntfManager.notifyCallInvitation(invitation)
} else {
val contact = invitation.contact
controller.ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
}
}
}
}

View File

@@ -3,11 +3,12 @@ package chat.simplex.app.views.call
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.*
import android.content.pm.ActivityInfo
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.media.*
import android.os.Build
import android.os.PowerManager
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
import android.util.Log
import android.view.ViewGroup
import android.webkit.*
@@ -19,6 +20,7 @@ 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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -37,7 +39,7 @@ import androidx.webkit.WebViewClientCompat
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.usersettings.NotificationsMode
@@ -53,6 +55,7 @@ fun ActiveCallView(chatModel: ChatModel) {
val call = chatModel.activeCall.value
if (call != null) withApi { chatModel.callManager.endCall(call) }
})
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name }
LaunchedEffect(Unit) {
// Start service when call happening since it's not already started.
@@ -60,17 +63,48 @@ fun ActiveCallView(chatModel: ChatModel) {
if (!ntfModeService) SimplexService.start(SimplexApp.context)
}
DisposableEffect(Unit) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
var btDeviceCount = 0
val audioCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
super.onAudioDevicesAdded(addedDevices)
val addedCount = addedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
btDeviceCount += addedCount
audioViaBluetooth.value = btDeviceCount > 0
if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
}
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
super.onAudioDevicesRemoved(removedDevices)
val removedCount = removedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
btDeviceCount -= removedCount
audioViaBluetooth.value = btDeviceCount > 0
if (btDeviceCount == 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
}
}
}
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, SimplexApp.context.packageName + ":proximityLock")
} else {
null
}
proximityLock?.acquire()
onDispose {
// Stop it when call ended
if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context)
// Clear selected communication device to default value after we changed it in call
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am.clearCommunicationDevice()
}
dropAudioManagerOverrides()
am.unregisterAudioDeviceCallback(audioCallback)
proximityLock?.release()
}
}
val cxt = LocalContext.current
val scope = rememberCoroutineScope()
Box(Modifier.fillMaxSize()) {
WebRTCView(chatModel.callCommand) { apiMsg ->
@@ -100,6 +134,7 @@ fun ActiveCallView(chatModel: ChatModel) {
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
if (callStatus == WebRTCCallStatus.Connected) {
chatModel.activeCall.value = call.copy(callState = CallState.Connected)
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
withApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
} catch (e: Error) {
@@ -108,8 +143,7 @@ fun ActiveCallView(chatModel: ChatModel) {
is WCallResponse.Connected -> {
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
scope.launch {
delay(2000L)
setCallSound(cxt, call)
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
}
is WCallResponse.Ended -> {
@@ -143,15 +177,18 @@ fun ActiveCallView(chatModel: ChatModel) {
}
}
val call = chatModel.activeCall.value
if (call != null) ActiveCallOverlay(call, chatModel)
if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth)
}
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
val prevVolumeControlStream = activity.volumeControlStream
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
// Lock orientation to portrait in order to have good experience with calls
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
onDispose {
activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
@@ -159,10 +196,10 @@ fun ActiveCallView(chatModel: ChatModel) {
}
@Composable
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) {
var cxt = LocalContext.current
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetooth: MutableState<Boolean>) {
ActiveCallOverlayLayout(
call = call,
speakerCanBeEnabled = !audioViaBluetooth.value,
dismiss = { withApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) },
toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) },
@@ -171,38 +208,55 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) {
if (call != null) {
call = call.copy(soundSpeaker = !call.soundSpeaker)
chatModel.activeCall.value = call
setCallSound(cxt, call)
setCallSound(call.soundSpeaker, audioViaBluetooth)
}
},
flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) }
)
}
private fun setCallSound(cxt: Context, call: Call) {
Log.d(TAG, "setCallSound: set audio mode")
val am = cxt.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (call.soundSpeaker) {
am.mode = AudioManager.MODE_NORMAL
am.isSpeakerphoneOn = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
am.availableCommunicationDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }?.let {
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
am.mode = AudioManager.MODE_IN_COMMUNICATION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val btDevice = am.availableCommunicationDevices.lastOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
if (btDevice != null) {
am.setCommunicationDevice(btDevice)
} else if (am.communicationDevice?.type != preferredSecondaryDevice) {
am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let {
am.setCommunicationDevice(it)
}
}
} else {
am.mode = AudioManager.MODE_IN_CALL
am.isSpeakerphoneOn = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
am.availableCommunicationDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }?.let {
am.setCommunicationDevice(it)
}
if (audioViaBluetooth.value) {
am.isSpeakerphoneOn = false
am.startBluetoothSco()
} else {
am.stopBluetoothSco()
am.isSpeakerphoneOn = speaker
}
am.isBluetoothScoOn = am.isBluetoothScoAvailableOffCall && audioViaBluetooth.value
}
}
private fun dropAudioManagerOverrides() {
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
am.mode = AudioManager.MODE_NORMAL
// Clear selected communication device to default value after we changed it in call
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
am.clearCommunicationDevice()
} else {
am.isSpeakerphoneOn = false
am.stopBluetoothSco()
}
}
@Composable
private fun ActiveCallOverlayLayout(
call: Call,
speakerCanBeEnabled: Boolean,
dismiss: () -> Unit,
toggleAudio: () -> Unit,
toggleVideo: () -> Unit,
@@ -240,7 +294,7 @@ private fun ActiveCallOverlayLayout(
CallInfoView(call, alignment = Alignment.CenterHorizontally)
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = 48.dp), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss) {
Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
@@ -251,7 +305,7 @@ private fun ActiveCallOverlayLayout(
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.padding(end = 32.dp)) {
ToggleSoundButton(call, toggleSound)
ToggleSoundButton(call, speakerCanBeEnabled, toggleSound)
}
}
}
@@ -261,10 +315,10 @@ private fun ActiveCallOverlayLayout(
}
@Composable
private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: Int, action: () -> Unit) {
private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: Int, action: () -> Unit, enabled: Boolean = true) {
if (call.hasMedia) {
IconButton(onClick = action) {
Icon(icon, stringResource(iconText), tint = Color(0xFFFFFFD8), modifier = Modifier.size(40.dp))
IconButton(onClick = action, enabled = enabled) {
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else HighOrLowlight, modifier = Modifier.size(40.dp))
}
} else {
Spacer(Modifier.size(40.dp))
@@ -281,11 +335,11 @@ private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
}
@Composable
private fun ToggleSoundButton(call: Call, toggleSound: () -> Unit) {
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
if (call.soundSpeaker) {
ControlButton(call, Icons.Outlined.VolumeUp, R.string.icon_descr_speaker_off, toggleSound)
ControlButton(call, Icons.Outlined.VolumeUp, R.string.icon_descr_speaker_off, toggleSound, enabled)
} else {
ControlButton(call, Icons.Outlined.VolumeDown, R.string.icon_descr_speaker_on, toggleSound)
ControlButton(call, Icons.Outlined.VolumeDown, R.string.icon_descr_speaker_on, toggleSound, enabled)
}
}
@@ -485,6 +539,7 @@ fun PreviewActiveCallOverlayVideo() {
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
)
),
speakerCanBeEnabled = true,
dismiss = {},
toggleAudio = {},
toggleVideo = {},
@@ -509,6 +564,7 @@ fun PreviewActiveCallOverlayAudio() {
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
)
),
speakerCanBeEnabled = true,
dismiss = {},
toggleAudio = {},
toggleVideo = {},

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.chat
import android.app.Activity
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
@@ -133,6 +134,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
searchText,
useLinkPreviews = useLinkPreviews,
linkMode = chatModel.simplexLinkMode.value,
allowVideoAttachment = chatModel.controller.appPrefs.xftpSendEnabled.get(),
chatModelIncognito = chatModel.incognito.value,
back = {
hideKeyboard(view)
@@ -152,9 +154,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
}
} else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
var groupLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
var groupLink = link?.first
var groupLinkMemberRole = link?.second
ModalManager.shared.showModalCloseable(true) { close ->
GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close)
GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, {
groupLink = it.first;
groupLinkMemberRole = it.second
}, close)
}
}
}
@@ -193,27 +200,42 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
deleteMessage = { itemId, mode ->
withApi {
val cInfo = chat.chatInfo
val r = chatModel.controller.apiDeleteChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = itemId,
mode = mode
)
if (r != null) {
val toChatItem = r.toChatItem
if (toChatItem == null) {
chatModel.removeChatItem(cInfo, r.deletedChatItem.chatItem)
} else {
chatModel.upsertChatItem(cInfo, toChatItem.chatItem)
}
val toDeleteItem = chatModel.chatItems.firstOrNull { it.id == itemId }
val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo)
val groupInfo = toModerate?.first
val groupMember = toModerate?.second
val deletedChatItem: ChatItem?
val toChatItem: ChatItem?
if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
val r = chatModel.controller.apiDeleteMemberChatItem(
groupId = groupInfo.groupId,
groupMemberId = groupMember.groupMemberId,
itemId = itemId
)
deletedChatItem = r?.first
toChatItem = r?.second
} else {
val r = chatModel.controller.apiDeleteChatItem(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = itemId,
mode = mode
)
deletedChatItem = r?.deletedChatItem?.chatItem
toChatItem = r?.toChatItem?.chatItem
}
if (toChatItem == null && deletedChatItem != null) {
chatModel.removeChatItem(cInfo, deletedChatItem)
} else if (toChatItem != null) {
chatModel.upsertChatItem(cInfo, toChatItem)
}
}
},
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) }
@@ -286,6 +308,7 @@ fun ChatLayout(
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
allowVideoAttachment: Boolean,
chatModelIncognito: Boolean,
back: () -> Unit,
info: () -> Unit,
@@ -293,6 +316,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,
@@ -316,6 +340,7 @@ fun ChatLayout(
sheetContent = {
ChooseAttachmentView(
attachmentOption,
allowVideoAttachment,
hide = { scope.launch { attachmentBottomSheetState.hide() } }
)
},
@@ -337,7 +362,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,
)
}
}
@@ -510,6 +535,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,
@@ -556,6 +582,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(
@@ -575,10 +606,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))
}
}
}
}
@@ -618,11 +651,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
@@ -633,7 +666,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)
}
}
@@ -913,21 +946,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) {
@@ -941,16 +979,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) {
@@ -962,7 +1012,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) {
@@ -1033,6 +1083,7 @@ fun PreviewChatLayout() {
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
allowVideoAttachment = true,
chatModelIncognito = false,
back = {},
info = {},
@@ -1040,6 +1091,7 @@ fun PreviewChatLayout() {
loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
startCall = {},
acceptCall = { _ -> },
@@ -1092,6 +1144,7 @@ fun PreviewGroupChatLayout() {
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
allowVideoAttachment = true,
chatModelIncognito = false,
back = {},
info = {},
@@ -1099,6 +1152,7 @@ fun PreviewGroupChatLayout() {
loadPrevMessages = { _ -> },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
startCall = {},
acceptCall = { _ -> },

View File

@@ -7,13 +7,12 @@ 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.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract
@@ -51,6 +50,7 @@ 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 VideoPreview(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()
}
@@ -97,6 +97,7 @@ data class ComposeState(
get() = {
val hasContent = when (preview) {
is ComposePreview.ImagePreview -> true
is ComposePreview.VideoPreview -> true
is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true
else -> message.isNotEmpty() || liveMessage != null
@@ -110,6 +111,7 @@ data class ComposeState(
get() =
when (preview) {
is ComposePreview.ImagePreview -> false
is ComposePreview.VideoPreview -> false
is ComposePreview.VoicePreview -> false
is ComposePreview.FilePreview -> false
else -> useLinkPreviews
@@ -160,6 +162,7 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview {
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.MCVideo -> ComposePreview.VideoPreview(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
@@ -180,14 +183,17 @@ fun ComposeView(
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val xftpSendEnabled = chatModel.controller.appPrefs.xftpSendEnabled.get()
val maxFileSize = getMaxFileSize(fileProtocol = if (xftpSendEnabled) FileProtocol.XFTP else FileProtocol.SMP)
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.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
}
}
}
val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
@@ -201,28 +207,21 @@ fun ComposeView(
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) {
val drawable = getDrawableFromUri(uri)
var bitmap: Bitmap? = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
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(MAX_FILE_SIZE))
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
} else {
@@ -237,10 +236,25 @@ fun ComposeView(
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview, content))
}
}
val processPickedVideo = { uris: List<Uri>, text: String? ->
val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
val (bitmap: Bitmap?, durationMs: Long?) = getBitmapFromVideo(uri)
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
if (bitmap != null) {
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
}
}
if (imagesPreview.isNotEmpty()) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.VideoPreview(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))
@@ -248,13 +262,15 @@ 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()) { processPickedImage(it, null) }
val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedVideo(it, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedVideo(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
@@ -273,9 +289,17 @@ fun ComposeView(
}
AttachmentOption.PickImage -> {
try {
galleryLauncher.launch(0)
galleryImageLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
galleryLauncherFallback.launch("image/*")
galleryImageLauncherFallback.launch("image/*")
}
attachmentOption.value = null
}
AttachmentOption.PickVideo -> {
try {
galleryVideoLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
galleryVideoLauncherFallback.launch("video/*")
}
attachmentOption.value = null
}
@@ -396,6 +420,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)
@@ -440,6 +465,7 @@ fun ComposeView(
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
else -> return@forEachIndexed
}
if (file != null) {
files.add(file)
@@ -447,6 +473,18 @@ fun ComposeView(
}
}
}
is ComposePreview.VideoPreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.Video -> saveFileFromUri(context, it.uri)
else -> return@forEachIndexed
}
if (file != null) {
files.add(file)
msgs.add(MsgContent.MCVideo(if (preview.content.lastIndex == index) msgText else "", preview.images[index], it.duration))
}
}
}
is ComposePreview.VoicePreview -> {
val tmpFile = File(preview.voice)
AudioPlayer.stop(tmpFile.absolutePath)
@@ -477,7 +515,12 @@ 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.ImagePreview ||
cs.preview is ComposePreview.VideoPreview ||
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
}
}
@@ -604,6 +647,11 @@ fun ComposeView(
::cancelImages,
cancelEnabled = !composeState.value.editing
)
is ComposePreview.VideoPreview -> ComposeImageView(
preview.images,
::cancelImages,
cancelEnabled = !composeState.value.editing
)
is ComposePreview.VoicePreview -> ComposeVoiceView(
preview.voice,
preview.durationMs,
@@ -645,6 +693,9 @@ fun ComposeView(
chatModel.sharedContent.value = null
}
val userCanSend = rememberUpdatedState(chat.userCanSend)
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
Column {
contextItemView()
when {
@@ -656,11 +707,11 @@ fun ComposeView(
modifier = Modifier.padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) {
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) {
Icon(
Icons.Filled.AttachFile,
contentDescription = stringResource(R.string.attach),
tint = if (!composeState.value.attachmentDisabled) MaterialTheme.colors.primary else HighOrLowlight,
tint = if (!composeState.value.attachmentDisabled && userCanSend.value) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
@@ -698,6 +749,13 @@ fun ComposeView(
}
}
LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) {
if (!chat.userCanSend) {
clearCurrentDraft()
clearState()
}
}
val activity = LocalContext.current as Activity
DisposableEffect(Unit) {
val orientation = activity.resources.configuration.orientation
@@ -733,6 +791,8 @@ fun ComposeView(
needToAllowVoiceToContact,
allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
userIsObserver = userIsObserver.value,
userCanSend = userCanSend.value,
sendMessage = {
sendMessage()
resetLinkPreview()
@@ -759,7 +819,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)
@@ -784,3 +844,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

@@ -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(
@@ -60,6 +63,8 @@ fun SendMsgView(
liveMessageAlertShown: SharedPreference<Boolean>,
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
userIsObserver: Boolean,
userCanSend: Boolean,
allowVoiceToContact: () -> Unit,
sendMessage: () -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null,
@@ -70,14 +75,22 @@ 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.ImagePreview || cs.preview is ComposePreview.VideoPreview || cs.preview is ComposePreview.FilePreview)
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, onMessageChange)
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
// Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview) {
Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { }))
if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
Box(Modifier
.matchParentSize()
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.observer_cant_send_message_title),
text = generalGetString(R.string.observer_cant_send_message_desc)
)
})
)
}
if (showDeleteTextButton.value) {
DeleteTextButton(composeState)
@@ -99,11 +112,11 @@ fun SendMsgView(
Row(verticalAlignment = Alignment.CenterVertically) {
val stopRecOnNextClick = remember { mutableStateOf(false) }
when {
needToAllowVoiceToContact || !allowedVoiceByPrefs -> {
DisallowedVoiceButton {
needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> {
DisallowedVoiceButton(userCanSend) {
if (needToAllowVoiceToContact) {
showNeedToAllowVoiceAlert(allowVoiceToContact)
} else {
} else if (!allowedVoiceByPrefs) {
showDisabledVoiceAlert(isDirectChat)
}
}
@@ -118,7 +131,7 @@ fun SendMsgView(
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem) {
Spacer(Modifier.width(10.dp))
StartLiveMessageButton {
StartLiveMessageButton(userCanSend) {
if (composeState.value.preview is ComposePreview.NoPreview) {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
}
@@ -173,6 +186,7 @@ private fun NativeKeyboard(
composeState: MutableState<ComposeState>,
textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean,
onMessageChange: (String) -> Unit
) {
val cs = composeState.value
@@ -229,7 +243,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
@@ -253,15 +277,22 @@ private fun NativeKeyboard(
showDeleteTextButton.value = it.lineCount >= 4
}
if (composeState.value.preview is ComposePreview.VoicePreview) {
Text(
generalGetString(R.string.voice_message_send_text),
Modifier.padding(padding),
color = HighOrLowlight,
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
)
ComposeOverlay(R.string.voice_message_send_text, textStyle, padding)
} else if (userIsObserver) {
ComposeOverlay(R.string.you_are_observer, textStyle, padding)
}
}
@Composable
private fun ComposeOverlay(textId: Int, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
Text(
generalGetString(textId),
Modifier.padding(padding),
color = HighOrLowlight,
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
)
}
@Composable
private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) {
IconButton(
@@ -322,8 +353,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
}
@Composable
private fun DisallowedVoiceButton(onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp)) {
private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
Icon(
Icons.Outlined.KeyboardVoice,
stringResource(R.string.icon_descr_record_voice_message),
@@ -454,13 +485,13 @@ private fun SendMsgButton(
}
@Composable
private fun StartLiveMessageButton(onClick: () -> Unit) {
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier.requiredSize(36.dp)
.clickable(
onClick = onClick,
enabled = true,
enabled = enabled,
role = Role.Button,
interactionSource = interactionSource,
indication = rememberRipple(bounded = false, radius = 24.dp)
@@ -470,7 +501,7 @@ private fun StartLiveMessageButton(onClick: () -> Unit) {
Icon(
Icons.Filled.Bolt,
stringResource(R.string.icon_descr_send_message),
tint = MaterialTheme.colors.primary,
tint = if (enabled) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier
.size(36.dp)
.padding(4.dp)
@@ -571,6 +602,8 @@ fun PreviewSendMsgView() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
@@ -599,6 +632,8 @@ fun PreviewSendMsgViewEditing() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },
@@ -627,6 +662,8 @@ fun PreviewSendMsgViewInProgress() {
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = {},
onMessageChange = { _ -> },

View File

@@ -34,7 +34,7 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.*
@Composable
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) {
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) {
BackHandler(onBack = close)
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val developerTools = chatModel.controller.appPrefs.developerTools.get()
@@ -82,6 +82,9 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdat
editGroupProfile = {
ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) }
},
addOrEditWelcomeMessage = {
ModalManager.shared.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) }
},
openPreferences = {
ModalManager.shared.showCustomModal { close ->
GroupPreferencesView(
@@ -95,9 +98,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdat
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
manageGroupLink = {
withApi {
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) }
}
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) }
}
)
}
@@ -149,6 +150,7 @@ fun GroupChatInfoLayout(
addMembers: () -> Unit,
showMemberInfo: (GroupMember) -> Unit,
editGroupProfile: () -> Unit,
addOrEditWelcomeMessage: () -> Unit,
openPreferences: () -> Unit,
deleteGroup: () -> Unit,
clearChat: () -> Unit,
@@ -173,6 +175,8 @@ fun GroupChatInfoLayout(
if (groupInfo.canEdit) {
SectionItemView(editGroupProfile) { EditGroupProfileButton() }
SectionDivider()
SectionItemView(addOrEditWelcomeMessage) { AddOrEditWelcomeMessage(groupInfo.groupProfile.description) }
SectionDivider()
}
GroupPreferencesButton(openPreferences)
}
@@ -300,6 +304,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false) {
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier.weight(1f).padding(end = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
@@ -388,6 +393,28 @@ fun EditGroupProfileButton() {
}
}
@Composable
private fun AddOrEditWelcomeMessage(welcomeMessage: String?) {
val text = if (welcomeMessage == null) {
stringResource(R.string.button_add_welcome_message)
} else {
stringResource(R.string.button_welcome_message)
}
Row(
Modifier
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Outlined.MapsUgc,
text,
tint = HighOrLowlight
)
Spacer(Modifier.size(8.dp))
Text(text)
}
}
@Composable
private fun LeaveGroupButton() {
Row(
@@ -433,7 +460,7 @@ fun PreviewGroupChatInfoLayout() {
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
developerTools = false,
groupLink = null,
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
)
}
}

View File

@@ -1,6 +1,9 @@
package chat.simplex.app.views.chat.group
import SectionItemView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
@@ -15,22 +18,26 @@ 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.ChatModel
import chat.simplex.app.model.GroupInfo
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) {
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) }
val cxt = LocalContext.current
fun createLink() {
creatingLink = true
withApi {
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
onGroupLinkUpdated(groupLink)
val link = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
}
creatingLink = false
}
}
@@ -41,9 +48,24 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
}
GroupLinkLayout(
groupLink = groupLink,
groupInfo,
groupLinkMemberRole,
creatingLink,
createLink = ::createLink,
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
updateLink = {
val role = groupLinkMemberRole.value
if (role != null) {
withBGApi {
val link = chatModel.controller.apiGroupLinkMemberRole(groupInfo.groupId, role)
if (link != null) {
groupLink = link.first
groupLinkMemberRole.value = link.second
onGroupLinkUpdated(groupLink to groupLinkMemberRole.value)
}
}
}
},
deleteLink = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.delete_link_question),
@@ -54,7 +76,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
if (r) {
groupLink = null
onGroupLinkUpdated(null)
onGroupLinkUpdated(null to null)
}
}
}
@@ -69,13 +91,18 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
@Composable
fun GroupLinkLayout(
groupLink: String?,
groupInfo: GroupInfo,
groupLinkMemberRole: MutableState<GroupMemberRole?>,
creatingLink: Boolean,
createLink: () -> Unit,
share: () -> Unit,
updateLink: () -> Unit,
deleteLink: () -> Unit
) {
Column(
Modifier.padding(horizontal = DEFAULT_PADDING),
Modifier
.verticalScroll(rememberScrollState())
.padding(start = DEFAULT_PADDING, bottom = DEFAULT_BOTTOM_PADDING, end = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top
) {
@@ -93,7 +120,17 @@ fun GroupLinkLayout(
if (groupLink == null) {
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, disabled = creatingLink, click = createLink)
} else {
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
SectionItemView(padding = PaddingValues(bottom = DEFAULT_PADDING)) {
RoleSelectionRow(groupInfo, groupLinkMemberRole)
}
var initialLaunch by remember { mutableStateOf(true) }
LaunchedEffect(groupLinkMemberRole.value) {
if (!initialLaunch) {
updateLink()
}
initialLaunch = false
}
QRCode(groupLink, Modifier.aspectRatio(1f))
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
@@ -116,6 +153,25 @@ fun GroupLinkLayout(
}
}
@Composable
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole?>, enabled: Boolean = true) {
Row(
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val values = listOf(GroupMemberRole.Member, GroupMemberRole.Observer).map { it to it.text }
ExposedDropDownSettingRow(
generalGetString(R.string.initial_member_role),
values,
selectedRole,
icon = null,
enabled = rememberUpdatedState(enabled),
onSelected = { selectedRole.value = it }
)
}
}
@Composable
fun ProgressIndicator() {
Box(

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

@@ -0,0 +1,94 @@
package chat.simplex.app.views.chat.group
import SectionItemView
import SectionSpacer
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
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.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
var groupInfo by remember { mutableStateOf(groupInfo) }
val welcomeText = remember { mutableStateOf(groupInfo.groupProfile.description ?: "") }
fun save(afterSave: () -> Unit = {}) {
withApi {
var welcome: String? = welcomeText.value.trim('\n', ' ')
if (welcome?.length == 0) {
welcome = null
}
val groupProfileUpdated = groupInfo.groupProfile.copy(description = welcome)
val res = m.controller.apiUpdateGroup(groupInfo.groupId, groupProfileUpdated)
if (res != null) {
groupInfo = res
m.updateGroup(res)
welcomeText.value = welcome ?: ""
}
afterSave()
}
}
ModalView(
close = {
if (welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)) close()
else showUnsavedChangesAlert({ save(close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
GroupWelcomeLayout(
welcomeText,
groupInfo,
save = ::save
)
}
}
@Composable
private fun GroupWelcomeLayout(
welcomeText: MutableState<String>,
groupInfo: GroupInfo,
save: () -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(R.string.group_welcome_title))
val welcomeText = remember { welcomeText }
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
SectionSpacer()
SaveButton(
save = save,
disabled = welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
)
}
}
@Composable
private fun SaveButton(save: () -> Unit, disabled: Boolean) {
SectionView {
SectionItemView(save, disabled = disabled) {
Text(stringResource(R.string.save_and_update_group_profile), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_welcome_message_question),
confirmText = generalGetString(R.string.save_and_update_group_profile),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}

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,32 @@ 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.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)
}
} else {
fileIcon()
@@ -191,7 +228,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,18 @@ 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.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)
else -> {}
}
}
@@ -136,7 +134,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 +177,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

@@ -11,6 +11,7 @@ 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.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -58,7 +59,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
StatusIconText(Icons.Filled.Circle, Color.Transparent)
Spacer(Modifier.width(4.dp))
}
Text(meta.timestampText, color = color, fontSize = 13.sp)
Text(meta.timestampText, color = color, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
// the conditions in this function should match CIMetaText

View File

@@ -0,0 +1,339 @@
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.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)
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
@@ -25,6 +27,7 @@ import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
import 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 +43,7 @@ fun ChatItemView(
linkMode: SimplexLinkMode,
deleteMessage: (Long, CIDeleteMode) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
@@ -91,6 +95,14 @@ fun ChatItemView(
}
}
fun moderateMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.moderate_message_will_be_deleted_warning)
} else {
generalGetString(R.string.moderate_message_will_be_marked_warning)
}
}
@Composable
fun MsgContentItemDropdownMenu() {
DropdownMenu(
@@ -120,14 +132,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
@@ -150,9 +168,16 @@ fun ChatItemView(
}
)
}
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancellable) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile)
}
if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
val groupInfo = cItem.memberToModerate(cInfo)?.first
if (groupInfo != null) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
}
}
}
@@ -163,14 +188,16 @@ fun ChatItemView(
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
ItemAction(
stringResource(R.string.reveal_verb),
Icons.Outlined.Visibility,
onClick = {
revealed.value = true
showMenu.value = false
}
)
if (!cItem.isDeletedContent) {
ItemAction(
stringResource(R.string.reveal_verb),
Icons.Outlined.Visibility,
onClick = {
revealed.value = true
showMenu.value = false
}
)
}
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@@ -238,14 +265,31 @@ fun ChatItemView(
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
is CIContent.SndModerated -> DeletedItem()
is CIContent.RcvModerated -> DeletedItem()
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
}
}
}
}
@Composable
fun CancelFileItemAction(
fileId: Long,
showMenu: MutableState<Boolean>,
cancelFile: (Long) -> Unit
) {
ItemAction(
stringResource(R.string.cancel_verb),
Icons.Outlined.Close,
onClick = {
showMenu.value = false
cancelFileAlertDialog(fileId, cancelFile = cancelFile)
},
color = Color.Red
)
}
@Composable
fun DeleteItemAction(
cItem: ChatItem,
@@ -264,6 +308,24 @@ fun DeleteItemAction(
)
}
@Composable
fun ModerateItemAction(
cItem: ChatItem,
questionText: String,
showMenu: MutableState<Boolean>,
deleteMessage: (Long, CIDeleteMode) -> Unit
) {
ItemAction(
stringResource(R.string.moderate_verb),
Icons.Outlined.Flag,
onClick = {
showMenu.value = false
moderateMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
},
color = Color.Red
)
}
@Composable
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) {
DropdownMenuItem(onClick) {
@@ -281,6 +343,18 @@ fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Colo
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.cancel_file__question),
text = generalGetString(R.string.file_transfer_will_be_cancelled_warning),
confirmText = generalGetString(R.string.confirm_verb),
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),
@@ -308,6 +382,18 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
)
}
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_member_message__question),
text = questionText,
confirmText = generalGetString(R.string.delete_verb),
destructive = true,
onConfirm = {
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
}
)
}
private fun showMsgDeliveryErrorAlert(description: String) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.message_delivery_error_title),
@@ -329,6 +415,7 @@ fun PreviewChatItemView() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
@@ -349,6 +436,7 @@ fun PreviewChatItemViewDeletedContent() {
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
deleteMessage = { _, _ -> },
receiveFile = {},
cancelFile = {},
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},

View File

@@ -7,6 +7,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Flag
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -20,6 +21,7 @@ import androidx.compose.ui.platform.UriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.*
import androidx.compose.ui.unit.*
import androidx.compose.ui.util.fastMap
@@ -75,10 +77,7 @@ fun FramedItemView(
Modifier
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight)
.fillMaxWidth()
.padding(start = 8.dp)
.padding(end = 12.dp)
.padding(top = 6.dp)
.padding(bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
if (icon != null) {
@@ -96,6 +95,8 @@ fun FramedItemView(
}
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@@ -124,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)
@@ -150,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))
@@ -166,7 +180,11 @@ fun FramedItemView(
Column(Modifier.width(IntrinsicSize.Max)) {
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
if (ci.meta.itemDeleted != null) {
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
FramedItemHeader(String.format(stringResource(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, Icons.Outlined.Flag)
} else {
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
}
} else if (ci.meta.isLive) {
FramedItemHeader(stringResource(R.string.live), false)
}
@@ -193,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

@@ -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
@@ -10,6 +9,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -30,19 +30,32 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
buildAnnotatedString {
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
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)
}
}
}
@Composable
private fun MarkedDeletedText(text: String) {
Text(
buildAnnotatedString {
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(text) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.annotatedStringResource
import chat.simplex.app.views.helpers.openUriCatching
import chat.simplex.app.views.usersettings.MarkdownHelpView
import chat.simplex.app.views.usersettings.simplexTeamUri
@@ -36,7 +37,7 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Text(
annotatedStringResource(R.string.you_can_connect_to_simplex_chat_founder),
modifier = Modifier.clickable(onClick = {
uriHandler.openUri(simplexTeamUri)
uriHandler.openUriCatching(simplexTeamUri)
}),
lineHeight = 22.sp
)

View File

@@ -132,7 +132,7 @@ private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
val uriHandler = LocalUriHandler.current
ConnectButton(generalGetString(R.string.chat_with_developers)) {
uriHandler.openUri(simplexTeamUri)
uriHandler.openUriCatching(simplexTeamUri)
}
Spacer(Modifier.height(DEFAULT_PADDING))
ConnectButton(generalGetString(R.string.tap_to_start_new_chat), openNewChatSheet)
@@ -208,9 +208,9 @@ 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 { !it.user.activeUser }
.filter { u -> !u.user.activeUser && !u.user.hidden }
.all { u -> u.unreadCount == 0 }
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
if (users.size == 1) {
@@ -247,7 +247,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

@@ -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,7 +108,7 @@ 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(

View File

@@ -9,11 +9,12 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.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.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
@@ -33,10 +34,24 @@ 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 { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } }
val users by remember {
derivedStateOf {
chatModel.users
.filter { u -> u.user.activeUser || !u.user.hidden }
.sortedByDescending { it.user.activeUser }
}
}
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
LaunchedEffect(Unit) {
launch {
@@ -94,23 +109,22 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
.background(if (isInDarkTheme()) MaterialTheme.colors.background.darker(-0.7f) else MaterialTheme.colors.background, MaterialTheme.shapes.medium)
) {
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
openSettings()
settingsClicked()
userPickerState.value = AnimatedViewState.GONE
}) {
userPickerState.value = AnimatedViewState.HIDING
if (!u.user.activeUser) {
chatModel.chats.clear()
scope.launch {
val job = launch {
delay(500)
switchingUsers.value = true
}
chatModel.controller.changeActiveUser(u.user.userId)
chatModel.controller.changeActiveUser(u.user.userId, null)
job.cancel()
switchingUsers.value = false
}
@@ -120,9 +134,17 @@ 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
}
}
}
}
@@ -144,33 +166,19 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
ProfileImage(
image = u.image,
size = 54.dp
)
Text(
u.displayName,
modifier = Modifier
.padding(start = 8.dp, end = 8.dp),
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
UserProfileRow(u)
if (u.activeUser) {
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
} else if (u.hidden) {
Icon(Icons.Outlined.Lock, null, Modifier.size(20.dp), tint = HighOrLowlight)
} else if (unreadCount > 0) {
Row {
Text(
unreadCountStr(unreadCount),
color = MaterialTheme.colors.onPrimary,
color = Color.White,
fontSize = 11.sp,
modifier = Modifier
.background(MaterialTheme.colors.primary, shape = CircleShape)
.background(if (u.showNtfs) MaterialTheme.colors.primary else HighOrLowlight, shape = CircleShape)
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
.padding(horizontal = 3.dp)
.padding(vertical = 1.dp),
@@ -179,12 +187,35 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit
)
Spacer(Modifier.width(2.dp))
}
} else {
} else if (!u.showNtfs) {
Icon(Icons.Outlined.NotificationsOff, null, Modifier.size(20.dp), tint = HighOrLowlight)
} else {
Box(Modifier.size(20.dp))
}
}
}
@Composable
fun UserProfileRow(u: User) {
Row(
Modifier
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
ProfileImage(
image = u.image,
size = 54.dp
)
Text(
u.displayName,
modifier = Modifier
.padding(start = 8.dp, end = 8.dp),
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
}
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
@@ -196,3 +227,15 @@ private fun SettingsPickerItem(onClick: () -> Unit) {
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
}
}
@Composable
private fun CancelPickerItem(onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
val text = generalGetString(R.string.cancel_verb)
Text(
text,
color = MaterialTheme.colors.onBackground,
)
Icon(Icons.Outlined.Close, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
}
}

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.database
import SectionDivider
import SectionItemView
import SectionItemViewSpaceBetween
import SectionTextFooter
@@ -25,13 +26,13 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.datetime.Clock
import kotlin.math.log2
@@ -161,7 +162,9 @@ fun DatabaseEncryptionLayout(
}
if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) {
DatabaseKeyField(
SectionDivider()
PassphraseField(
currentKey,
generalGetString(R.string.current_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
@@ -170,7 +173,9 @@ fun DatabaseEncryptionLayout(
)
}
DatabaseKeyField(
SectionDivider()
PassphraseField(
newKey,
generalGetString(R.string.new_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
@@ -201,7 +206,9 @@ fun DatabaseEncryptionLayout(
!validKey(newKey.value) ||
progressIndicator.value
DatabaseKeyField(
SectionDivider()
PassphraseField(
confirmNewKey,
generalGetString(R.string.confirm_new_passphrase),
modifier = Modifier.padding(horizontal = DEFAULT_PADDING),
@@ -212,7 +219,9 @@ fun DatabaseEncryptionLayout(
}),
)
SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled) {
SectionDivider()
SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) {
Text(generalGetString(R.string.update_database_passphrase), color = if (disabled) HighOrLowlight else MaterialTheme.colors.primary)
}
}
@@ -285,9 +294,10 @@ fun SavePassphraseSetting(
initialRandomDBPassphrase: Boolean,
storedKey: Boolean,
progressIndicator: Boolean,
minHeight: Dp = TextFieldDefaults.MinHeight,
onCheckedChange: (Boolean) -> Unit,
) {
SectionItemView {
SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (storedKey) Icons.Filled.VpnKey else Icons.Filled.VpnKeyOff,
@@ -349,13 +359,14 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DatabaseKeyField(
fun PassphraseField(
key: MutableState<String>,
placeholder: String,
modifier: Modifier = Modifier,
showStrength: Boolean = false,
isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(),
dependsOn: MutableState<String>? = null,
) {
var valid by remember { mutableStateOf(validKey(key.value)) }
var showKey by remember { mutableStateOf(false) }
@@ -436,6 +447,13 @@ fun DatabaseKeyField(
)
}
)
LaunchedEffect(Unit) {
snapshotFlow { dependsOn?.value }
.distinctUntilChanged()
.collect {
valid = isValid(state.value.text)
}
}
}
// based on https://generatepasswords.org/how-to-calculate-entropy/

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
@@ -39,24 +42,39 @@ fun DatabaseErrorView(
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
val context = LocalContext.current
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
val saveAndRunChatOnClick: () -> Unit = {
fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
val useKey = if (useKeychain) null else dbKey.value
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences)
}
fun saveAndRunChatOnClick() {
DatabaseUtils.setDatabaseKey(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,9 +250,17 @@ 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) {
DatabaseKeyField(
PassphraseField(
text,
generalGetString(R.string.enter_passphrase),
isValid = ::validKey,

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.*
@@ -154,7 +154,7 @@ fun DatabaseLayout(
val operationsDisabled = !stopped || progressIndicator
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 48.dp),
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = DEFAULT_BOTTOM_PADDING),
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.your_chat_database))
@@ -190,7 +190,7 @@ fun DatabaseLayout(
disabled = operationsDisabled
)
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.Backup, stringResource(R.string.full_backup), privacyFullBackup)
AppDataBackupPreference(privacyFullBackup, initialRandomDBPassphrase)
SectionDivider()
SettingsActionItem(
Icons.Outlined.IosShare,
@@ -270,6 +270,36 @@ fun DatabaseLayout(
}
}
@Composable
private fun AppDataBackupPreference(privacyFullBackup: SharedPreference<Boolean>, initialRandomDBPassphrase: SharedPreference<Boolean>) {
SectionItemView {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Outlined.Backup, stringResource(R.string.full_backup), tint = HighOrLowlight)
Spacer(Modifier.padding(horizontal = 4.dp))
val prefState = remember { mutableStateOf(privacyFullBackup.get()) }
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.full_backup), Modifier.padding(end = 24.dp))
Spacer(Modifier.fillMaxWidth().weight(1f))
Switch(
checked = prefState.value,
onCheckedChange = {
if (initialRandomDBPassphrase.get()) {
exportProhibitedAlert()
} else {
privacyFullBackup.set(it)
prefState.value = it
}
},
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
)
)
}
}
}
}
private fun setChatItemTTLAlert(
m: ChatModel, selectedChatItemTTL: MutableState<ChatItemTTL>,
progressIndicator: MutableState<Boolean>,
@@ -590,7 +620,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")

View File

@@ -15,12 +15,14 @@ import chat.simplex.app.views.newchat.ActionButton
sealed class AttachmentOption {
object TakePhoto: AttachmentOption()
object PickImage: AttachmentOption()
object PickVideo: AttachmentOption()
object PickFile: AttachmentOption()
}
@Composable
fun ChooseAttachmentView(
attachmentOption: MutableState<AttachmentOption?>,
allowVideoAttachment: Boolean,
hide: () -> Unit
) {
Box(
@@ -45,6 +47,12 @@ fun ChooseAttachmentView(
attachmentOption.value = AttachmentOption.PickImage
hide()
}
if (allowVideoAttachment) {
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Videocam) {
attachmentOption.value = AttachmentOption.PickVideo
hide()
}
}
ActionButton(null, stringResource(R.string.choose_file), icon = Icons.Outlined.InsertDriveFile) {
attachmentOption.value = AttachmentOption.PickFile
hide()

View File

@@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -22,7 +23,11 @@ fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit
Modifier
.padding(top = 4.dp), // Like in DefaultAppBar
content = {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Row(
Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
NavigationButtonBack(close)
Row {
endButtons()

View File

@@ -66,8 +66,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

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

View File

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

@@ -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
@@ -114,7 +113,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!!)
@@ -205,17 +204,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)
)
}
}
}

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

@@ -85,13 +85,14 @@ fun SectionItemView(
click: (() -> Unit)? = null,
minHeight: Dp = 46.dp,
disabled: Boolean = false,
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
content: (@Composable RowScope.() -> Unit)
) {
val modifier = Modifier
.fillMaxWidth()
.sizeIn(minHeight = minHeight)
Row(
if (click == null || disabled) modifier.padding(horizontal = DEFAULT_PADDING) else modifier.clickable(onClick = click).padding(horizontal = DEFAULT_PADDING),
if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding),
verticalAlignment = Alignment.CenterVertically
) {
content()

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

@@ -1,12 +1,18 @@
package chat.simplex.app.views.helpers
import android.app.Activity
import android.app.Application
//import android.app.LocaleManager
import android.content.ActivityNotFoundException
import android.content.Context
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.FileUtils
import android.os.*
import android.provider.OpenableColumns
import android.text.Spanned
import android.text.SpannedString
@@ -29,11 +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.model.CIFile
import chat.simplex.app.model.json
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.*
@@ -231,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"
}
@@ -319,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)
@@ -327,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)
}
@@ -400,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")
@@ -436,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()]
@@ -483,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)
@@ -501,3 +581,63 @@ inline fun <reified T> serializableSaver(): Saver<T, *> = Saver(
save = { json.encodeToString(it) },
restore = { json.decodeFromString(it) }
)
fun saveAppLocale(pref: SharedPreference<String?>, activity: Activity, languageCode: String? = null) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// val localeManager = SimplexApp.context.getSystemService(LocaleManager::class.java)
// localeManager.applicationLocales = LocaleList(Locale.forLanguageTag(languageCode ?: return))
// } else {
pref.set(languageCode)
if (languageCode == null) {
activity.applyLocale(SimplexApp.context.defaultLocale)
}
activity.recreate()
// }
}
fun Activity.applyAppLocale(pref: SharedPreference<String?>) {
// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
val lang = pref.get()
if (lang == null || lang == Locale.getDefault().language) return
applyLocale(Locale.forLanguageTag(lang))
// }
}
private fun Activity.applyLocale(locale: Locale) {
Locale.setDefault(locale)
val appConf = Configuration(SimplexApp.context.resources.configuration).apply { setLocale(locale) }
val activityConf = Configuration(resources.configuration).apply { setLocale(locale) }
@Suppress("DEPRECATION")
SimplexApp.context.resources.updateConfiguration(appConf, resources.displayMetrics)
@Suppress("DEPRECATION")
resources.updateConfiguration(activityConf, resources.displayMetrics)
}
fun UriHandler.openUriCatching(uri: String) {
try {
openUri(uri)
} catch (e: ActivityNotFoundException) {
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()
}
}
}
}

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

@@ -36,7 +36,7 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
val uriHandler = LocalUriHandler.current
Text(
annotatedStringResource(R.string.read_more_in_github_with_link),
modifier = Modifier.padding(bottom = 12.dp).clickable { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#readme") },
modifier = Modifier.padding(bottom = 12.dp).clickable { uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#readme") },
lineHeight = 22.sp
)
} else {

View File

@@ -18,8 +18,7 @@ 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
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.User
@@ -47,15 +46,15 @@ fun SimpleXInfoLayout(
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING),
) {
Box(Modifier.fillMaxWidth().padding(top = 24.dp, bottom = 8.dp), contentAlignment = Alignment.Center) {
Box(Modifier.fillMaxWidth().padding(top = 8.dp), contentAlignment = Alignment.Center) {
SimpleXLogo()
}
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 24.dp), textAlign = TextAlign.Center)
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)
InfoRow(painterResource(R.drawable.privacy), R.string.privacy_redefined, R.string.first_platform_without_user_ids)
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)
InfoRow(painterResource(R.drawable.decentralized), R.string.decentralized, R.string.opensource_protocol_and_code_anybody_can_run_servers)
InfoRow(painterResource(if (isInDarkTheme()) R.drawable.decentralized_light else R.drawable.decentralized), R.string.decentralized, R.string.opensource_protocol_and_code_anybody_can_run_servers)
Spacer(Modifier.fillMaxHeight().weight(1f))
@@ -89,14 +88,14 @@ fun SimpleXLogo() {
}
@Composable
private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: Int) {
Row(Modifier.padding(bottom = 20.dp), verticalAlignment = Alignment.Top) {
private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: Int, width: Dp = 76.dp) {
Row(Modifier.padding(bottom = 27.dp), verticalAlignment = Alignment.Top) {
Image(icon, contentDescription = null, modifier = Modifier
.width(60.dp)
.padding(top = 8.dp, end = 16.dp))
.width(width)
.padding(top = 8.dp, start = 8.dp, end = 24.dp))
Column(horizontalAlignment = Alignment.Start) {
Text(stringResource(titleId), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h3, lineHeight = 24.sp)
Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.caption)
Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.body1)
}
}
}

View File

@@ -35,7 +35,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
Icon(
Icons.Outlined.OpenInNew, stringResource(titleId), tint = MaterialTheme.colors.primary,
modifier = Modifier
.clickable { uriHandler.openUri(link) }
.clickable { uriHandler.openUriCatching(link) }
)
}
@@ -270,8 +270,45 @@ private val versionDescriptions: List<VersionDescription> = listOf(
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
)
),
VersionDescription(
version = "v4.6",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.Lock,
titleId = R.string.v4_6_hidden_chat_profiles,
descrId = R.string.v4_6_hidden_chat_profiles_descr
),
FeatureDescription(
icon = Icons.Outlined.Flag,
titleId = R.string.v4_6_group_moderation,
descrId = R.string.v4_6_group_moderation_descr
),
FeatureDescription(
icon = Icons.Outlined.MapsUgc,
titleId = R.string.v4_6_group_welcome_message,
descrId = R.string.v4_6_group_welcome_message_descr
),
FeatureDescription(
icon = Icons.Outlined.Call,
titleId = R.string.v4_6_audio_video_calls,
descrId = R.string.v4_6_audio_video_calls_descr
),
FeatureDescription(
icon = Icons.Outlined.Battery3Bar,
titleId = R.string.v4_6_reduced_battery_usage,
descrId = R.string.v4_6_reduced_battery_usage_descr
),
FeatureDescription(
icon = Icons.Outlined.Translate,
titleId = R.string.v4_6_chinese_spanish_interface,
descrId = R.string.v4_6_chinese_spanish_interface_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
),
)
private val lastVersion = versionDescriptions.last().version
fun setLastVersionDefault(m: ChatModel) {

View File

@@ -2,13 +2,20 @@ package chat.simplex.app.views.usersettings
import SectionCustomFooter
import SectionDivider
import SectionItemView
import SectionItemViewSpaceBetween
import SectionItemWithValue
import SectionSpacer
import SectionView
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
import android.net.Uri
import android.os.Build
import android.provider.Settings
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
@@ -17,6 +24,7 @@ import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Circle
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.shadow
@@ -30,9 +38,13 @@ import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.SharedPreference
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import com.godaddy.android.colorpicker.*
import kotlinx.coroutines.delay
import java.util.*
enum class AppIcon(val resId: Int) {
DEFAULT(R.mipmap.icon),
@@ -40,7 +52,7 @@ enum class AppIcon(val resId: Int) {
}
@Composable
fun AppearanceView() {
fun AppearanceView(m: ChatModel) {
val appIcon = remember { mutableStateOf(findEnabledIcon()) }
fun setAppIcon(newIcon: AppIcon) {
@@ -62,6 +74,7 @@ fun AppearanceView() {
AppearanceLayout(
appIcon,
m.controller.appPrefs.appLanguage,
changeIcon = ::setAppIcon,
editPrimaryColor = { primary ->
ModalManager.shared.showModalCloseable { close ->
@@ -73,6 +86,7 @@ fun AppearanceView() {
@Composable fun AppearanceLayout(
icon: MutableState<AppIcon>,
languagePref: SharedPreference<String?>,
changeIcon: (AppIcon) -> Unit,
editPrimaryColor: (Color) -> Unit,
) {
@@ -81,6 +95,37 @@ fun AppearanceView() {
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.appearance_settings))
SectionView(stringResource(R.string.settings_section_title_language), padding = PaddingValues()) {
val context = LocalContext.current
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// SectionItemWithValue(
// generalGetString(R.string.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
// remember { mutableStateOf("system") },
// listOf(ValueTitleDesc("system", generalGetString(R.string.change_verb), "")),
// onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) }
// )
// } else {
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
SectionItemView {
LangSelector(state) {
state.value = it
withApi {
delay(200)
val activity = context as? Activity
if (activity != null) {
if (it == "system") {
saveAppLocale(languagePref, activity)
} else {
saveAppLocale(languagePref, activity, it)
}
}
}
}
}
// }
}
SectionSpacer()
SectionView(stringResource(R.string.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
LazyRow {
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
@@ -179,6 +224,32 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
)
}
@Composable
private fun LangSelector(state: State<String>, onSelected: (String) -> Unit) {
// Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs`
val supportedLanguages = mapOf(
"system" to generalGetString(R.string.language_system),
"en" to "English",
"cs" to "Čeština",
"de" to "Deutsch",
"es" to "Español",
"fr" to "Français",
"it" to "Italiano",
"nl" to "Nederlands",
"ru" to "Русский",
"zh-CN" to "简体中文"
)
val values by remember { mutableStateOf(supportedLanguages.map { it.key to it.value }) }
ExposedDropDownSettingRow(
generalGetString(R.string.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
values,
state,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
@Composable
private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme) -> Unit) {
val darkTheme = isSystemInDarkTheme()
@@ -193,6 +264,9 @@ private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme)
)
}
//private fun openSystemLangPicker(activity: Activity) {
// activity.startActivity(Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:" + SimplexApp.context.packageName)))
//}
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
SimplexApp.context.packageManager.getComponentEnabledSetting(
@@ -206,6 +280,7 @@ fun PreviewAppearanceSettings() {
SimpleXTheme {
AppearanceLayout(
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
languagePref = SharedPreference({ null }, {}),
changeIcon = {},
editPrimaryColor = {},
)

View File

@@ -2,6 +2,7 @@ package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionTextFooter
import SectionView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -43,16 +44,23 @@ fun CallSettingsLayout(
AppBarTitle(stringResource(R.string.your_calls))
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
SectionView(stringResource(R.string.settings_section_title_settings)) {
SectionItemView() {
SharedPreferenceToggle(stringResource(R.string.connect_calls_via_relay), webrtcPolicyRelay)
}
SectionItemView(editIceServers) { Text(stringResource(R.string.webrtc_ice_servers)) }
SectionDivider()
val enabled = remember { mutableStateOf(true) }
SectionItemView { LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it }) }
SectionDivider()
SectionItemView(editIceServers) { Text(stringResource(R.string.webrtc_ice_servers)) }
SectionItemView() {
SharedPreferenceToggle(stringResource(R.string.always_use_relay), webrtcPolicyRelay)
}
}
SectionTextFooter(
if (remember { webrtcPolicyRelay.state }.value) {
generalGetString(R.string.relay_server_protects_ip)
} else {
generalGetString(R.string.relay_server_if_necessary)
}
)
}
}

View File

@@ -0,0 +1,59 @@
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()
val xftpSendEnabled = m.controller.appPrefs.xftpSendEnabled
val xftpEnabled = remember { mutableStateOf(xftpSendEnabled.get()) }
SectionView(generalGetString(R.string.settings_section_title_experimenta)) {
SettingsPreferenceItem(Icons.Outlined.UploadFile, stringResource(R.string.settings_send_files_via_xftp), xftpSendEnabled, xftpEnabled) {
withApi { m.controller.apiSetXFTPConfig(m.controller.getXFTPCfg()) }
}
}
if (xftpEnabled.value) {
SectionTextFooter(generalGetString(R.string.xftp_requires_v461))
}
}
}

View File

@@ -5,18 +5,18 @@ 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.material.icons.outlined.UploadFile
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
import chat.simplex.app.views.helpers.withApi
@Composable
fun ExperimentalFeaturesView(chatModel: ChatModel, enableCalls: MutableState<Boolean>) {
fun ExperimentalFeaturesView(chatModel: ChatModel) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
@@ -27,7 +27,11 @@ fun ExperimentalFeaturesView(chatModel: ChatModel, enableCalls: MutableState<Boo
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)
SettingsPreferenceItem(Icons.Outlined.UploadFile, stringResource(R.string.settings_send_files_via_xftp), chatModel.controller.appPrefs.xftpSendEnabled) {
withApi {
chatModel.controller.apiSetXFTPConfig(chatModel.controller.getXFTPCfg())
}
}
}
}
}
}

View File

@@ -0,0 +1,90 @@
package chat.simplex.app.views.usersettings
import SectionDivider
import SectionItemView
import SectionItemViewSpaceBetween
import SectionSpacer
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chatlist.UserProfileRow
import chat.simplex.app.views.database.PassphraseField
import chat.simplex.app.views.helpers.*
@Composable
fun HiddenProfileView(
m: ChatModel,
user: User,
close: () -> Unit,
) {
HiddenProfileLayout(
user,
saveProfilePassword = { hidePassword ->
withBGApi {
try {
val u = m.controller.apiHideUser(user.userId, hidePassword)
m.updateUser(u)
close()
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.error_saving_user_password),
text = e.stackTraceToString()
)
}
}
}
)
}
@Composable
private fun HiddenProfileLayout(
user: User,
saveProfilePassword: (String) -> Unit
) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_BOTTOM_PADDING),
) {
AppBarTitle(stringResource(R.string.hide_profile))
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
UserProfileRow(user)
}
SectionSpacer()
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 == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } }
SectionView(stringResource(R.string.hidden_profile_password).uppercase()) {
SectionItemView {
PassphraseField(hidePassword, generalGetString(R.string.password_to_show), isValid = { passwordValid }, showStrength = true)
}
SectionDivider()
SectionItemView {
PassphraseField(confirmHidePassword, stringResource(R.string.confirm_password), isValid = { confirmValid }, dependsOn = hidePassword)
}
SectionDivider()
SectionItemViewSpaceBetween({ saveProfilePassword(hidePassword.value) }, disabled = saveDisabled, minHeight = TextFieldDefaults.MinHeight) {
Text(generalGetString(R.string.save_profile_password), color = if (saveDisabled) HighOrLowlight else MaterialTheme.colors.primary)
}
}
SectionTextFooter(stringResource(R.string.to_reveal_profile_enter_password))
}
}

View File

@@ -25,6 +25,7 @@ fun NetworkAndServersView(
chatModel: ChatModel,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
) {
// It's not a state, just a one-time value. Shouldn't be used in any state-related situations
val netCfg = remember { chatModel.controller.getNetCfg() }
@@ -44,6 +45,7 @@ fun NetworkAndServersView(
sessionMode = sessionMode,
showModal = showModal,
showSettingsModal = showSettingsModal,
showCustomModal = showCustomModal,
toggleSocksProxy = { enable ->
if (enable) {
AlertManager.shared.showAlertMsg(
@@ -138,6 +140,7 @@ fun NetworkAndServersView(
sessionMode: MutableState<TransportSessionMode>,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
toggleSocksProxy: (Boolean) -> Unit,
useOnion: (OnionHosts) -> Unit,
updateSessionMode: (TransportSessionMode) -> Unit,
@@ -149,7 +152,7 @@ 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), showSettingsModal { SMPServersView(it) })
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> SMPServersView(m, close) })
SectionDivider()
SectionItemView {
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
@@ -297,6 +300,7 @@ fun PreviewNetworkAndServersLayout() {
networkUseSocksProxy = remember { mutableStateOf(true) },
showModal = { {} },
showSettingsModal = { {} },
showCustomModal = { {} },
toggleSocksProxy = {},
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },

View File

@@ -13,7 +13,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R

View File

@@ -198,7 +198,7 @@ private fun howToButton() {
val uriHandler = LocalUriHandler.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/WEBRTC.md#configure-mobile-apps") }
modifier = Modifier.clickable { uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/WEBRTC.md#configure-mobile-apps") }
) {
Text(stringResource(R.string.how_to), color = MaterialTheme.colors.primary)
Icon(

View File

@@ -27,7 +27,7 @@ import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.launch
@Composable
fun SMPServersView(m: ChatModel) {
fun SMPServersView(m: ChatModel, close: () -> Unit) {
var servers by remember {
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
}
@@ -72,83 +72,90 @@ fun SMPServersView(m: ChatModel) {
}
}
val scope = rememberCoroutineScope()
SMPServersLayout(
testing = testing.value,
servers = servers,
serversUnchanged = serversUnchanged.value,
saveDisabled = saveDisabled.value,
allServersDisabled = allServersDisabled.value,
m.currentUser.value,
addServer = {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.smp_servers_add),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
servers = servers + ServerCfg.empty
// No saving until something will be changed on the next screen to prevent blank servers on the list
showServer(servers.last())
}) {
Text(stringResource(R.string.smp_servers_enter_manually))
}
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.shared.showModalCloseable { close ->
ScanSMPServer {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
ModalView(
close = {
if (saveDisabled.value) close()
else showUnsavedChangesAlert({ saveSMPServers(servers, m, close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
SMPServersLayout(
testing = testing.value,
servers = servers,
serversUnchanged = serversUnchanged.value,
saveDisabled = saveDisabled.value,
allServersDisabled = allServersDisabled.value,
m.currentUser.value,
addServer = {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.smp_servers_add),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
servers = servers + ServerCfg.empty
// No saving until something will be changed on the next screen to prevent blank servers on the list
showServer(servers.last())
}) {
Text(stringResource(R.string.smp_servers_enter_manually))
}
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.shared.showModalCloseable { close ->
ScanSMPServer {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
}
}
}
) {
Text(stringResource(R.string.smp_servers_scan_qr))
}
val hasAllPresets = hasAllPresets(servers, m)
if (!hasAllPresets) {
SectionItemView({
AlertManager.shared.hideAlert()
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
}) {
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
}
}
}
) {
Text(stringResource(R.string.smp_servers_scan_qr))
}
val hasAllPresets = hasAllPresets(servers, m)
if (!hasAllPresets) {
SectionItemView({
AlertManager.shared.hideAlert()
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
}) {
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
}
}
}
)
},
testServers = {
scope.launch {
testServers(testing, servers, m) {
servers = it
m.userSMPServersUnsaved.value = servers
}
}
)
},
testServers = {
scope.launch {
testServers(testing, servers, m) {
servers = it
m.userSMPServersUnsaved.value = servers
}
}
},
resetServers = {
servers = m.userSMPServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveSMPServers(servers, m)
},
showServer = ::showServer,
)
},
resetServers = {
servers = m.userSMPServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveSMPServers(servers, m)
},
showServer = ::showServer,
)
if (testing.value) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
if (testing.value) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = HighOrLowlight,
strokeWidth = 2.5.dp
)
}
}
}
}
@@ -247,7 +254,7 @@ private fun HowToButton() {
SettingsActionItem(
Icons.Outlined.OpenInNew,
stringResource(R.string.how_to_use_your_servers),
{ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md") },
{ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md") },
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary
)
@@ -324,12 +331,22 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
return fs
}
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel) {
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
withApi {
if (m.controller.setUserSMPServers(servers)) {
m.userSMPServers.value = servers
m.userSMPServersUnsaved.value = null
}
afterSave()
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.smp_save_servers_question),
confirmText = generalGetString(R.string.save_verb),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}

View File

@@ -6,6 +6,7 @@ import SectionSpacer
import SectionView
import android.content.Context
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
@@ -13,6 +14,7 @@ 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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -20,7 +22,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
@@ -53,11 +57,22 @@ 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 = rememberSaveable { mutableStateOf("") }
ModalView(
{ close() },
if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight,
endButtons = {
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), alwaysVisible = true) { search.value = it }
},
content = { modalView(chatModel, search) })
}
},
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
showVersion = {
withApi {
@@ -110,11 +125,11 @@ fun SettingsLayout(
encrypted: Boolean,
incognito: MutableState<Boolean>,
incognitoPref: SharedPreference<Boolean>,
developerTools: SharedPreference<Boolean>,
userDisplayName: String,
setPerformLA: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
showVersion: () -> Unit,
withAuth: (block: () -> Unit) -> Unit
@@ -141,7 +156,8 @@ fun SettingsLayout(
ProfilePreview(profile, stopped = stopped)
}
SectionDivider()
SettingsActionItem(Icons.Outlined.ManageAccounts, stringResource(R.string.your_chat_profiles), { withAuth { showSettingsModal { UserProfilesView(it) }() } }, disabled = stopped)
val profileHidden = rememberSaveable { mutableStateOf(false) }
SettingsActionItem(Icons.Outlined.ManageAccounts, stringResource(R.string.your_chat_profiles), { withAuth { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped)
SectionDivider()
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { showModal { IncognitoView() }() }
SectionDivider()
@@ -154,13 +170,13 @@ fun SettingsLayout(
SectionView(stringResource(R.string.settings_section_title_settings)) {
SettingsActionItem(Icons.Outlined.Bolt, stringResource(R.string.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped)
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal, showCustomModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView() }, disabled = stopped)
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped)
SectionDivider()
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
}
@@ -173,9 +189,9 @@ fun SettingsLayout(
SectionDivider()
SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
SectionDivider()
SettingsActionItem(Icons.Outlined.Tag, stringResource(R.string.chat_with_the_founder), { uriHandler.openUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
SettingsActionItem(Icons.Outlined.Tag, stringResource(R.string.chat_with_the_founder), { uriHandler.openUriCatching(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Email, stringResource(R.string.send_us_an_email), { uriHandler.openUri("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary)
SettingsActionItem(Icons.Outlined.Email, stringResource(R.string.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary)
}
SectionSpacer()
@@ -189,16 +205,9 @@ 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) })
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it) })
// SectionDivider()
AppVersionItem(showVersion)
}
@@ -313,7 +322,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
@Composable private fun ContributeItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#contribute") }) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) {
Icon(
Icons.Outlined.Keyboard,
contentDescription = "GitHub",
@@ -326,8 +335,8 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
@Composable private fun RateAppItem(uriHandler: UriHandler) {
SectionItemView({
runCatching { uriHandler.openUri("market://details?id=chat.simplex.app") }
.onFailure { uriHandler.openUri("https://play.google.com/store/apps/details?id=chat.simplex.app") }
runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") }
.onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") }
}
) {
Icon(
@@ -341,7 +350,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
@Composable private fun StarOnGithubItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
@@ -352,7 +361,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),
@@ -364,8 +373,8 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
}
}
@Composable private fun InstallTerminalAppItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
@Composable fun InstallTerminalAppItem(uriHandler: UriHandler) {
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
Icon(
painter = painterResource(id = R.drawable.ic_github),
contentDescription = "GitHub",
@@ -377,9 +386,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) {
@@ -526,11 +537,11 @@ fun PreviewSettingsLayout() {
encrypted = false,
incognito = remember { mutableStateOf(false) },
incognitoPref = SharedPreference({ false }, {}),
developerTools = SharedPreference({ false }, {}),
userDisplayName = "Alice",
setPerformLA = {},
showModal = { {} },
showSettingsModal = { {} },
showSettingsModalWithSearch = { },
showCustomModal = { {} },
showVersion = {},
withAuth = {},

View File

@@ -2,14 +2,18 @@ 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.*
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
@@ -17,18 +21,30 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.chatPasswordHash
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) {
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>) {
val searchTextOrPassword = rememberSaveable { search }
val users by remember { derivedStateOf { m.users.map { it.user } } }
val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } }
UserProfilesView(
users = users,
filteredUsers = filteredUsers,
profileHidden = profileHidden,
searchTextOrPassword = searchTextOrPassword,
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
visibleUsersCount = visibleUsersCount(m),
addUser = {
ModalManager.shared.showModalCloseable { close ->
CreateProfile(m, close)
@@ -36,38 +52,85 @@ fun UserProfilesView(m: ChatModel) {
},
activateUser = { user ->
withBGApi {
m.controller.changeActiveUser(user.userId)
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
}
},
removeUser = { user ->
val text = buildAnnotatedString {
append(generalGetString(R.string.users_delete_all_chats_deleted) + "\n\n" + generalGetString(R.string.users_delete_profile_for) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(user.displayName)
if (m.users.size > 1 && (user.hidden || visibleUsersCount(m) > 1)) {
val text = buildAnnotatedString {
append(generalGetString(R.string.users_delete_all_chats_deleted) + "\n\n" + generalGetString(R.string.users_delete_profile_for) + " ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(user.displayName)
}
append(":")
}
append(":")
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.users_delete_question),
text = text,
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, true, searchTextOrPassword.value.trim())
}) {
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, false, searchTextOrPassword.value.trim())
}
) {
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
}
}
}
)
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.cant_delete_user_profile),
text = if (m.users.size > 1) {
generalGetString(R.string.should_be_at_least_one_visible_profile)
} else {
generalGetString(R.string.should_be_at_least_one_profile)
}
)
}
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(R.string.users_delete_question),
text = text,
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, true)
}) {
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, false)
}
) {
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
},
unhideUser = { user ->
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 ->
withBGApi {
setUserPrivacy(m, onSuccess = {
if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert)
}) { m.controller.apiMuteUser(user.userId) }
}
},
unmuteUser = { user ->
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } }
},
showHiddenProfile = { user ->
ModalManager.shared.showModalCloseable(true) { close ->
HiddenProfileView(m, user) {
profileHidden.value = true
withBGApi {
delay(10_000)
profileHidden.value = false
}
close()
}
}
}
)
}
@@ -75,9 +138,18 @@ fun UserProfilesView(m: ChatModel) {
@Composable
private fun UserProfilesView(
users: List<User>,
filteredUsers: List<User>,
searchTextOrPassword: MutableState<String>,
profileHidden: MutableState<Boolean>,
visibleUsersCount: Int,
showHiddenProfilesNotice: SharedPreference<Boolean>,
addUser: () -> Unit,
activateUser: (User) -> Unit,
removeUser: (User) -> Unit,
unhideUser: (User) -> Unit,
muteUser: (User) -> Unit,
unmuteUser: (User) -> Unit,
showHiddenProfile: (User) -> Unit,
) {
Column(
Modifier
@@ -85,25 +157,59 @@ private fun UserProfilesView(
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
) {
if (profileHidden.value) {
SectionView {
SettingsActionItem(Icons.Outlined.LockOpen, stringResource(R.string.enter_password_to_show), click = {
profileHidden.value = false
}
)
}
SectionSpacer()
}
AppBarTitle(stringResource(R.string.your_chat_profiles))
SectionView {
for (user in users) {
UserView(user, users, activateUser, removeUser)
for (user in filteredUsers) {
UserView(user, users, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
SectionDivider()
}
SectionItemView(addUser, minHeight = 68.dp) {
Icon(Icons.Outlined.Add, stringResource(R.string.users_add), tint = MaterialTheme.colors.primary)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(stringResource(R.string.users_add), color = MaterialTheme.colors.primary)
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))
Text(stringResource(R.string.users_add), color = MaterialTheme.colors.primary)
}
}
}
SectionTextFooter(stringResource(R.string.tap_to_activate_profile))
LaunchedEffect(Unit) {
if (showHiddenProfilesNotice.state.value && users.size > 1) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.make_profile_private),
text = generalGetString(R.string.you_can_hide_or_mute_user_profile),
confirmText = generalGetString(R.string.ok),
dismissText = generalGetString(R.string.dont_show_again),
onDismiss = {
showHiddenProfilesNotice.set(false)
},
)
}
}
SectionTextFooter(stringResource(R.string.your_chat_profiles_stored_locally))
}
}
@Composable
private fun UserView(user: User, users: List<User>, activateUser: (User) -> Unit, removeUser: (User) -> Unit) {
private fun UserView(
user: User,
users: List<User>,
visibleUsersCount: Int,
activateUser: (User) -> Unit,
removeUser: (User) -> Unit,
unhideUser: (User) -> Unit,
muteUser: (User) -> Unit,
unmuteUser: (User) -> Unit,
showHiddenProfile: (User) -> Unit,
) {
var showDropdownMenu by remember { mutableStateOf(false) }
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
activateUser(user)
@@ -114,28 +220,172 @@ private fun UserView(user: User, users: List<User>, activateUser: (User) -> Unit
onDismissRequest = { showDropdownMenu = false },
Modifier.width(220.dp)
) {
if (user.hidden) {
ItemAction(stringResource(R.string.user_unhide), Icons.Outlined.LockOpen, onClick = {
showDropdownMenu = false
unhideUser(user)
})
} else {
if (visibleUsersCount > 1) {
ItemAction(stringResource(R.string.user_hide), Icons.Outlined.Lock, onClick = {
showDropdownMenu = false
showHiddenProfile(user)
})
}
if (user.showNtfs) {
ItemAction(stringResource(R.string.user_mute), Icons.Outlined.NotificationsOff, onClick = {
showDropdownMenu = false
muteUser(user)
})
} else {
ItemAction(stringResource(R.string.user_unmute), Icons.Outlined.Notifications, onClick = {
showDropdownMenu = false
unmuteUser(user)
})
}
}
ItemAction(stringResource(R.string.delete_verb), Icons.Outlined.Delete, color = Color.Red, onClick = {
removeUser(user)
showDropdownMenu = false
}
)
})
}
}
}
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean) {
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.hidden) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) {
true
} else {
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.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 (passwordEntryRequired(user, searchTextOrPassword)) {
ModalManager.shared.showModalCloseable(true) { close ->
ProfileActionView(UserProfileAction.DELETE, user) { pwd ->
withBGApi {
doRemoveUser(m, user, users, delSMPQueues, pwd)
close()
}
}
}
} else {
withBGApi { doRemoveUser(m, user, users, delSMPQueues, userViewPassword(user, searchTextOrPassword.trim())) }
}
}
private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, viewPwd: String?) {
if (users.size < 2) return
withBGApi {
try {
if (user.activeUser) {
val newActive = users.first { !it.activeUser }
m.controller.changeActiveUser_(newActive.userId)
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))
}
m.controller.apiDeleteUser(user.userId, delSMPQueues)
m.users.removeAll { it.user.userId == user.userId }
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
} 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()
)
}
}
private fun showMuteProfileAlert(showMuteProfileAlert: SharedPreference<Boolean>) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.muted_when_inactive),
text = generalGetString(R.string.you_will_still_receive_calls_and_ntfs),
confirmText = generalGetString(R.string.ok),
dismissText = generalGetString(R.string.dont_show_again),
onDismiss = {
showMuteProfileAlert.set(false)
},
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 43 KiB

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,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="accept_contact_button">اقبل</string>
<string name="about_simplex_chat">عن <xliff:g id="appNameFull"> ٍSimpleX </xliff:g></string>
<string name="a_plus_b">a + b</string>
<string name="accept">اقبل</string>
<string name="chat_item_ttl_week">اسبوع 1</string>
<string name="chat_item_ttl_month">شهر 1</string>
<string name="color_primary">لون تمييزي</string>
<string name="chat_item_ttl_day">يوم 1</string>
<string name="accept_feature">اقبل</string>
<string name="about_simplex">عن SimpleX</string>
<string name="above_then_preposition_continuation">أعلاه ، ثم:</string>
<string name="accept_call_on_lock_screen">اقبل</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string>
<string name="alert_message_no_group">هذه المجموعة لم تعد موجودة.</string>
<string name="this_QR_code_is_not_a_link">رمز QR هذا ليس رابطًا!</string>
<string name="next_generation_of_private_messaging">الجيل القادم من الرسائل الخاصة</string>
<string name="delete_files_and_media_desc">لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة.</string>
<string name="enable_automatic_deletion_message">لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق.</string>
<string name="messages_section_description">ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string>
<string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات الاتصال الخاصة بك فقط.</string>
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string>
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيتلقى العضو دعوة جديدة.</string>
<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>

File diff suppressed because it is too large Load Diff

View File

@@ -513,13 +513,15 @@
<!-- Call settings -->
<string name="settings_audio_video_calls">Audio- &amp; Videoanrufe</string>
<string name="your_calls">Ihre Anrufe</string>
<string name="connect_calls_via_relay">Über ein Relais verbinden</string>
<string name="always_use_relay">Über ein Relais verbinden</string>
<string name="call_on_lock_screen">Anrufe auf Sperrbildschirm:</string>
<string name="accept_call_on_lock_screen">Akzeptieren</string>
<string name="show_call_on_lock_screen">Anzeigen</string>
<string name="no_call_on_lock_screen">Deaktivieren</string>
<string name="your_ice_servers">Ihre ICE-Server</string>
<string name="webrtc_ice_servers">WebRTC ICE-Server</string>
<string name="relay_server_protects_ip">Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen.</string>
<string name="relay_server_if_necessary">Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden.</string>
<!-- Call Lock Screen -->
<string name="open_simplex_chat_to_accept_call">Öffnen Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g>, um den Anruf anzunehmen.</string>
<string name="allow_accepting_calls_from_lock_screen">Aktivieren Sie Anrufe vom Sperrbildschirm über die Einstellungen.</string>
@@ -763,7 +765,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>
@@ -1021,7 +1023,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>
@@ -1040,4 +1041,95 @@
<string name="v4_5_italian_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="v4_4_french_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="v4_5_private_filenames_descr">Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen.</string>
<string name="moderated_description">Moderiert</string>
<string name="moderated_item_description">Von %s moderiert</string>
<string name="moderate_verb">Moderieren</string>
<string name="moderate_message_will_be_marked_warning">Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet.</string>
<string name="you_are_observer">Sie sind Beobachter</string>
<string name="observer_cant_send_message_title">Sie können keine Nachrichten versenden!</string>
<string name="group_member_role_observer">Beobachter</string>
<string name="initial_member_role">Anfängliche Rolle</string>
<string name="delete_member_message__question">Nachricht des Mitglieds löschen\?</string>
<string name="error_updating_link_for_group">Fehler beim Aktualisieren des Gruppen-Links</string>
<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">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">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">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>
<string name="hidden_profile_password">Verborgenes Profil-Passwort</string>
<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">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="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>
<string name="muted_when_inactive">Bei Inaktivität stummgeschaltet!</string>
<string name="v4_6_hidden_chat_profiles_descr">Schützen Sie Ihre Chat-Profile mit einem Passwort!</string>
<string name="v4_6_audio_video_calls_descr">Bluetooth-Unterstützung und weitere Verbesserungen.</string>
<string name="v4_6_group_moderation_descr">Administratoren können nun
\n- Nachrichten von Gruppenmitgliedern löschen
\n- Gruppenmitglieder deaktivieren (\"Beobachter\"-Rolle)</string>
<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">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">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="settings_send_files_via_xftp">Videos und Dateien per XFTP versenden</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">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-Herabstufung</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="xftp_requires_v461">Für den Empfang per XFTP wird v4.6.1 oder neuer benötigt.</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>
</resources>

File diff suppressed because it is too large Load Diff

View File

@@ -598,12 +598,14 @@
<string name="incoming_audio_call">Appel audio entrant</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> veut se connecter à vous via</string>
<string name="your_calls">Vos appels</string>
<string name="connect_calls_via_relay">Se connecter via relais</string>
<string name="always_use_relay">Se connecter via relais</string>
<string name="call_on_lock_screen">Appels en écran verrouillé :</string>
<string name="show_call_on_lock_screen">Montrer</string>
<string name="no_call_on_lock_screen">Désactiver</string>
<string name="your_ice_servers">Vos serveurs ICE</string>
<string name="webrtc_ice_servers">Serveurs WebRTC ICE</string>
<string name="relay_server_protects_ip">Le serveur relais protège votre adresse IP, mais il peut observer la durée de l\'appel.</string>
<string name="relay_server_if_necessary">Le serveur relais n\'est utilisé que si nécessaire. Un tiers peut observer votre adresse IP.</string>
<string name="open_simplex_chat_to_accept_call">Ouvrez <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour décrocher</string>
<string name="status_no_e2e_encryption">sans chiffrement de bout en bout</string>
<string name="status_contact_has_e2e_encryption">Ce contact a le chiffrement de bout en bout</string>
@@ -742,7 +744,7 @@
<string name="member_role_will_be_changed_with_notification">Le rôle sera changé pour «%s». Les membres du groupe seront notifiés.</string>
<string name="icon_descr_contact_checked">Contact vérifié⸱e</string>
<string name="clear_contacts_selection_button">Effacer</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contact·s sélectionné·e·s</string>
<string name="num_contacts_selected">%d contact·s sélectionné·e·s</string>
<string name="skip_inviting_button">Passer linvitation de membres</string>
<string name="select_contacts">Sélectionnez des contacts</string>
<string name="no_contacts_selected">Aucun contact sélectionné</string>
@@ -940,7 +942,6 @@
<string name="users_delete_with_connections">Profil et connexions au serveur</string>
<string name="network_session_mode_transport_isolation">Isolement du transport</string>
<string name="update_network_session_mode_question">Mettre à jour le mode d\'isolation du transport \?</string>
<string name="your_chat_profiles_stored_locally">Vos profils de chat sont stockés localement, uniquement sur votre appareil</string>
<string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>.
\n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.</string>
<string name="network_session_mode_user">Profil de chat</string>
@@ -966,4 +967,86 @@
<string name="v4_5_private_filenames_descr">Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC.</string>
<string name="v4_5_transport_isolation">Isolation du transport</string>
<string name="v4_5_multiple_chat_profiles_descr">Différents noms, avatars et mode d\'isolation de transport.</string>
<string name="moderated_description">modéré</string>
<string name="moderated_item_description">modéré par %s</string>
<string name="delete_member_message__question">Supprimer le message de ce membre \?</string>
<string name="moderate_verb">Modéré</string>
<string name="moderate_message_will_be_deleted_warning">Le message sera supprimé pour tous les membres.</string>
<string name="moderate_message_will_be_marked_warning">Le message sera marqué comme modéré pour tous les membres.</string>
<string name="you_are_observer">vous êtes observateur</string>
<string name="error_updating_link_for_group">Erreur lors de la mise à jour du lien de groupe</string>
<string name="initial_member_role">Rôle initial</string>
<string name="observer_cant_send_message_desc">Veuillez contacter l\'administrateur du groupe.</string>
<string name="observer_cant_send_message_title">Vous ne pouvez pas envoyer de messages !</string>
<string name="group_member_role_observer">observateur</string>
<string name="language_system">Système</string>
<string name="smp_save_servers_question">Sauvegarder les serveurs \?</string>
<string name="dont_show_again">Ne plus afficher</string>
<string name="button_add_welcome_message">Ajouter un message d\'accueil</string>
<string name="cant_delete_user_profile">Impossible de supprimer le profil d\'utilisateur !</string>
<string name="v4_6_group_moderation">Modération de groupe</string>
<string name="user_hide">Cacher</string>
<string name="muted_when_inactive">Mute en cas d\'inactivité !</string>
<string name="confirm_password">Confirmer le mot de passe</string>
<string name="v4_6_reduced_battery_usage">Réduction accrue de l\'utilisation de la batterie</string>
<string name="v4_6_chinese_spanish_interface">Interface en chinois et en espagnol</string>
<string name="enter_password_to_show">Entrez le mot de passe dans le champ de recherche</string>
<string name="v4_6_audio_video_calls">Appels audio et vidéo</string>
<string name="v4_6_group_welcome_message">Message d\'accueil du groupe</string>
<string name="error_saving_user_password">Erreur d\'enregistrement du mot de passe de l\'utilisateur</string>
<string name="error_updating_user_privacy">Erreur de mise à jour de la confidentialité de l\'utilisateur</string>
<string name="hidden_profile_password">Mot de passe de profil caché</string>
<string name="v4_6_hidden_chat_profiles">Profils de chat cachés</string>
<string name="v4_6_group_moderation_descr">Désormais, les administrateurs peuvent :
\n- supprimer les messages des membres.
\n- désactiver des membres (rôle \"observateur\")</string>
<string name="save_welcome_message_question">Sauvegarder le message d\'accueil \?</string>
<string name="v4_6_group_welcome_message_descr">Choisissez un message à l\'attention des nouveaux membres !</string>
<string name="hide_profile">Masquer le profil</string>
<string name="v4_6_reduced_battery_usage_descr">D\'autres améliorations sont à venir !</string>
<string name="password_to_show">Mot de passe à afficher</string>
<string name="make_profile_private">Rendre un profil privé !</string>
<string name="user_mute">Mute</string>
<string name="v4_6_hidden_chat_profiles_descr">Protégez vos profils de chat par un mot de passe !</string>
<string name="tap_to_activate_profile">Appuyez pour activer le profil.</string>
<string name="save_and_update_group_profile">Sauvegarder et mettre à jour le profil du groupe</string>
<string name="save_profile_password">Enregistrer le mot de passe du profil</string>
<string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page Profils de chat.</string>
<string name="v4_6_audio_video_calls_descr">Prise en charge du Bluetooth et autres améliorations.</string>
<string name="v4_6_chinese_spanish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="should_be_at_least_one_profile">Il doit y avoir au moins un profil d\'utilisateur.</string>
<string name="should_be_at_least_one_visible_profile">Il doit y avoir au moins un profil d\'utilisateur visible.</string>
<string name="user_unhide">Dévoiler</string>
<string name="user_unmute">Démute</string>
<string name="button_welcome_message">Message d\'accueil</string>
<string name="group_welcome_title">Message d\'accueil</string>
<string name="you_can_hide_or_mute_user_profile">Vous pouvez masquer ou mettre en sourdine un profil d\'utilisateur - maintenez-le enfoncé pour accéder au menu.</string>
<string name="you_will_still_receive_calls_and_ntfs">Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu\'ils sont actifs.</string>
<string name="settings_send_files_via_xftp">Envoi de fichiers via XFTP</string>
<string name="database_downgrade">Rétrogradation de la base de données</string>
<string name="database_upgrade">Mise à niveau de la base de données</string>
<string name="incompatible_database_version">Version de la base de données incompatible</string>
<string name="downgrade_and_open_chat">Rétrograder et ouvrir le chat</string>
<string name="invalid_migration_confirmation">Confirmation de migration invalide</string>
<string name="upgrade_and_open_chat">Mettre à niveau et ouvrir le chat</string>
<string name="database_migrations">Migrations : %s</string>
<string name="database_downgrade_warning">Attention : vous risquez de perdre des données !</string>
<string name="confirm_database_upgrades">Confirmer la mise à niveau de la base de données</string>
<string name="mtr_error_no_down_migration">la base de données a une version plus récente que celle de l\'application, mais il n\'y a pas de rétrogradation pour : %s</string>
<string name="mtr_error_different">migration différente dans l\'app/la base de données : %s / %s</string>
<string name="image_will_be_received_when_contact_completes_uploading">L\'image sera reçue lorsque votre contact aura terminé de la mettre en ligne.</string>
<string name="show_dev_options">Afficher :</string>
<string name="show_developer_options">Afficher les options pour les développeurs</string>
<string name="file_will_be_received_when_contact_completes_uploading">Le fichier sera reçu lorsque votre contact aura terminé de le mettre en ligne.</string>
<string name="xftp_requires_v461">v4.6.1+ nécessaire pour la réception via XFTP.</string>
<string name="developer_options">IDs de base de données et option d\'isolation du transport.</string>
<string name="settings_section_title_experimenta">EXPÉRIMENTALE</string>
<string name="hide_dev_options">Cacher :</string>
<string name="unhide_chat_profile">Dévoiler le profil de chat</string>
<string name="unhide_profile">Dévoiler le profil</string>
<string name="delete_chat_profile">Supprimer le profil de chat</string>
<string name="delete_profile">Supprimer le profil</string>
<string name="cancel_file__question">Annuler le transfert de fichiers \?</string>
<string name="file_transfer_will_be_cancelled_warning">Le transfert de fichiers sera annulé. S\'il est en cours, il sera interrompu.</string>
<string name="profile_password">Mot de passe de profil</string>
</resources>

View File

@@ -97,7 +97,7 @@
<string name="invite_to_group_button">समूह में आमंत्रित करें</string>
<string name="message_deletion_prohibited_in_chat">इस समूह में अपरिवर्तनीय संदेश हटाना प्रतिबंधित है।</string>
<string name="italic">तिरछा</string>
<string name="join_group_button">जोड़ना</string>
<string name="join_group_button">प्रवेश लेना</string>
<string name="join_group_incognito_button">गुप्त में शामिल हों</string>
<string name="joining_group">समूह में शामिल होना</string>
<string name="join_group_question">समूह में शामिल हों\?</string>
@@ -107,7 +107,7 @@
<string name="leave_group_question">समूह छोड़ दें</string>
<string name="rcv_group_event_member_left">बाएं</string>
<string name="group_member_status_left">बाएं</string>
<string name="theme_light">रोशनी</string>
<string name="theme_light">प्रकाश</string>
<string name="info_row_local_name">स्थानीय नाम</string>
<string name="users_delete_data_only">केवल स्थानीय प्रोफ़ाइल डेटा</string>
<string name="auth_log_in_using_credential">अपने क्रेडेंशियल का उपयोग करके लॉग इन करें</string>
@@ -121,7 +121,7 @@
<string name="network_status">नेटवर्क की स्थिति</string>
<string name="notification_new_contact_request">नया संपर्क अनुरोध</string>
<string name="delete_files_and_media_all">सभी फाइलों को मिटा दें</string>
<string name="delete_archive">संग्रह हटाएं</string>
<string name="delete_archive">संग्रह हटा देना</string>
<string name="new_database_archive">नया डेटाबेस संग्रह</string>
<string name="new_member_role">नए सदस्य की भूमिका</string>
<string name="settings_notifications_mode_title">अधिसूचना सेवा</string>
@@ -133,7 +133,7 @@
<string name="settings_notification_preview_title">अधिसूचना पूर्वावलोकन</string>
<string name="notifications">सूचनाएं</string>
<string name="full_deletion">सभी के लिए हटाएं</string>
<string name="delete_chat_archive_question">चैट संग्रह मिटाएं\?</string>
<string name="delete_chat_archive_question">लिखचीत संग्रह हटा दे\?</string>
<string name="delete_chat_profile_question">चैट प्रोफ़ाइल हटाएं\?</string>
<string name="users_delete_question">चैट प्रोफ़ाइल हटाएं\?</string>
<string name="users_delete_profile_for">के लिए चैट प्रोफ़ाइल हटाएं</string>
@@ -144,4 +144,40 @@
<string name="delete_image">छवि हटाएं</string>
<string name="button_delete_group">समूह हटाएं</string>
<string name="for_me_only">मेरे लिए हटाएं</string>
</resources>
<string name="hide_verb">छिपाना</string>
<string name="both_you_and_your_contacts_can_delete">आप और आपका संपर्क दोनों भेजे गए संदेशों को अपरिवर्तनीय रूप से हटा सकते हैं।</string>
<string name="message_deletion_prohibited">इस चैट में अपरिवर्तनीय संदेश हटाना प्रतिबंधित है।</string>
<string name="chat_with_the_founder">प्रश्न और विचार भेजें</string>
<string name="send_us_an_email">हमें ईमेल भेजें</string>
<string name="display_name">प्रदर्शित होने वाला नाम</string>
<string name="confirm_verb">पुष्टि करना</string>
<string name="you_accepted_connection">आपने कनेक्शन स्वीकार किया</string>
<string name="contribute">योगदान देना</string>
<string name="both_you_and_your_contact_can_send_disappearing">आप और आपका संपर्क दोनों गायब होने वाले संदेश भेज सकते हैं।</string>
<string name="allow_to_send_disappearing">गायब होने वाले संदेश भेजने की अनुमति दें।</string>
<string name="prohibit_sending_disappearing">गायब होने वाले संदेश भेजने पर रोक लगाएं।</string>
<string name="how_to_use_simplex_chat">इसका उपयोग कैसे करना है</string>
<string name="group_member_status_group_deleted">समूह हटाया गया</string>
<string name="delete_message_cannot_be_undone_warning">संदेश हटा दिया जाएगा - इसे पूर्ववत नहीं किया जा सकता!</string>
<string name="message_delivery_error_title">संदेश वितरण त्रुटि</string>
<string name="group_display_name_field">समूह प्रदर्शन नाम:</string>
<string name="group_full_name_field">समूह का पूरा नाम:</string>
<string name="group_invitation_expired">समूह आमंत्रण समाप्त हो गया</string>
<string name="v4_3_irreversible_message_deletion_desc">आपके संपर्क पूर्ण संदेश को हटाने की अनुमति दे सकते हैं।</string>
<string name="new_in_version">%s में नया</string>
<string name="icon_descr_group_inactive">समूह निष्क्रिय</string>
<string name="delete_group_menu_action">मिटाना</string>
<string name="delete_contact_menu_action">मिटाना</string>
<string name="create_your_profile">अपना प्रोफ़ाइल बनाए</string>
<string name="set_contact_name">संपर्क नाम सेट करें</string>
<string name="icon_descr_email">ईमेल</string>
<string name="callstate_received_answer">जवाब मिला…</string>
<string name="use_chat">चैट का प्रयोग करें</string>
<string name="error_with_info">गलती: %s</string>
<string name="delete_messages_after">संदेशों को बाद में हटाएं</string>
<string name="image_descr">छवि</string>
<string name="icon_descr_server_status_error">गलती</string>
<string name="restore_database_alert_confirm">वापस लौटाना</string>
<string name="a_plus_b">ए + बी</string>
<string name="incognito_random_profile">आपका यादृच्छिक प्रोफ़ाइल</string>
</resources>

View File

@@ -378,7 +378,7 @@
<string name="icon_descr_call_progress">Chiamata in corso</string>
<string name="call_on_lock_screen">Chiamate sulla schermata di blocco:</string>
<string name="icon_descr_call_connecting">Connessione chiamata</string>
<string name="connect_calls_via_relay">Connetti via relay</string>
<string name="always_use_relay">Connetti via relay</string>
<string name="status_contact_has_e2e_encryption">il contatto ha la crittografia e2e</string>
<string name="status_contact_has_no_e2e_encryption">il contatto non ha la crittografia e2e</string>
<string name="no_call_on_lock_screen">Disattiva</string>
@@ -613,7 +613,7 @@
<string name="your_contact_address">Il tuo indirizzo di contatto</string>
<string name="smp_servers_your_server">Il tuo server</string>
<string name="smp_servers_your_server_address">L\'indirizzo del tuo server</string>
<string name="your_simplex_contact_address">Il tuo indirizzo di contatto di <xliff:g id="appName">SimpleX</xliff:g>.</string>
<string name="your_simplex_contact_address">Il tuo indirizzo di contatto di <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="network_disable_socks_info">Se confermi, i server di messaggistica saranno in grado di vedere il tuo indirizzo IP e il tuo fornitore, a quali server ti stai connettendo.</string>
<string name="install_simplex_chat_for_terminal">Installa <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per terminale</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi.</string>
@@ -718,6 +718,8 @@
<string name="icon_descr_video_off">Video off</string>
<string name="icon_descr_video_on">Video on</string>
<string name="webrtc_ice_servers">Server WebRTC ICE</string>
<string name="relay_server_protects_ip">Il server relay protegge il tuo indirizzo IP, ma può osservare la durata della chiamata.</string>
<string name="relay_server_if_necessary">Il server relay viene usato solo se necessario. Un altro utente può osservare il tuo indirizzo IP.</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> messaggio/i saltato/i</string>
<string name="your_calls">Le tue chiamate</string>
<string name="your_ice_servers">I tuoi server ICE</string>
@@ -817,7 +819,7 @@
<string name="button_send_direct_message">Invia messaggio diretto</string>
<string name="skip_inviting_button">Salta l\'invito di membri</string>
<string name="switch_verb">Cambia</string>
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contatto/i selezionato/i</string>
<string name="num_contacts_selected">%d contatto/i selezionato/i</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBRI</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini.</string>
<string name="invite_prohibited_description">Stai tentando di invitare un contatto con cui hai condiviso un profilo in incognito nel gruppo in cui stai usando il tuo profilo principale</string>
@@ -946,7 +948,6 @@
<string name="delete_files_and_media_for_all_users">Elimina i file per tutti i profili di chat</string>
<string name="failed_to_active_user_title">Errore nel cambio di profilo!</string>
<string name="failed_to_create_user_title">Errore nella creazione del profilo!</string>
<string name="your_chat_profiles_stored_locally">I tuoi profili di chat sono memorizzati localmente, solo sul tuo dispositivo</string>
<string name="error_deleting_user">Errore nell\'eliminazione del profilo utente</string>
<string name="users_delete_with_connections">Profilo e connessioni al server</string>
<string name="your_chat_profiles">I tuoi profili di chat</string>
@@ -966,4 +967,86 @@
<string name="v4_5_transport_isolation">Isolamento del trasporto</string>
<string name="v4_5_private_filenames_descr">Per proteggere il fuso orario, i file immagine/vocali usano UTC.</string>
<string name="v4_5_multiple_chat_profiles_descr">Nomi e avatar diversi, isolamento del trasporto.</string>
<string name="moderated_description">moderato</string>
<string name="moderated_item_description">moderato da %s</string>
<string name="delete_member_message__question">Eliminare il messaggio del membro\?</string>
<string name="moderate_verb">Modera</string>
<string name="moderate_message_will_be_deleted_warning">Il messaggio verrà eliminato per tutti i membri.</string>
<string name="moderate_message_will_be_marked_warning">Il messaggio sarà segnato come moderato per tutti i membri.</string>
<string name="you_are_observer">sei un osservatore</string>
<string name="initial_member_role">Ruolo iniziale</string>
<string name="error_updating_link_for_group">Errore nell\'aggiornamento del link del gruppo</string>
<string name="group_member_role_observer">osservatore</string>
<string name="observer_cant_send_message_desc">Contatta l\'amministratore del gruppo.</string>
<string name="observer_cant_send_message_title">Non puoi inviare messaggi!</string>
<string name="language_system">Sistema</string>
<string name="button_add_welcome_message">Aggiungi messaggio di benvenuto</string>
<string name="button_welcome_message">Messaggio di benvenuto</string>
<string name="save_welcome_message_question">Salvare il messaggio di benvenuto\?</string>
<string name="group_welcome_title">Messaggio di benvenuto</string>
<string name="save_and_update_group_profile">Salva e aggiorna il profilo del gruppo</string>
<string name="enter_password_to_show">Inserisci password nella ricerca</string>
<string name="user_mute">Silenzia</string>
<string name="tap_to_activate_profile">Tocca per attivare il profilo.</string>
<string name="user_unhide">Svela</string>
<string name="make_profile_private">Rendi privato il profilo!</string>
<string name="should_be_at_least_one_profile">Deve esserci almeno un profilo utente.</string>
<string name="should_be_at_least_one_visible_profile">Deve esserci almeno un profilo utente visibile.</string>
<string name="you_can_hide_or_mute_user_profile">Puoi nascondere o silenziare un profilo utente - tienilo premuto per il menu.</string>
<string name="dont_show_again">Non mostrare più</string>
<string name="muted_when_inactive">Silenzioso quando inattivo!</string>
<string name="you_will_still_receive_calls_and_ntfs">Continuerai a ricevere chiamate e notifiche da profili silenziati quando sono attivi.</string>
<string name="v4_6_audio_video_calls">Chiamate audio e video</string>
<string name="v4_6_group_moderation">Moderazione del gruppo</string>
<string name="v4_6_hidden_chat_profiles_descr">Proteggi i tuoi profili di chat con una password!</string>
<string name="v4_6_audio_video_calls_descr">Supporto a bluetooth e altri miglioramenti.</string>
<string name="v4_6_group_welcome_message">Messaggio di benvenuto del gruppo</string>
<string name="v4_6_reduced_battery_usage_descr">Altri miglioramenti sono in arrivo!</string>
<string name="v4_6_chinese_spanish_interface">Interfaccia cinese e spagnola</string>
<string name="v4_6_chinese_spanish_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="hidden_profile_password">Password del profilo nascosta</string>
<string name="save_profile_password">Salva la password del profilo</string>
<string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina \"I tuoi profili di chat\".</string>
<string name="password_to_show">Password per mostrare</string>
<string name="v4_6_group_moderation_descr">Ora gli amministratori possono:
\n- eliminare i messaggi dei membri.
\n- disattivare i membri (ruolo \"osservatore\")</string>
<string name="cant_delete_user_profile">Impossibile eliminare il profilo utente!</string>
<string name="hide_profile">Nascondi il profilo</string>
<string name="confirm_password">Conferma password</string>
<string name="error_updating_user_privacy">Errore nell\'aggiornamento della privacy dell\'utente</string>
<string name="smp_save_servers_question">Salvare i server\?</string>
<string name="error_saving_user_password">Errore nel salvataggio della password utente</string>
<string name="v4_6_reduced_battery_usage">Ulteriore riduzione del consumo della batteria</string>
<string name="v4_6_hidden_chat_profiles">Profili di chat nascosti</string>
<string name="user_hide">Nascondi</string>
<string name="v4_6_group_welcome_message_descr">Imposta il messaggio mostrato ai nuovi membri!</string>
<string name="user_unmute">Riattiva audio</string>
<string name="settings_send_files_via_xftp">Invia file via XFTP</string>
<string name="database_downgrade">Downgrade del database</string>
<string name="database_upgrade">Aggiornamento del database</string>
<string name="incompatible_database_version">Versione del database incompatibile</string>
<string name="upgrade_and_open_chat">Aggiorna e apri chat</string>
<string name="developer_options">ID del database e opzione isolamento del trasporto.</string>
<string name="hide_dev_options">Nascondi:</string>
<string name="show_dev_options">Mostra:</string>
<string name="show_developer_options">Mostra opzioni sviluppatore</string>
<string name="xftp_requires_v461">v4.6.1+ necessaria per ricevere via XFTP.</string>
<string name="downgrade_and_open_chat">Esegui downgrade e apri chat</string>
<string name="database_migrations">Migrazioni: %s</string>
<string name="database_downgrade_warning">Attenzione: potresti perdere alcuni dati!</string>
<string name="confirm_database_upgrades">Conferma aggiornamenti database</string>
<string name="mtr_error_different">migrazione diversa nell\'app/nel database: %s / %s</string>
<string name="invalid_migration_confirmation">Conferma di migrazione non valida</string>
<string name="settings_section_title_experimenta">SPERIMENTALE</string>
<string name="image_will_be_received_when_contact_completes_uploading">L\'immagine verrà ricevuta quando il tuo contatto completerà l\'invio.</string>
<string name="mtr_error_no_down_migration">la versione del database è più recente di quella dell\'app, ma nessuna migrazione downgrade per: %s</string>
<string name="file_will_be_received_when_contact_completes_uploading">Il file verrà ricevuto quando il tuo contatto completerà l\'invio.</string>
<string name="cancel_file__question">Annullare il trasferimento di file\?</string>
<string name="file_transfer_will_be_cancelled_warning">Il trasferimento di file verrà annullato. Se è in corso, verrà interrotto.</string>
<string name="unhide_chat_profile">Svela il profilo chat</string>
<string name="unhide_profile">Svela profilo</string>
<string name="delete_chat_profile">Elimina il profilo di chat</string>
<string name="delete_profile">Elimina profilo</string>
<string name="profile_password">Password del profilo</string>
</resources>

View File

@@ -83,4 +83,881 @@
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>設定メニューにてオフにできます。</b> アプリがアクティブ時に通知が出ます。</string>
<string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QRコードを読み込み</b>連絡相手のQRコードをスキャンすると繋がります。</string>
<string name="chat_archive_section">チャットのアーカイブ</string>
<string name="delete_chat_archive_question">チャットのアーカイブを削除しますか?</string>
<string name="join_group_incognito_button">シークレットモードで参加</string>
<string name="group_member_status_intro_invitation">接続待ち (招待)</string>
<string name="group_member_status_accepted">接続待ち (承諾済み)</string>
<string name="group_member_status_creator">作成者</string>
<string name="no_contacts_selected">連絡先が選択されてません</string>
<string name="delete_link_question">リンクを削除しますか?</string>
<string name="section_title_for_console">コンソール</string>
<string name="info_row_local_name">ローカルネーム</string>
<string name="member_info_section_title_member">メンバー</string>
<string name="network_options_reset_to_defaults">既定に戻す</string>
<string name="network_option_ping_count">PING回数</string>
<string name="users_delete_profile_for">チャットのプロフィールを削除</string>
<string name="reset_color">既定色に戻す</string>
<string name="group_preferences">グループの設定</string>
<string name="feature_off">オフ</string>
<string name="prohibit_sending_voice_messages">音声メッセージを使用禁止にする。</string>
<string name="only_you_can_send_voice">音声メッセージを送れるのはあなただけです。</string>
<string name="delete_after">次の期間が経ったら削除:</string>
<string name="ttl_sec">%d 秒</string>
<string name="ttl_m">%d分</string>
<string name="v4_2_group_links">グループのリンク</string>
<string name="thousand_abbreviation">k</string>
<string name="connect_via_link_verb">接続</string>
<string name="server_connected">接続中</string>
<string name="server_error">エラー</string>
<string name="server_connecting">接続待ち</string>
<string name="invalid_data">無効なデータ</string>
<string name="invalid_chat">無効なチャット</string>
<string name="live">ライブ (リアルタイム)</string>
<string name="marked_deleted_description">削除済みとマークされました。</string>
<string name="receiving_files_not_yet_supported">ファイルの受信機能はまだ対応されてません。</string>
<string name="display_name_invited_to_connect">招待済み</string>
<string name="failed_to_parse_chat_title">チャット読み込みに失敗</string>
<string name="failed_to_parse_chats_title">チャット読み込みに失敗</string>
<string name="contact_developers">アプリを更新し、開発者にご連絡ください。</string>
<string name="service_notifications">即時通知!</string>
<string name="service_notifications_disabled">即時通知が無効になってます!</string>
<string name="enter_passphrase_notification_title">暗証フレーズが必要</string>
<string name="notification_preview_mode_hidden">プライベート</string>
<string name="notification_preview_mode_contact">連絡先の名前</string>
<string name="notification_preview_somebody">連絡先が非表示:</string>
<string name="auth_open_chat_console">チャットのコンソールを開く</string>
<string name="delete_verb">削除する</string>
<string name="file_saved">ファイルを保存しました</string>
<string name="delete_contact_question">連絡先を削除しますか?</string>
<string name="connect_via_link_or_qr">リンク・QRコード経由で繋がる</string>
<string name="mark_code_verified">検証済みにマーク</string>
<string name="how_to">使い方</string>
<string name="network_use_onion_hosts_required">必須</string>
<string name="incoming_video_call">ビデオ通話着信</string>
<string name="paste_the_link_you_received">頂いたリンクを貼り付ける</string>
<string name="icon_descr_hang_up">通話を切る</string>
<string name="messages_section_title">メッセージ</string>
<string name="rcv_group_event_invited_via_your_group_link">グループリンク経由で招待された</string>
<string name="message_deletion_prohibited">このチャットではメッセージの完全削除が使用禁止です。</string>
<string name="incognito_info_allows">一つのチャットプロフィールで複数の匿名な接続を立ち上げ、それぞれのデータを別々に共通を防ぐことができます。</string>
<string name="onboarding_notifications_mode_subtitle">後でも設定で変えられます。</string>
<string name="error_changing_address">アドレス変更にエラー発生</string>
<string name="error_creating_link_for_group">グループリンク発行にエラー発生</string>
<string name="error_changing_role">役割変更にエラー発生</string>
<string name="error_exporting_chat_database">チャットデータベースのエキスポートにエラー発生</string>
<string name="error_saving_smp_servers">SMPサーバ保存にエラー発生</string>
<string name="error_saving_ICE_servers">ICEサーバ保存にエラー発生</string>
<string name="error_setting_network_config">ネットワーク設定の更新にエラー発生</string>
<string name="icon_descr_file">ファイル</string>
<string name="simplex_link_mode_full">フルリンク</string>
<string name="group_full_name_field">グループのフルネーム:</string>
<string name="group_invitation_expired">グループ招待が期限切れ</string>
<string name="icon_descr_group_inactive">非アクティブなグループ</string>
<string name="group_members_can_send_voice">グループのメンバーが音声メッセージを送信できます。</string>
<string name="group_members_can_delete">グループのメンバーがメッセージを完全削除することができます。</string>
<string name="group_profile_is_stored_on_members_devices">グループのプロフィールはサーバではなく、メンバーの端末に保存されます。</string>
<string name="snd_group_event_group_profile_updated">グループのプロフィールが更新されました。</string>
<string name="notification_display_mode_hidden_desc">連絡先とメッセージ内容をプライベートにする。</string>
<string name="icon_descr_instant_notifications">即時通知</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">SMPサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICEサーバのアドレスを正しく1行ずつに分けて、重複しないように、形式もご確認ください。</string>
<string name="many_people_asked_how_can_it_deliver">よく聞かれるのは、 <i>if <xliff:g id="appName">SimpleX</xliff:g> にユーザIDがなければ、メッセージをどうやって届けるのでしょうかと。</i></string>
<string name="how_simplex_works"><xliff:g id="appName">SimpleX</xliff:g> の仕様</string>
<string name="icon_descr_call_progress">通話中</string>
<string name="onboarding_notifications_mode_service_desc"><b>電池消費がより高い</b>!非アクティブ時でもバックグラウンドのサービスが常に稼働します(着信次第に通知がすぐに出ます)。</string>
<string name="callstatus_calling">発信中</string>
<string name="callstatus_ended">通話終了 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="icon_descr_call_ended">通話が終了しました。</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">直接会えない時は、 <b>ビデオ通話中にQRコードを見せてもらうか</b>、招待リンクを送ってもらえば相手に繋がります。</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">メンバーをグループから除名する (※元に戻せません※)</string>
<string name="message_delivery_error_title">メッセージ送信エラー</string>
<string name="call_on_lock_screen">通話をロック画面に表示</string>
<string name="icon_descr_cancel_link_preview">リンクのプレビューを中止</string>
<string name="icon_descr_cancel_live_message">ライブメッセージを中止</string>
<string name="change_member_role_question">グループの役割を変えますか?</string>
<string name="cannot_receive_file">ファイル受信ができません。</string>
<string name="invite_prohibited">連絡先を招待できません!</string>
<string name="change_verb">変更</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">直接会えない時は、 <b>ビデオ通話にてQRコードを見せるか</b>、招待リンクを送って、相手に繋がります。</string>
<string name="rcv_conn_event_switch_queue_phase_completed">あなたのアドレスを変えました。</string>
<string name="snd_conn_event_switch_queue_phase_changing">アドレスを変更しています…</string>
<string name="chat_database_section">チャットのデータベース</string>
<string name="chat_is_stopped_indication">チャットが停止してます。</string>
<string name="network_disable_socks_info">承諾すると、メッセージのサーバがIPアドレス、ISP、接続サーバを特定できます。</string>
<string name="chat_preferences">チャット設定</string>
<string name="clear_chat_button">消す</string>
<string name="icon_descr_close_button">閉じるボタン</string>
<string name="clear_verification">検証を消す</string>
<string name="image_saved">画像をギャラリーに保存しました。</string>
<string name="choose_file">ファイルを選ぶ</string>
<string name="clear_chat_menu_action">消す</string>
<string name="image_will_be_received_when_contact_is_online">連絡先がオンラインになったら受信されます。しばらくお待ちください。</string>
<string name="v4_4_verify_connection_security_desc">連絡先とセキュリティコードを確認</string>
<string name="install_simplex_chat_for_terminal">ターミナル版の <xliff:g id="appNameFull">SimpleX Chat</xliff:g> をインストール</string>
<string name="v4_3_improved_server_configuration">サーバ設定の向上</string>
<string name="v4_3_improved_privacy_and_security">プライバシーとセキュリティ強化</string>
<string name="settings_section_title_incognito">シークレットモード</string>
<string name="group_unsupported_incognito_main_profile_sent">ここではシークレットモードが無効です。メインのプロフィールがグループのメンバーに送られます。</string>
<string name="new_in_version">%s バージョンアップで新しい</string>
<string name="new_passphrase">新しい暗証フレーズ</string>
<string name="chat_item_ttl_none">一度も</string>
<string name="notification_preview_new_message">新しいメッセージ</string>
<string name="network_use_onion_hosts_no">いいえ</string>
<string name="notifications">通知</string>
<string name="chat_preferences_off">オフ</string>
<string name="callstate_connecting">接続待ち…</string>
<string name="notification_contact_connected">接続中</string>
<string name="contact_connection_pending">接続待ち…</string>
<string name="display_name_connection_established">接続済み</string>
<string name="connect_via_link">リンク経由で繋がる。</string>
<string name="connection_error">接続エラー</string>
<string name="network_use_onion_hosts_required_desc_in_alert">接続にオニオンのホストが必要となります。</string>
<string name="group_member_status_introduced">接続待ち (紹介済み)</string>
<string name="connection_error_auth">接続エラー (AUTH)</string>
<string name="connection_timeout">接続タイムアウト</string>
<string name="connection_request_sent">接続リクエストを送信しました!</string>
<string name="connection_local_display_name">接続 <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="connect_via_contact_link">連絡先リンク経由で繋がりますか?</string>
<string name="connect_via_group_link">グループリンク経由で繋がりますか?</string>
<string name="connect_via_invitation_link">招待リンク経由で繋がりますか?</string>
<string name="status_contact_has_e2e_encryption">連絡先はエンドツーエンド暗号化があります。</string>
<string name="description_via_group_link_incognito">グループリンク経由でシークレットモード</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">連絡先と全メッセージが削除されます (※元に戻せません※)</string>
<string name="contact_preferences">連絡先の設定</string>
<string name="status_contact_has_no_e2e_encryption">連絡先はエンドツーエンド暗号化がありません。</string>
<string name="alert_title_contact_connection_pending">連絡先がまだ繋がってません!</string>
<string name="contact_requests">連絡先のリクエスト</string>
<string name="icon_descr_context">追加情報アイコン</string>
<string name="network_use_onion_hosts_prefer_desc">オニオンのホストが利用可能時に使われます。</string>
<string name="network_use_onion_hosts_no_desc">オニオンのホストが使われません。</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">オニオンのホストが利用可能時に使われます。</string>
<string name="images_limit_desc">画像を1回で最大10枚を送信できます。</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><b>2層エンドツーエンド暗号化</b>で送信されたプロフィール、連絡先、グループ、メッセージは、クライント端末にしか保存されません。</string>
<string name="only_group_owners_can_change_prefs">グループ設定を変えられるのはグループのオーナーだけです。</string>
<string name="only_group_owners_can_enable_voice">音声メッセージを利用可能に設定できるのはグループのオーナーだけです。</string>
<string name="create_profile">プロフィールを作成する</string>
<string name="only_you_can_delete_messages">メッセージの完全削除はあなたにしかできません (あなたの連絡先は削除対象とすることができます)。</string>
<string name="create_your_profile">プロフィールを作成する</string>
<string name="only_your_contact_can_delete">メッセージを完全削除できるのはあなたの連絡相手だけです (あなたは削除対象とすることができます)。</string>
<string name="open_chat">チャットを開く</string>
<string name="only_your_contact_can_send_voice">音声メッセージを送れるのはあなたの連絡相手だけです。</string>
<string name="database_encryption_will_be_updated">データベース暗号化の暗号フレーズが更新され、キーストア (暗証キー保管庫)に保存されます。</string>
<string name="database_error">データベースエラー</string>
<string name="set_password_to_export_desc">データベースはランダムな暗証フレーズで暗号化済みです。エキスポートする前に更新してください。</string>
<string name="passphrase_is_different">データベース暗証フレーズはキーストア (暗証キー保管庫) に保存された暗証フレーズと異なります。</string>
<string name="ttl_d">%d日</string>
<string name="ttl_day">%d 日</string>
<string name="delete_group_menu_action">削除</string>
<string name="incoming_audio_call">音声通話着信</string>
<string name="button_delete_contact">連絡先を削除</string>
<string name="open_simplex_chat_to_accept_call">通話を受けるには <xliff:g id="appNameFull">SimpleX Chat</xliff:g> を開きます。</string>
<string name="deleted_description">削除完了</string>
<string name="delete_files_and_media_for_all_users">全チャットプロフィールのファイルを削除</string>
<string name="full_deletion">全員分を削除</string>
<string name="for_me_only">自分側で削除</string>
<string name="delete_image">画像を削除</string>
<string name="group_invitation_item_description">グループへの招待 <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="invalid_connection_link">無効な繋がりリンク</string>
<string name="smp_servers_invalid_address">無効なサーバアドレス</string>
<string name="invalid_message_format">無効なメッセージのフォーマット</string>
<string name="simplex_link_mode_description">説明</string>
<string name="alert_title_group_invitation_expired">招待が期限切れました!</string>
<string name="smp_servers_delete_server">サーバを削除</string>
<string name="auth_device_authentication_is_disabled_turning_off">端末認証がオフです。SimpleXロックを解除します。</string>
<string name="direct_messages_are_prohibited_in_chat">このグループではメンバー間のダイレクトメッセージが使用禁止です。</string>
<string name="disappearing_messages_are_prohibited">このグループでは消えるメッセージが使用禁止です。</string>
<string name="ttl_min">%d 分</string>
<string name="ttl_weeks">%d 週</string>
<string name="network_option_enable_tcp_keep_alive">TCP keep-aliveを有効にする</string>
<string name="icon_descr_email">メール</string>
<string name="allow_accepting_calls_from_lock_screen">設定メニューでロック画面からの通話を有効にできます。</string>
<string name="people_can_connect_only_via_links_you_share">あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。</string>
<string name="icon_descr_profile_image_placeholder">仮のプロフィール画像</string>
<string name="image_descr_qr_code">QRコード</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">リンクが正しいかどうかご確認ください。または、連絡相手にもう一度リンクをお求めください。</string>
<string name="network_error_desc"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> との接続を確認し、もう一度お試しください。</string>
<string name="enter_correct_current_passphrase">現在の正しい暗証フレーズを入力してください。</string>
<string name="smp_servers_preset_server">プレセットサーバ</string>
<string name="smp_servers_preset_address">プレセットサーバのアドレス</string>
<string name="display_name_connecting">接続待ち…</string>
<string name="description_via_contact_address_link_incognito">連絡先リンク経由でシークレットモード</string>
<string name="description_via_one_time_link_incognito">使い捨てリンク経由でシークレットモード</string>
<string name="moderated_description">モデレーターによって介入済み</string>
<string name="simplex_link_mode_browser_warning">ブラウザでリンクを開くと接続のプライバシーとセキュリティが下がる可能性があります。信頼されないSimpleXリンクは読み込まれません。</string>
<string name="error_joining_group">グループ参加にエラー発生</string>
<string name="error_sending_message">メッセージ送信にエラー発生</string>
<string name="error_accepting_contact_request">連絡先リクエストの承諾にエラー発生</string>
<string name="error_receiving_file">ファイル受信にエラー発生</string>
<string name="error_deleting_contact">連絡先の削除にエラー発生</string>
<string name="error_deleting_group">グループ削除にエラー発生</string>
<string name="error_deleting_pending_contact_connection">接続待ちの連絡先削除にエラー発生</string>
<string name="error_smp_test_certificate">サーバアドレスの証明証IDが正しくないかもしれません。</string>
<string name="smp_server_test_connect">接続</string>
<string name="error_deleting_user">ユーザのプロフィール削除にエラー発生</string>
<string name="periodic_notifications">定期的な通知</string>
<string name="periodic_notifications_disabled">定期的な通知が無効となってます!</string>
<string name="auth_disable_simplex_lock">SimpleXロックを無効にする</string>
<string name="auth_enable_simplex_lock">SimpleXロックを有効にする</string>
<string name="auth_log_in_using_credential">ログイン情報でアクセス</string>
<string name="auth_confirm_credential">アクセス情報を確認</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">端末認証が起動してません。起動したら、設定でSimpleXロックをオンにできます。</string>
<string name="message_delivery_error_desc">恐らくこの連絡先があなたとの接続を削除されました。</string>
<string name="for_everybody">全員用</string>
<string name="error_saving_file">ファイル保存にエラー発生</string>
<string name="icon_descr_server_status_connected">接続中</string>
<string name="icon_descr_server_status_pending">確認待ち</string>
<string name="cancel_verb">中止</string>
<string name="add_contact">使い捨ての招待リンク</string>
<string name="icon_descr_record_voice_message">音声メッセージを録音</string>
<string name="from_gallery_button">ギャラリーから</string>
<string name="delete_contact_menu_action">削除</string>
<string name="icon_descr_help">ヘルプ</string>
<string name="invalid_contact_link">無効なリンク!</string>
<string name="invalid_QR_code">無効なQRコード</string>
<string name="icon_descr_more_button">つづき</string>
<string name="paste_connection_link_below_to_connect">連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。</string>
<string name="incorrect_code">誤ったセキュリティコード!</string>
<string name="paste_button">貼り付け</string>
<string name="chat_console">チャットのコンソール</string>
<string name="configure_ICE_servers">ICEサーバを設定</string>
<string name="how_to_use_your_servers">自分のサーバの使い方</string>
<string name="enter_one_ICE_server_per_line">ICEサーバ (1行に1サーバ)</string>
<string name="network_and_servers">ネットワークとサーバ</string>
<string name="network_settings_title">ネットワーク設定</string>
<string name="network_use_onion_hosts_no_desc_in_alert">オニオンのホストが使われません。</string>
<string name="delete_address">アドレスを削除</string>
<string name="exit_without_saving">保存せずに閉じる</string>
<string name="display_name_cannot_contain_whitespace">表示の名前には空白が使用できません。</string>
<string name="how_to_use_markdown">マークダウン (書式) の使い方</string>
<string name="italic">イタリック文字</string>
<string name="callstatus_rejected">拒否した通話</string>
<string name="callstatus_error">通話エラー</string>
<string name="callstatus_in_progress">通話中</string>
<string name="callstate_connected">接続中</string>
<string name="callstatus_connecting">発信中…</string>
<string name="callstate_ended">終了</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">プロトコル技術とコードはオープンソースで、どなたでもご自分のサーバを運用できます。</string>
<string name="privacy_redefined">プライバシーの基準を新境地に</string>
<string name="how_it_works">技術の説明</string>
<string name="make_private_connection">プライベートな接続をする</string>
<string name="onboarding_notifications_mode_title">プライベートな通知</string>
<string name="read_more_in_github_with_link"><font color="#0088ff">GitHubリポジトリ</font>で詳細をご確認ください。</string>
<string name="read_more_in_github">GitHubリポジトリで詳細をご確認ください。</string>
<string name="encrypted_video_call">エンドツーエンド暗号化済みビデオ通話</string>
<string name="no_call_on_lock_screen">無効にする</string>
<string name="status_no_e2e_encryption">エンドツーエンド暗号化がありません</string>
<string name="open_verb">開く</string>
<string name="icon_descr_call_connecting">発信中</string>
<string name="icon_descr_flip_camera">カメラ切り替え</string>
<string name="icon_descr_call_missed">不在着信</string>
<string name="icon_descr_call_rejected">拒否した通話</string>
<string name="settings_section_title_develop">開発</string>
<string name="integrity_msg_duplicate">重複メッセージ</string>
<string name="settings_section_title_help">ヘルプ</string>
<string name="protect_app_screen">アプリ画面を守る</string>
<string name="settings_section_title_chats">チャット</string>
<string name="settings_developer_tools">開発ツール</string>
<string name="settings_experimental_features">β機能</string>
<string name="settings_section_title_messages">メッセージ</string>
<string name="settings_section_title_calls">通話</string>
<string name="chat_is_running">チャットは稼働中</string>
<string name="chat_is_stopped">チャットが停止してます。</string>
<string name="database_passphrase">データベース暗証フレーズ</string>
<string name="export_database">データベースをエキスポート</string>
<string name="delete_database">データベースを削除</string>
<string name="import_database">データベースを読み込みますか?</string>
<string name="new_database_archive">新しいデータベースのアーカイブ</string>
<string name="old_database_archive">過去のデータベースアーカイブ</string>
<string name="delete_files_and_media_all">ファイルを全て削除</string>
<string name="delete_files_and_media_question">ファイルとメディアを削除しますか?</string>
<string name="total_files_count_and_size">%d ファイル|合計: %s</string>
<string name="encrypt_database">暗号化する</string>
<string name="remove_passphrase">削除</string>
<string name="current_passphrase">現在の暗証フレーズ</string>
<string name="encrypted_with_random_passphrase">データベースはランダムな暗証フレーズで暗号化済みで、変更可能です。</string>
<string name="change_database_passphrase_question">データベースの暗証フレーズを更新しますか?</string>
<string name="database_will_be_encrypted">データベースが暗号化されます。</string>
<string name="database_will_be_encrypted_and_passphrase_stored">データベースが暗号化され、暗証フレーズがキーストア (暗証キー保管庫) にほぞんされます。</string>
<string name="cannot_access_keychain">キーストア (暗証キーの保管庫) にアクセスできないため、データベースのパスワードが保存できません。</string>
<string name="database_passphrase_will_be_updated">データベース暗号化の暗号フレーズが更新されます。</string>
<string name="database_passphrase_is_required">チャットを開くにはデータベース暗証フレーズが必要です。</string>
<string name="file_with_path">ファイル: %s</string>
<string name="archive_created_on_ts">作成日時 \u0020<xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="delete_archive">アーカイブを削除</string>
<string name="join_group_button">参加</string>
<string name="join_group_question">グループに参加しますか?</string>
<string name="joining_group">グループに参加</string>
<string name="leave_group_button">脱退</string>
<string name="leave_group_question">グループを脱退しますか?</string>
<string name="alert_message_group_invitation_expired">グループ招待が無効となり、送信元によって取り消されました。</string>
<string name="alert_title_no_group">グループが見つかりません!</string>
<string name="alert_title_cant_invite_contacts">連絡先を招待できません!</string>
<string name="rcv_group_event_group_deleted">削除されたグループ</string>
<string name="rcv_conn_event_switch_queue_phase_changing">アドレスを変更しています…</string>
<string name="clear_contacts_selection_button">選択を取り消す</string>
<string name="group_member_status_complete">完了</string>
<string name="group_member_status_connected">接続中</string>
<string name="group_member_status_announced">接続待ち (アナウンス済み)</string>
<string name="icon_descr_contact_checked">連絡先が確認済み</string>
<string name="delete_group_question">グループを削除をしますか?</string>
<string name="delete_link">リンクを削除</string>
<string name="group_member_status_group_deleted">グループ削除済み</string>
<string name="delete_group_for_all_members_cannot_undo_warning">全員にとってグループが削除されます (※元に戻せません※)</string>
<string name="group_member_status_invited">招待済み</string>
<string name="button_add_members">メンバーを招待する</string>
<string name="group_member_status_left">脱退しました。</string>
<string name="group_member_role_member">メンバー</string>
<string name="group_member_status_removed">除名されました</string>
<string name="button_remove_member">メンバーを除名する</string>
<string name="change_role">役割変更</string>
<string name="info_row_connection">接続</string>
<string name="conn_level_desc_direct">ダイレクト</string>
<string name="receiving_via">経由で受信</string>
<string name="create_secret_group_title">シークレットグループを作成する</string>
<string name="group_display_name_field">グループの表示名前</string>
<string name="network_status">ネットワーク状況</string>
<string name="network_option_ping_interval">PING間合い</string>
<string name="network_option_protocol_timeout">プロトコル・タイムアウト</string>
<string name="theme_dark">ダークモード</string>
<string name="chat_preferences_contact_allows">連絡先の任意で</string>
<string name="feature_enabled_for_contact">連絡先に有効</string>
<string name="feature_enabled_for_you">あなたに有効</string>
<string name="only_your_contact_can_send_disappearing">消えるメッセージを送れるのはあなたの連絡相手だけです。</string>
<string name="group_members_can_send_dms">グループのメンバーがダイレクトメッセージを送信できます。</string>
<string name="message_deletion_prohibited_in_chat">このグループではメッセージの完全削除が使用禁止です。</string>
<string name="ttl_days">%d 日</string>
<string name="ttl_month">%d 月</string>
<string name="ttl_w">%d週</string>
<string name="ttl_week">%d 週</string>
<string name="v4_4_live_messages_desc">あなたが入力しながら、送信先が更新に気づきます。</string>
<string name="v4_5_italian_interface">イタリア語UI</string>
<string name="contact_already_exists">連絡先が既に存在します。</string>
<string name="error_creating_address">アドレス作成にエラー発生</string>
<string name="error_deleting_contact_request">連絡先リクエスト削除にエラー発生</string>
<string name="hide_notification">プライベートにする</string>
<string name="settings_notifications_mode_title">通知サービス</string>
<string name="notifications_mode_periodic_desc">10分毎に新しいメッセージを1分間確認する。</string>
<string name="icon_descr_edited">編集済み</string>
<string name="icon_descr_cancel_image_preview">画像のプレビューを中止</string>
<string name="chat_with_developers">開発者とチャット</string>
<string name="image_decoding_exception_title">デコードエラー</string>
<string name="image_descr">画像</string>
<string name="icon_descr_server_status_disconnected">切断されました。</string>
<string name="icon_descr_server_status_error">エラー</string>
<string name="ok">OK</string>
<string name="toast_permission_denied">許可がありません!</string>
<string name="clear_verb">消す</string>
<string name="image_descr_link_preview">リンクプレビュー画像</string>
<string name="image_descr_profile_image">プロフィール画像</string>
<string name="markdown_in_messages">メッセージのマークダウン (書式編集)</string>
<string name="smp_servers_check_address">サーバのアドレスを確認してから再度試してください。</string>
<string name="contribute">貢献する</string>
<string name="rate_the_app">アプリを評価</string>
<string name="network_session_mode_user">チャットのプロフィール</string>
<string name="network_session_mode_entity">接続</string>
<string name="core_version">コアのバージョン: v%s</string>
<string name="edit_image">画像を編集</string>
<string name="callstatus_missed">不在着信</string>
<string name="always_use_relay">リレー経由で繋がる。</string>
<string name="status_e2e_encrypted">エンドツーエンド暗号化済み</string>
<string name="chat_database_deleted">チャットのデータベースが削除されました。</string>
<string name="delete_messages_after">次の期間が経ったら、メッセージを削除:</string>
<string name="enable_automatic_deletion_question">自動メッセージ削除を有効にしますか?</string>
<string name="remove_passphrase_from_keychain">キーストア (暗証キー保管庫) を削除しますか?</string>
<string name="database_encrypted">データベースは暗号化済み!</string>
<string name="error_encrypting_database">データベース暗号化ににエラー発生</string>
<string name="confirm_new_passphrase">新しい暗証フレーズを確認…</string>
<string name="encrypt_database_question">データベースを暗号化しますか?</string>
<string name="store_passphrase_securely_without_recover">暗証フレーズを失くさないように保管してください。失くすとチャットにアクセスできなくなります。</string>
<string name="enter_correct_passphrase">正しい暗証フレーズを入力</string>
<string name="enter_passphrase">暗証フレーズを入力</string>
<string name="restore_passphrase_not_found_desc">キーストア (暗証キー保管庫) に暗証フレーズが見つかりません。入力してください。アプリのデータをバックアップのツールで復元させると暗証フレーズが見つかりかねます。そうではない場合は開発者にご連絡ください。</string>
<string name="button_create_group_link">リンクを発行する</string>
<string name="info_row_group">グループ</string>
<string name="error_saving_group_profile">グループのプロフィール保存にエラー発生</string>
<string name="feature_enabled">有効</string>
<string name="group_members_can_send_disappearing">グループのメンバーが消えるメッセージを送信できます。</string>
<string name="ttl_mth">%d月</string>
<string name="v4_3_irreversible_message_deletion">メッセージの完全削除</string>
<string name="v4_3_voice_messages_desc">最大40秒、即受信。</string>
<string name="v4_3_improved_privacy_and_security_desc">起動中アプリリストに出ないようにする。</string>
<string name="v4_4_live_messages">ライブメッセージ</string>
<string name="remove_member_confirmation">除名</string>
<string name="rcv_group_event_member_deleted">除名されました: <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="reply_verb">返信する</string>
<string name="failed_to_create_user_duplicate_title">表示の名前が重複してます!</string>
<string name="failed_to_create_user_title">プロフィール作成にエラー発生</string>
<string name="failed_to_active_user_title">プロフィール切り替えにエラー発生</string>
<string name="error_adding_members">メンバー追加にエラー発生</string>
<string name="smp_server_test_create_queue">サーバの待ち行列を作成する</string>
<string name="smp_server_test_delete_queue">待ち行列を削除</string>
<string name="smp_server_test_disconnect">切断</string>
<string name="turn_off_battery_optimization">利用するには次の画面にてSimpleXに対する <b>電気省電力の設定をオフ</b> for <xliff:g id="appName">SimpleX</xliff:g> してください。そうしないと通知が無効になります。</string>
<string name="database_initialization_error_title">データベースを起動できません。</string>
<string name="settings_notification_preview_title">通知のプレビュー</string>
<string name="simplex_service_notification_text">メッセージ受信中…</string>
<string name="notification_preview_mode_message">メッセージ内容</string>
<string name="notification_new_contact_request">新しい繋がりのリクエスト</string>
<string name="copy_verb">コピー</string>
<string name="delete_message__question">メッセージを削除しますか?</string>
<string name="edit_verb">編集する</string>
<string name="hide_verb">プライベートにする</string>
<string name="delete_message_cannot_be_undone_warning">メッセージが削除されます (※元に戻せません※)</string>
<string name="delete_message_mark_deleted_warning">メッセージが削除対象となります。宛先にはメッセージの解読ができます。</string>
<string name="icon_descr_cancel_file_preview">ファイルのプレビューを中止</string>
<string name="group_connection_pending">接続待ち…</string>
<string name="group_preview_join_as">%s として参加</string>
<string name="icon_descr_image_snd_complete">画像送信済み</string>
<string name="maximum_supported_file_size">現在、ファイルサイズの最上限は <xliff:g id="maxFileSize">%1$s</xliff:g></string>
<string name="large_file">大型ファイル!</string>
<string name="file_not_found">ファイルが見つかりません</string>
<string name="file_will_be_received_when_contact_is_online">連絡先がオンラインになったら受信されます。しばらくお待ちください。</string>
<string name="live_message">ライブメッセージ!</string>
<string name="ask_your_contact_to_enable_voice">音声メッセージを有効にするように連絡相手に要求してください。</string>
<string name="confirm_verb">確認</string>
<string name="no_details">詳細がありません</string>
<string name="reset_verb">戻す</string>
<string name="copied">クリップボードにコピー完了</string>
<string name="share_one_time_link">使い捨てリンクを発行する</string>
<string name="create_group">シークレットグループを作成する</string>
<string name="only_stored_on_members_devices">(グループのメンバーのみに保存されてます)</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 デスクトップ: <b>「QRコードを読み取る」</b>経由でアプリに表示されるQRコードを読み取る</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser"><xliff:g id="appName">SimpleX Chat</xliff:g> の招待リンクをもらったら、ブラウザで開けます:</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 携帯: <b>「アプリで開く」</b>をタップし、 アプリ内で<b>「接続」</b>をタップする。</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">拒否しても、相手には知らされません。</string>
<string name="reject_contact_button">拒否</string>
<string name="clear_chat_question">チャットを消しますか?</string>
<string name="mark_read">既読にする</string>
<string name="mark_unread">未読にする</string>
<string name="mute_chat">ミュート</string>
<string name="delete_pending_connection__question">接続待ちの繋がりを削除しますか?</string>
<string name="connect_button">接続</string>
<string name="create_one_time_link">使い捨てリンクを発行する</string>
<string name="one_time_link">使い捨ての招待リンク</string>
<string name="database_passphrase_and_export">データベース暗証フレーズとエキスポート</string>
<string name="how_to_use_simplex_chat">使い方</string>
<string name="markdown_help">マークダウン (書式編集) ガイド</string>
<string name="smp_servers_enter_manually">サーバを手動で入力</string>
<string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。</string>
<string name="core_build_timestamp">コアのビルド@: %s</string>
<string name="create_address">アドレスを作成</string>
<string name="delete_address__question">アドレスを削除しますか?</string>
<string name="display_name__field">表示の名前:</string>
<string name="full_name__field">フルネーム:</string>
<string name="create_profile_button">作成</string>
<string name="display_name">表示の名前</string>
<string name="full_name_optional__prompt">フルネーム (任意)</string>
<string name="colored">色付き</string>
<string name="callstate_received_answer">応答</string>
<string name="decentralized">分散型</string>
<string name="immune_to_spam_and_abuse">スパムや悪質送信を完全防止</string>
<string name="onboarding_notifications_mode_service">即時</string>
<string name="onboarding_notifications_mode_periodic">定期的</string>
<string name="call_already_ended">通話は既に終了してます!</string>
<string name="encrypted_audio_call">エンドツーエンド暗号化済みの音声通話</string>
<string name="ignore">無視</string>
<string name="reject">拒否</string>
<string name="call_connection_peer_to_peer">P2P</string>
<string name="privacy_and_security">プライバシーとセキュリティ</string>
<string name="alert_text_skipped_messages_it_can_happen_when">次の場合に起こりうる:
\n① メッセージが受信せずにサーバに溜まって30日が経つ場合
\n② 連絡相手からメッセージが送信中に、ご利用の受信サーバが更新・再起動する場合
\n③ 接続が不安定の場合
\nサーバの更新情報をご希望でしたら、設定にて開発者とお繋ぎください。
\nメッセージの喪失を防ぐために、サーバ冗長性を向上してまいります。</string>
<string name="settings_section_title_device">端末</string>
<string name="no_received_app_files">送受信済みのファイルがありません</string>
<string name="delete_messages">メッセージを削除</string>
<string name="chat_archive_header">チャットのアーカイブ</string>
<string name="rcv_group_event_member_connected">接続中</string>
<string name="rcv_group_event_user_deleted">あなたを除名しました。</string>
<string name="group_link">グループのリンク</string>
<string name="info_row_database_id">データベースID</string>
<string name="error_deleting_link_for_group">グループリンク削除にエラー発生</string>
<string name="users_delete_question">チャットのプロフィールを削除しますか?</string>
<string name="users_delete_data_only">ローカルなプロフィールデータのみ</string>
<string name="users_delete_with_connections">プロフィールとサーバ接続</string>
<string name="theme_light">ライトテーマ</string>
<string name="chat_preferences_default">デフォルト (%s)</string>
<string name="prohibit_sending_disappearing_messages">消えるメッセージを使用禁止にする。</string>
<string name="contacts_can_mark_messages_for_deletion">連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。</string>
<string name="only_you_can_send_disappearing">消えるメッセージを送れるのはあなただけです。</string>
<string name="prohibit_message_deletion">メッセージの完全削除を使用禁止にする。</string>
<string name="prohibit_sending_voice">音声メッセージを使用禁止にする。</string>
<string name="feature_cancelled_item">中止されました %s</string>
<string name="v4_4_disappearing_messages">消えるメッセージ</string>
<string name="v4_5_transport_isolation_descr">チャットのプロフィール (既存設定)、または接続 (β機能設定) による</string>
<string name="v4_5_multiple_chat_profiles_descr">異なった名前、アバター、トランスポート隔離。</string>
<string name="v4_4_french_interface">フランス語UI</string>
<string name="v4_5_message_draft">メッセージの下書き</string>
<string name="v4_5_reduced_battery_usage_descr">まだまだ改善してまいります!</string>
<string name="v4_5_multiple_chat_profiles">複数チャットのプロフィール</string>
<string name="v4_5_message_draft_descr">添付を含めて、下書きを保存する。</string>
<string name="v4_5_private_filenames">プライベートなファイル名</string>
<string name="v4_5_reduced_battery_usage">電池使用量低減</string>
<string name="error_removing_member">メンバー除名にエラー発生</string>
<string name="conn_level_desc_indirect">関節 (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="incognito">シークレットモード</string>
<string name="incognito_info_protects">シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されるという対策です。</string>
<string name="chat_preferences_no">いいえ</string>
<string name="chat_preferences_on">オン</string>
<string name="direct_messages">ダイレクトメッセージ</string>
<string name="timed_messages">消えるメッセージ</string>
<string name="disappearing_prohibited_in_this_chat">このチャットでは消えるメッセージが使用禁止です。</string>
<string name="prohibit_direct_messages">メンバー間のダイレクトメッセージを使用禁止にする。</string>
<string name="prohibit_sending_disappearing">消えるメッセージを使用禁止にする。</string>
<string name="ttl_s">%d秒</string>
<string name="ttl_months">%d 月</string>
<string name="ttl_hour">%d 時</string>
<string name="ttl_hours">%d 時</string>
<string name="ttl_h">%d時</string>
<string name="error_starting_chat">チャット開始にエラー発生</string>
<string name="error_stopping_chat">チャット停止にエラー発生</string>
<string name="import_database_question">チャットのデータベースを読み込みますか?</string>
<string name="chat_database_imported">チャットのデータベースが読み込まれました。</string>
<string name="error_deleting_database">チャットデータベース削除にエラー発生</string>
<string name="error_importing_database">チャットデータベースの読み込みにエラー発生</string>
<string name="import_database_confirmation">読み込む</string>
<string name="restart_the_app_to_use_imported_chat_database">読み込んだデータベースを利用するにはアプリを再起動する必要があります。</string>
<string name="delete_chat_profile_question">チャットのプロフィールを削除しますか?</string>
<string name="restart_the_app_to_create_a_new_chat_profile">再起動して新しいチャットプロフィールを作る。</string>
<string name="files_and_media_section">ファイルとメディア</string>
<string name="error_changing_message_deletion">設定変更にエラー発生</string>
<string name="notifications_will_be_hidden">アプリが起動中のみに通知が出ます!</string>
<string name="encrypted_database">暗号化済みデータベース</string>
<string name="keychain_error">キーチェーンのエラー</string>
<string name="store_passphrase_securely">暗証フレーズを失くさないように保管してください。失くすと変更できなくなります。</string>
<string name="error_with_info">エラー: %s</string>
<string name="restore_database_alert_desc">データベースのバックアップで復旧する際に前の暗証フレーズを入力してください。行われたら元に戻せません。</string>
<string name="icon_descr_add_members">メンバーを招待する</string>
<string name="rcv_group_event_member_left">脱退しました。</string>
<string name="rcv_group_event_changed_member_role">%s の役割を %s に変えました。</string>
<string name="rcv_group_event_changed_your_role">役割を %s に変えました。</string>
<string name="rcv_group_event_member_added">招待された <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">アドレスを変更いたします: %s</string>
<string name="group_member_role_owner">オーナー</string>
<string name="group_member_status_connecting">接続待ち</string>
<string name="icon_descr_expand_role">役割の選択を拡大</string>
<string name="invite_to_group_button">グループに招待する</string>
<string name="new_member_role">新しいメンバー役割</string>
<string name="no_contacts_to_add">追加できる連絡先がありません</string>
<string name="button_delete_group">グループを削除</string>
<string name="create_group_link">グループのリンクを発行する</string>
<string name="button_edit_group_profile">グループのプロフィールを編集</string>
<string name="delete_group_for_self_cannot_undo_warning">あなたにとってグループが削除されます (※元に戻せません※)</string>
<string name="button_leave_group">グループを脱退</string>
<string name="database_restore_error">データベース復元エラー</string>
<string name="rcv_group_event_updated_group_profile">グループプロフィールを更新しました</string>
<string name="snd_group_event_user_left">脱退しました。</string>
<string name="invite_prohibited_description">あなたのメインプロフィールで参加してるグループにシークレットモードのプロフィールで繋がってる連絡先を招待しようとしてます。</string>
<string name="group_info_member_you">あなた: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="role_in_group">役割</string>
<string name="sending_via">経由で送信</string>
<string name="theme_system">システム</string>
<string name="incognito_info_share">連絡相手にシークレットモードのプロフィールを共有すると、その連絡相手に招待されたグループでも同じプロフィールが使われます。</string>
<string name="theme">テーマ</string>
<string name="voice_messages">音声メッセージ</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="you_will_join_group">このリンクのグループに参加し、そのメンバーに繋がります。</string>
<string name="connected_to_server_to_receive_messages_from_contact">この連絡先から受信するメッセージのサーバに既に接続してます。</string>
<string name="sending_files_not_yet_supported">ファイル送信機能がまだ実装されてません</string>
<string name="trying_to_connect_to_server_to_receive_messages">このコンタクトから受信するメッセージのサーバに接続しようとしてます。</string>
<string name="unknown_message_format">不明なメッセージ形式</string>
<string name="sender_you_pronoun">あなた</string>
<string name="description_via_group_link">グループリンク経由</string>
<string name="notification_preview_mode_contact_desc">連絡先のみ表示</string>
<string name="auth_stop_chat">チャットを閉じる</string>
<string name="icon_descr_received_msg_status_unread">未読</string>
<string name="icon_descr_sent_msg_status_send_failed">送信失敗</string>
<string name="personal_welcome">ようこそ <xliff:g>%1$s</xliff:g></string>
<string name="voice_message">音声メッセージ</string>
<string name="view_security_code">セキュリティコードを確認</string>
<string name="send_live_message">ライブメッセージを送信</string>
<string name="scan_QR_code">QRコードを読み込む</string>
<string name="to_connect_via_link_title">リンク経由で接続</string>
<string name="security_code">セキュリティコード</string>
<string name="is_verified">%s は認証済み</string>
<string name="smp_servers">SMPサーバ</string>
<string name="save_servers_button">保存</string>
<string name="update_network_session_mode_question">トランスポート隔離モードを更新しますか?</string>
<string name="save_preferences_question">この設定でよろしいですか?</string>
<string name="icon_descr_video_off">ビデオオフ</string>
<string name="call_connection_via_relay">リレー経由</string>
<string name="icon_descr_video_on">ビデオオン</string>
<string name="icon_descr_speaker_off">スピーカーオフ</string>
<string name="your_chat_database">あなたのチャットデータベース</string>
<string name="stop_chat_confirmation">停止</string>
<string name="stop_chat_to_enable_database_actions">データベース操作をするにはチャットを閉じてからです。</string>
<string name="simplex_link_contact">SimpleX連絡先アドレス</string>
<string name="simplex_link_invitation">SimpleX使い捨て招待リンク</string>
<string name="description_via_contact_address_link">連絡先アドレスリンク経由</string>
<string name="description_via_one_time_link">使い捨てリンク経由</string>
<string name="description_you_shared_one_time_link_incognito">シークレットモードで使い捨てリンクを送りました</string>
<string name="simplex_link_group">SimpleXグループリンク</string>
<string name="simplex_link_mode_browser">ブラウザ経由</string>
<string name="sender_cancelled_file_transfer">ファイル送信が中止されました。</string>
<string name="sender_may_have_deleted_the_connection_request">送信元が繋がりリクエストを削除したかもしれません。</string>
<string name="error_smp_test_server_auth">このサーバで待ち行列を作るには認証が必要です。パスワードをご確認ください。</string>
<string name="periodic_notifications_desc">アプリが定期的に新しいメッセージを受信します。一日の電池使用量が約3%で、プッシュ通知に頼らずに、あなたの端末のデータをサーバに送ることはありません。</string>
<string name="la_notice_title_simplex_lock">SimpleXロック</string>
<string name="enter_passphrase_notification_desc">通知を受けるには、データベースの暗証フレーズを入力してください。</string>
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> サービス</string>
<string name="auth_simplex_lock_turned_on">SimpleXロックオン</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">起動時、または非アクティブ状態で30秒が経った後に戻ると、認証する必要となります。</string>
<string name="share_verb">共有する</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">非認証の送信</string>
<string name="this_text_is_available_in_settings">このテキストは設定にあります。</string>
<string name="images_limit_title">画像数の上限を超えてます!</string>
<string name="welcome">ようこそ!</string>
<string name="group_preview_you_are_invited">グループ招待が届きました</string>
<string name="your_chats">あなたのチャット</string>
<string name="text_field_set_contact_placeholder">連絡先を設定…</string>
<string name="waiting_for_file">ファイル待ち</string>
<string name="switch_receiving_address_question">受信アドレスを変えますか?</string>
<string name="send_verb">送信する</string>
<string name="send_live_message_desc">ライブメッセージを送信 (入力しながら宛先の画面で更新される)</string>
<string name="to_share_with_your_contact">(連絡先に共有)</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(クリップボードから読み込むか、貼り付ける)</string>
<string name="thank_you_for_installing_simplex"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>をご利用いただきありがとうございます!</string>
<string name="use_camera_button">カメラ起動</string>
<string name="you_accepted_connection">繋がりを承認しました</string>
<string name="you_invited_your_contact">連絡先に招待を送りました</string>
<string name="connection_you_accepted_will_be_cancelled">承認ずみの接続がキャンセルされます!</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">あなたからリンクを受けた連絡先が接続できなくなります!</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g>アドレス</string>
<string name="show_QR_code">QRコードを表示</string>
<string name="this_link_is_not_a_valid_connection_link">このリンクは有効な接続リンクではありません!</string>
<string name="this_QR_code_is_not_a_link">このQRコードはリンクではありません</string>
<string name="you_will_be_connected_when_group_host_device_is_online">グループのホスト端末がオンラインになったら、接続されます。後でチェックするか、しばらくお待ちください。</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">連絡相手がアプリからQRコードを読み込めます。</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">連絡先がオンラインになったら、接続されます。後でチェックするか、しばらくお待ちください。</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">あなたのチャットプロフィールが
\n連絡相手に送られます。</string>
<string name="share_invitation_link">招待リンクを送る</string>
<string name="your_profile_will_be_sent">あなたのチャットプロフィールが連絡相手に送られます。</string>
<string name="scan_code">コードを読み込む</string>
<string name="scan_code_from_contacts_app">連絡相手のアプリからセキュリティコードを読み込む</string>
<string name="chat_lock">SimpleXロック</string>
<string name="is_not_verified">%s は未認証</string>
<string name="your_contact_address">あなたのチャットアドレス</string>
<string name="your_settings">あなたの設定</string>
<string name="smp_servers_test_server">テストサーバ</string>
<string name="smp_servers_save">サーバを保存</string>
<string name="smp_servers_test_failed">サーバテスト失敗!</string>
<string name="smp_servers_use_server">サーバを使う</string>
<string name="smp_servers_your_server">あなたのサーバ</string>
<string name="smp_servers_your_server_address">あなたのサーバアドレス</string>
<string name="smp_servers_per_user">現在のチャットプロフィールの新しい接続のサーバ</string>
<string name="smp_servers_use_server_for_new_conn">新しい接続に使う</string>
<string name="saved_ICE_servers_will_be_removed">保存されたWebRTC ICEサーバが削除されます。</string>
<string name="your_ICE_servers">あなたのICEサーバ</string>
<string name="network_disable_socks">直接にインタネットに繋がりますか?</string>
<string name="network_enable_socks">SOCKSプロキシを使いますか</string>
<string name="network_socks_toggle">SOCKSプロキシ (ポート9050) を使う</string>
<string name="update_onion_hosts_settings_question">.onionのホスト設定を更新しますか</string>
<string name="network_use_onion_hosts">.onionホストを使う</string>
<string name="network_use_onion_hosts_prefer">利用可能時に</string>
<string name="network_session_mode_transport_isolation">トランスポート隔離</string>
<string name="save_and_notify_contact">保存して、連絡先にに知らせる</string>
<string name="your_current_profile">現在のプロフィール</string>
<string name="save_and_notify_contacts">保存して、連絡先にに知らせる</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">あなたのプライバシーとセキュリティを守るメッセージとアプリのプラットフォーム</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">連絡先情報と届けたメッセージをサーバに保存することは一切ありません。</string>
<string name="you_control_your_chat">あなたのチャットはあなたが決めます!</string>
<string name="your_profile_is_stored_on_your_device">あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。</string>
<string name="profile_is_only_shared_with_your_contacts">プロフィールは連絡先にしか共有されません。</string>
<string name="you_can_use_markdown_to_format_messages__prompt">メッセージの書式をマークダウンで編集できます。</string>
<string name="secret">シークレット</string>
<string name="strikethrough">取り消し線</string>
<string name="callstate_starting">接続中…</string>
<string name="next_generation_of_private_messaging">次世代のプライバシー・メッセンジャー</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">あなたがメッセージの<b>受信</b>サーバを決められます。あなたの連絡先が同じく、自分に対する受信サーバを決められます。</string>
<string name="icon_descr_video_call">ビデオ通話</string>
<string name="onboarding_notifications_mode_off">アプリが稼働中に</string>
<string name="webrtc_ice_servers">WebRTC ICEサーバ</string>
<string name="your_ice_servers">あなたのICEサーバ</string>
<string name="settings_section_title_settings">設定</string>
<string name="alert_title_skipped_messages">飛ばしたメッセージ</string>
<string name="settings_section_title_you">あなた</string>
<string name="settings_section_title_themes">テーマ</string>
<string name="run_chat_section">チャット起動</string>
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">現在のチャットデータベースが読み込まれたデータベースに上書きされます。
\n元に戻せません。プロフィール、連絡先、メッセージ、ファイルが置き換えられます。</string>
<string name="chat_item_ttl_seconds">%s 秒</string>
<string name="delete_files_and_media_desc">ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。</string>
<string name="enable_automatic_deletion_message">選択中の以前の送受信したメッセージが削除されます (※元に戻せません※)。数分かかります。</string>
<string name="update_database">更新</string>
<string name="database_is_not_encrypted">チャットデータベースが暗号化されてません。暗証フレーズを設定して保管してください。</string>
<string name="restore_database">データベースを復元</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">アプリの設定・データベース経由で、またはアプリの再起動でチャットを始められます。</string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">グループ招待が届きました。参加してグループのメンバーとお繋がりください。</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">グループに入りました。招待をくれたメンバーに接続してます。</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">このグループからのメッセージが届かなくなります。チャットの履歴が残ります。</string>
<string name="alert_message_no_group">このグループはもう存在しません。</string>
<string name="group_invitation_tap_to_join">タップして参加</string>
<string name="group_invitation_tap_to_join_incognito">タップしてシークレットモードで参加</string>
<string name="you_joined_this_group">グループに入りました。</string>
<string name="you_rejected_group_invitation">グループの招待を拒否しました</string>
<string name="you_sent_group_invitation">グループの招待を送りました</string>
<string name="snd_group_event_changed_member_role">%sの役割を次に変えました%s</string>
<string name="button_send_direct_message">ダイレクトメッセージを送信</string>
<string name="num_contacts_selected">%d 連絡先が選択中</string>
<string name="snd_group_event_member_deleted">除名しました: <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="switch_verb">切り替える</string>
<string name="member_role_will_be_changed_with_notification">役割が「%s」となります。グループの全員に通知が出ます。</string>
<string name="conn_stats_section_title_servers">サーバ</string>
<string name="member_role_will_be_changed_with_invitation">役割が「%s」となります。メンバーに新しい招待が届きます。</string>
<string name="group_is_decentralized">グループは完全分散型で、メンバーしか内容を見れません。</string>
<string name="network_options_save">保存</string>
<string name="update_network_settings_question">ネットワーク設定を更新しますか?</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">設定を更新すると、全サーバにクライントの再接続が行われます。</string>
<string name="save_color">色を保存</string>
<string name="chat_preferences_you_allow">あなたが次を許可しています:</string>
<string name="chat_preferences_yes">はい</string>
<string name="set_group_preferences">グループ設定を行う</string>
<string name="your_preferences">あなたの設定</string>
<string name="voice_prohibited_in_this_chat">このチャットでは音声メッセージが使用禁止です。</string>
<string name="voice_messages_are_prohibited">このグループでは音声メッセージが使用禁止です。</string>
<string name="v4_2_security_assessment">セキュリティ評価</string>
<string name="whats_new">新着情報</string>
<string name="v4_2_security_assessment_desc">SimpleX ChatはTrail of Bitsによるセキュリティ監査を受けました。</string>
<string name="v4_2_auto_accept_contact_requests_desc">任意的な歓迎メッセージで</string>
<string name="v4_5_italian_interface_descr">ユーザーの皆様に感謝いたしますWeblateにて貢献できます</string>
<string name="v4_4_verify_connection_security">接続のセキュリティを確認</string>
<string name="error_smp_test_failed_at_step">テストが次のステップで失敗しました:%s</string>
<string name="ntf_channel_messages">SimpleX Chatメッセージ</string>
<string name="ntf_channel_calls">SimpleX Chat通話</string>
<string name="settings_notification_preview_mode_title">プレビューを表示</string>
<string name="notifications_mode_periodic">定期的に起動</string>
<string name="notifications_mode_off">アプリがアクティブ時に実行</string>
<string name="icon_descr_sent_msg_status_sent">送信済み</string>
<string name="you_have_no_chats">あなたはチャットがありません。</string>
<string name="share_message">メッセージを送る…</string>
<string name="share_file">ファイル共有…</string>
<string name="voice_message_send_text">音声メッセージ…</string>
<string name="voice_message_with_duration">音声メッセージ (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="icon_descr_send_message">メッセージを送信</string>
<string name="icon_descr_settings">設定</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g>開発チーム</string>
<string name="this_string_is_not_a_connection_link">このストリングは接続リンクではありません!</string>
<string name="chat_with_the_founder">質問やアイデアを送る</string>
<string name="star_on_github">GithubでStar</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">あなたのプロフィールはご自分の端末に保存され、あなたの連絡先のみに共有されます。
\n
\n<xliff:g id="appName">SimpleX</xliff:g>のサーバには開示されません。</string>
<string name="your_calls">あなたの通話</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g>飛ばしたメッセージ</string>
<string name="send_link_previews">リンクのプレビューを送信</string>
<string name="set_password_to_export">暗証フレーズを設定してからエキスポート</string>
<string name="stop_chat_question">チャットを閉じますか?</string>
<string name="save_passphrase_in_keychain">暗証フレーズをキーストア (暗証キー保管庫) に保存</string>
<string name="wrong_passphrase">データベースの暗証フレーズが違います</string>
<string name="unknown_error">不明なエラー</string>
<string name="save_passphrase_and_open_chat">暗証フレーズをを保存して、チャットを開始</string>
<string name="unknown_database_error_with_info">不明なデータベースのエラー: %s</string>
<string name="wrong_passphrase_title">暗証フレーズが違います!</string>
<string name="you_are_invited_to_group">グループ招待が届きました</string>
<string name="network_option_seconds_label"></string>
<string name="network_option_tcp_connection_timeout">TCP接続タイムアウト</string>
<string name="save_group_profile">保存グループのプロフィール</string>
<string name="v4_3_irreversible_message_deletion_desc">連絡先がメッセージの完全削除を許可できます。</string>
<string name="failed_to_create_user_duplicate_desc">同じ表示名前のチャットプロフィールが既にあります。別のを選んでください。</string>
<string name="smp_server_test_secure_queue">待ち行列セキュリティ確認</string>
<string name="database_initialization_error_desc">データベースが正しく稼働してません。詳細はタップにて</string>
<string name="notification_preview_mode_message_desc">連絡先とメッセージを表示</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">あなたのデータを守るために、SimpleXロックをオンにしてください。
\nオンにするには、認証ステップが行われます。</string>
<string name="la_notice_turn_on">オンにする</string>
<string name="auth_unlock">ロック解除</string>
<string name="save_verb">保存する</string>
<string name="reveal_verb">開示する</string>
<string name="tap_to_start_new_chat">タップして新しいチャットを始める</string>
<string name="image_decoding_exception_desc">画像が解読できません。別のイメージで試すか、開発者に伝えてください。</string>
<string name="icon_descr_waiting_for_image">画像待ち</string>
<string name="waiting_for_image">画像待ち</string>
<string name="contact_sent_large_file">向こうが最上限の大きさを超えるファイルを送ろうとしました (<xliff:g id="maxFileSize">%1$s</xliff:g>)。</string>
<string name="voice_messages_prohibited">音声メッセージは使用禁止です!</string>
<string name="add_contact_or_create_group">新しいチャットを始める</string>
<string name="you_can_connect_to_simplex_chat_founder"><font color="#0088ff"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>の開発者と繋がれば、質問したり、速報を受けたりすることができます</font></string>
<string name="to_start_a_new_chat_help_header">新しいチャットを始めるには</string>
<string name="chat_help_tap_button">ボタンをタップ</string>
<string name="unmute_chat">ミュート解除</string>
<string name="set_contact_name">連絡先を設定</string>
<string name="contact_wants_to_connect_with_you">はあなたと繋がりたいです!</string>
<string name="you_can_also_connect_by_clicking_the_link">リンクをクリックすることでも接続できます。ブラウザが起動すれば <b>Open in mobile app (アプリで開く)</b>ボタンをクリックしてください。</string>
<string name="your_chat_profiles">あなたのチャットプロフィール</string>
<string name="smp_servers_scan_qr">サーバのQRコードを読み込む</string>
<string name="smp_servers_test_some_failed">テストに失敗したサーバがあります:</string>
<string name="use_simplex_chat_servers__question"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>サーバを使いますか?</string>
<string name="using_simplex_chat_servers"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>を使っています。</string>
<string name="share_link">リンクを送る</string>
<string name="core_simplexmq_version">simplexmq: バージョン%s (%2s)</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">あなたと繋がるリンク、またはQRコードを共有できます。誰でも接続できます。後で削除しても、連絡先がそのままのこります。</string>
<string name="section_title_welcome_message">歓迎メッセージ</string>
<string name="callstate_waiting_for_answer">応答待ち…</string>
<string name="callstate_waiting_for_confirmation">確認待ち…</string>
<string name="first_platform_without_user_ids">世界初のユーザーIDのないプラットフォーム設計も元からプライベート</string>
<string name="use_chat">チャット</string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g>は次の方法であなたと繋がりたいです:</string>
<string name="video_call_no_encryption">ビデオ通話 (非エンドツーエンド暗号化)</string>
<string name="show_call_on_lock_screen">表示</string>
<string name="icon_descr_speaker_on">スピーカーオン</string>
<string name="your_privacy">あなたのプライバシー</string>
<string name="messages_section_description">この設定は現在のチャットプロフィールのメッセージに適応されます。</string>
<string name="you_have_to_enter_passphrase_every_time">アプリ起動時に暗証フレーズを入力しなければなりません。端末に保存されてません。</string>
<string name="database_backup_can_be_restored">データベースの暗証フレーズ変更が完了してません。</string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">リンク、またはQRコードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。</string>
<string name="network_options_revert">元に戻す</string>
<string name="update_network_settings_confirmation">更新</string>
<string name="accept_feature_set_1_day">1日に設定</string>
<string name="v4_4_disappearing_messages_desc">一定時間が経ったら送信されたメッセージが削除されます。</string>
<string name="v4_4_french_interface_descr">ユーザーの皆様に感謝いたしますWeblateにて貢献できます</string>
<string name="v4_5_private_filenames_descr">時間帯を漏らさないために、画像と音声ファイルはUTCを使います。</string>
<string name="v4_5_transport_isolation">トランスポート隔離</string>
<string name="restore_database_alert_confirm">復元</string>
<string name="restore_database_alert_title">データベースを復元しますか?</string>
<string name="save_and_notify_group_members">保存して、グループのメンバーにに知らせる</string>
<string name="send_us_an_email">メールを送る</string>
<string name="share_image">画像共有…</string>
<string name="simplex_link_mode">SimpleXリンク</string>
<string name="settings_section_title_support">SIMPLEX CHATを支援</string>
<string name="smp_servers_test_servers">テストサーバ</string>
<string name="switch_receiving_address_desc">開発中の機能です相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。</string>
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">あなたのプライバシーを守るために、このアプリはプッシュ通知の変わりに <b><xliff:g id="appName">SimpleX</xliff:g> バックグラウンド・サービス</b> を使ってます。一日の電池使用量は約3%です。</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに <xliff:g id="appName">SimpleX</xliff:g> メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string>
<string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに送られます。</string>
<string name="to_verify_compare">エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</string>
<string name="trying_to_connect_to_server_to_receive_messages_with_error">このコンタクトから受信するメッセージのサーバに接続しようとしてます。(エラー: <xliff:g id="errorMsg">%1$s</xliff:g>)。</string>
<string name="connection_error_auth_desc">使用済みリンク、または連絡先による接続の削除ではなければ、バッグの可能性があります。開発者にお伝えください。
\n繋がるには、連絡先に新しくリンクを発行してもらって、電波が安定かどうかご確認ください。</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">接続を完了するには、連絡相手がオンラインになる必要があります。
\nこの接続をキャンセルして、連絡先を削除をすることもできます (後でやり直すこともできます)。</string>
<string name="verify_security_code">セキュリティコードを確認</string>
<string name="simplex_link_connection"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g>経由</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g>ロゴ</string>
<string name="you_need_to_allow_to_send_voice">音声メッセージを送るには、連絡相手からの音声メッセージを許可しなければなりません。</string>
<string name="you_must_use_the_most_recent_version_of_database">あなたの最新データベースを1つの端末にしか使わなければ、一部の連絡先からメッセージが届きかねます。</string>
<string name="you_are_already_connected_to_vName_via_this_link">既に繋がってます: <xliff:g id="contactName" example="Alice">%1$s!</xliff:g></string>
<string name="alert_title_cant_invite_contacts_descr">シークレットモードのプロフィールでこのグループに参加しています。メインのプロフィールを守るために、招待することができません。</string>
<string name="description_you_shared_one_time_link">使い捨てリンクを送りました</string>
<string name="profile_will_be_sent_to_contact_sending_link">リンクを送ってくれた連絡先にあなたのプロフィールを送ります。</string>
<string name="your_SMP_servers">あなたのSMPサーバ</string>
<string name="your_simplex_contact_address">あなたの<xliff:g id="appName">SimpleX</xliff:g>アドレス</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">連絡先が繋がりリクエストを承認したら、接続されます。後でチェックするか、しばらくお待ちください。</string>
<string name="switch_receiving_address">受信アドレスを変える</string>
<string name="incognito_random_profile">あなたのランダム・プロフィール</string>
<string name="incognito_info_find">シークレットモード接続のプロフィールを確認するには、チャットの上部の連絡先、またはグループ名をタップします。</string>
<string name="v4_3_voice_messages">音声メッセージ</string>
<string name="settings_section_title_socks">SOCKSプロキシ</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">データベースのエキスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。</string>
<string name="update_database_passphrase">データベース暗証フレーズを更新</string>
<string name="save_archive">アーカイブを保存</string>
<string name="snd_group_event_changed_role_for_yourself">あなたが自分の役割を次に変えました:%s</string>
<string name="snd_conn_event_switch_queue_phase_completed">アドレスを変えました</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">%sのアドレスを変えました</string>
<string name="select_contacts">連絡先を選択</string>
<string name="skip_inviting_button">今はメンバーを招待しません</string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g>メンバー</string>
</resources>

View File

@@ -0,0 +1,619 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="server_connected">연결됨</string>
<string name="server_connecting">연결 중</string>
<string name="connect_via_group_link">그룹 링크를 통해 연결할까요\?</string>
<string name="connect_via_invitation_link">초대 링크로 연결할까요\?</string>
<string name="display_name_connection_established">연결 수립됨</string>
<string name="connection_timeout">연결 시간 초과</string>
<string name="cannot_receive_file">파일을 받을 수 없습니다</string>
<string name="contact_already_exists">이미 추가된 연락처에요.</string>
<string name="smp_server_test_connect">연결</string>
<string name="connection_error_auth">연결 오류 (인증)</string>
<string name="smp_server_test_create_queue">대기열 생성</string>
<string name="database_initialization_error_title">데이터베이스를 초기화할 수 없어요</string>
<string name="notifications_mode_service_desc">앱이 백그라운드에서 항상 실행돼요. 대신 메시지가 도착하자마자 바로 알림이 떠요.</string>
<string name="notifications_mode_periodic_desc">10분마다 최대 1분간 새 메시지 확인</string>
<string name="notification_contact_connected">연결됨</string>
<string name="notification_preview_somebody">숨긴 대화 상대 :</string>
<string name="notification_preview_mode_contact">대화 상대 이름</string>
<string name="allow_verb">허용</string>
<string name="auth_confirm_credential">자격 증명 확인</string>
<string name="copy_verb">복사</string>
<string name="contact_connection_pending">연결 중…</string>
<string name="group_connection_pending">연결 중…</string>
<string name="attach">첨부파일</string>
<string name="icon_descr_cancel_file_preview">파일 미리보기 취소</string>
<string name="icon_descr_context">컨텍스트 아이콘</string>
<string name="icon_descr_server_status_connected">연결됨</string>
<string name="back">뒤로</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>새 연락처 추가</b> : 일회용 QR 코드 만들기</string>
<string name="cancel_verb">취소</string>
<string name="icon_descr_cancel_live_message">라이브 메시지 취소</string>
<string name="choose_file">파일 선택</string>
<string name="confirm_verb">확인</string>
<string name="connect_via_link_or_qr">링크 / QR 코드를 통해 연결</string>
<string name="copied">클립보드로 복사됨</string>
<string name="create_group">비밀 그룹 생성</string>
<string name="accept_contact_button">수락</string>
<string name="clear_chat_warning">모든 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요! 메시지는 나에게서만 삭제돼요.</string>
<string name="clear_verb">지우기</string>
<string name="clear_chat_menu_action">지우기</string>
<string name="clear_chat_button">채팅 지우기</string>
<string name="clear_chat_question">채팅을 지울까요\?</string>
<string name="connection_request_sent">연결 요청 완료</string>
<string name="connect_via_link">링크를 통해 연결</string>
<string name="smp_servers_preset_add">프리셋 서버 추가</string>
<string name="smp_servers_add">서버 추가…</string>
<string name="chat_console">채팅 콘솔</string>
<string name="smp_servers_check_address">서버 주소를 확인 후 다시 시도하십시오.</string>
<string name="configure_ICE_servers">ICE 서버 설정</string>
<string name="contribute">기여</string>
<string name="network_settings">고급 네트워크 설정</string>
<string name="network_session_mode_user">채팅 프로필</string>
<string name="network_session_mode_entity">연결</string>
<string name="accept_requests">요청 수락</string>
<string name="app_version_code">앱 빌드 : %s</string>
<string name="appearance_settings">외관</string>
<string name="app_version_title">앱 버전</string>
<string name="app_version_name">앱 버전 : v%s</string>
<string name="accept_automatically">자동</string>
<string name="contact_requests">대화 상대의 요청</string>
<string name="core_version">코어 버전 : v%s</string>
<string name="callstatus_accepted">전화 받음</string>
<string name="bold">굵게</string>
<string name="callstatus_in_progress">전화 연결 중</string>
<string name="colored">색깔</string>
<string name="confirm_password">암호 확인</string>
<string name="callstatus_connecting">전화 연결 중</string>
<string name="create_profile_button">생성</string>
<string name="create_profile">프로필 생성</string>
<string name="callstatus_error">통화 오류</string>
<string name="callstate_connected">연결됨</string>
<string name="callstate_connecting">연결 중…</string>
<string name="create_your_profile">내 프로필 생성</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>배터리에 좋음</b>. 백그라운드 서비스는 10분마다 새 메시지를 확인합니다. 전화 및 긴급 메시지를 놓칠 수 있습니다.</string>
<string name="call_already_ended">전화가 이미 종료되었습니다!</string>
<string name="always_use_relay">항상 릴레이 사용</string>
<string name="icon_descr_audio_call">음성 전화</string>
<string name="settings_audio_video_calls">음성 &amp; 영상 전화</string>
<string name="call_on_lock_screen">잠금 화면에서의 전화</string>
<string name="status_contact_has_no_e2e_encryption">대화 상대와 종단간 암호화되지 않음</string>
<string name="answer_call">응답</string>
<string name="icon_descr_audio_on">소리 켜기</string>
<string name="icon_descr_audio_off">소리 끄기</string>
<string name="icon_descr_call_ended">통화 종료됨</string>
<string name="icon_descr_call_connecting">전화 연결 중</string>
<string name="auto_accept_images">이미지 자동 다운로드하기</string>
<string name="integrity_msg_bad_hash">잘못된 메시지 해시</string>
<string name="integrity_msg_bad_id">잘못된 메시지 아이디</string>
<string name="settings_section_title_calls">전화</string>
<string name="chat_is_running">채팅 기능이 작동하고 있어요</string>
<string name="settings_section_title_chats">채팅</string>
<string name="chat_database_imported">채팅 데이테베이스를 불러 왔어요</string>
<string name="impossible_to_recover_passphrase"><b>주의</b>: 비밀번호를 분실하면 복구나 비밀번호 변경을 할 수 없어요.</string>
<string name="change_database_passphrase_question">데이터베이스 암호를 바꾸겠습니까\?</string>
<string name="confirm_new_passphrase">새로운 암호 확인…</string>
<string name="chat_archive_section">채팅 기록 보관함</string>
<string name="rcv_group_event_changed_your_role">내 역할이 %s 역할로 변경되었습니다.</string>
<string name="rcv_conn_event_switch_queue_phase_changing">주소 바꾸기…</string>
<string name="snd_conn_event_switch_queue_phase_changing">주소 바꾸기…</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s의 주소 바꾸기…</string>
<string name="rcv_group_event_member_connected">연결됨</string>
<string name="group_member_status_complete">완료</string>
<string name="group_member_status_connected">연결됨</string>
<string name="group_member_status_connecting">연결 중</string>
<string name="group_member_status_accepted">연결 중 (수락됨)</string>
<string name="group_member_status_announced">연결 중 (알림)</string>
<string name="group_member_status_intro_invitation">연결 중(초대 시작)</string>
<string name="group_member_status_creator">제작자</string>
<string name="invite_prohibited">상대를 초대할 수 없습니다</string>
<string name="clear_contacts_selection_button">지우기</string>
<string name="icon_descr_contact_checked">대화 상대 확인됨</string>
<string name="create_group_link">그룹 링크 생성</string>
<string name="button_create_group_link">링크 생성</string>
<string name="change_member_role_question">그룹 역할을 바꾸겠습니까\?</string>
<string name="info_row_connection">연결</string>
<string name="users_add">프로필 추가</string>
<string name="incognito_random_profile_description">대화 상대에게 랜덤으로 만들어진 익명 프로필이 보내져요</string>
<string name="cant_delete_user_profile">사용자 프로필을 삭제할 수 없습니다</string>
<string name="chat_preferences_always">항상</string>
<string name="chat_preferences_contact_allows">대화 상대가 허용했어요.</string>
<string name="contact_preferences">연락처 설정</string>
<string name="allow_voice_messages_only_if">대화 상대도 허용한 경우에만 음성 메시지를 보낼 수 있습니다.</string>
<string name="allow_your_contacts_irreversibly_delete">모두에게서 메시지 영구 삭제 허용하기.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">대화 상대에게 자동 삭제되는 메시지 허용하기.</string>
<string name="allow_your_contacts_to_send_voice_messages">상대가 음성 메시지를 보내는 것을 허용하기.</string>
<string name="both_you_and_your_contacts_can_delete">나와 대화 상대 모두 메시지를 영구 삭제할 수 있어요.</string>
<string name="both_you_and_your_contact_can_send_voice">나와 대화 상대 모두 음성 메시지를 보낼 수 있어요.</string>
<string name="contacts_can_mark_messages_for_deletion">상대가 메시지에 삭제 표시를 할 수 있습니다. 그러나 삭제 표시된 메시지 내용은 여전히 볼 수 있습니다.</string>
<string name="allow_to_delete_messages">모두에게서 메시지 영구 삭제 허용하기.</string>
<string name="feature_cancelled_item">%s 취소됨</string>
<string name="v4_2_auto_accept_contact_requests">대화 상대의 요청 자동 수락</string>
<string name="v4_5_transport_isolation_descr">채팅 프로필(기본값) 또는 연결(베타).</string>
<string name="v4_4_verify_connection_security_desc">대화 상대와 보안 코드를 비교해 보세요.</string>
<string name="v4_6_chinese_spanish_interface">중국어 및 스페인어 인터페이스</string>
<string name="about_simplex">SimpleX에 대하여</string>
<string name="accept">수락</string>
<string name="share_one_time_link">일회용 초대 링크 생성</string>
<string name="create_address">주소 생성</string>
<string name="chat_item_ttl_day">1일</string>
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX</xliff:g>에 대하여</string>
<string name="color_primary">강조 색상</string>
<string name="accept_call_on_lock_screen">응답</string>
<string name="accept_connection_request__question">연결 요청을 수락할까요\?</string>
<string name="accept_feature">수락</string>
<string name="network_enable_socks_info">SOCKS 프록시(포트 9050)를 통해 서버에 액세스할까요\? 이 설정을 활성화하기 전에 프록시를 시작해야 해요.</string>
<string name="smp_servers_add_to_another_device">다른 기기에 추가</string>
<string name="v4_3_improved_server_configuration_desc">QR 코드 스캔으로 서버 추가</string>
<string name="button_add_welcome_message">환영 메시지 추가</string>
<string name="group_member_role_admin">관리자</string>
<string name="v4_2_group_links_desc">관리자는 그룹 가입을 위한 링크를 만들 수 있어요.</string>
<string name="allow_to_send_disappearing">자동 삭제되는 메시지 허용하기.</string>
<string name="users_delete_all_chats_deleted">모든 채팅과 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요!</string>
<string name="allow_to_send_voice">음성 메시지 허용하기.</string>
<string name="allow_voice_messages_question">음성 메시지를 허용하겠습니까\?</string>
<string name="allow_disappearing_messages_only_if">상대도 허용하는 경우에만 자동 삭제되는 메시지를 사용할 수 있어요.</string>
<string name="allow_direct_messages">그룹 멤버에게 1:1 채팅 허용하기.</string>
<string name="all_group_members_will_remain_connected">모든 그룹 멤버는 연결 상태가 계속 유지돼요.</string>
<string name="allow_irreversible_message_deletion_only_if">상대도 허용한 경우에만 모두에게서 메시지 영구 삭제가 가능해요.</string>
<string name="all_your_contacts_will_remain_connected">모든 연락처와 연결 상태가 계속 유지돼요.</string>
<string name="notifications_mode_service">항상 켜기</string>
<string name="keychain_is_storing_securely">안드로이드 암호 저장소는 비밀번호를 안전하게 저장하는 데 사용되고 알림이 작동하도록 해요.</string>
<string name="keychain_allows_to_receive_ntfs">안드로이드 암호 저장소는 앱을 다시 시작하거나 비밀번호 변경을 하고 나서 비밀번호를 안전하게 저장하는 데 사용되고 알림이 작동되도록 해요.</string>
<string name="notifications_mode_off_desc">앱이 실행 중일 때만 알림을 받을 수 있고 백그라운드에서 실행되지 않아요.</string>
<string name="full_backup">앱 데이터 백업</string>
<string name="settings_section_title_icon">앱 아이콘</string>
<string name="incognito_random_profile_from_contact_description">링크를 보낸 사람한테 랜덤으로 만들어진 익명 프로필이 보내져요</string>
<string name="network_session_mode_user_description">별도로 분리된 TCP 연결(그리고 SOCKS 자격 증명)이 <b>각각의 채팅 프로필</b>에 사용될 거예요.</string>
<string name="network_session_mode_entity_description">별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 연락처 및 그룹 구성원</b>에게 사용될 거예요.
\n<b>참고</b>: 연결이 많은 경우 배터리 및 트래픽 소비가 엄청 높을 수 있고 일부 연결이 실패할 수 있어요.</string>
<string name="icon_descr_asked_to_receive">이미지 수신 요청됨</string>
<string name="v4_6_audio_video_calls">음성 및 영상 전화</string>
<string name="audio_call_no_encryption">음성 전화 (종단간 암호화 X)</string>
<string name="auth_unavailable">인증할 수 없어요</string>
<string name="turning_off_service_and_periodic">새 메시지를 수신하기 위해 배터리 최적화 설정을 바꿉니다. 설정에서 언제든지 다시 바꿀 수 있습니다.</string>
<string name="onboarding_notifications_mode_off_desc"><b>배터리에 가장 좋음</b>. 앱이 실행 중일 때만 알림을 받게 되며 백그라운드에서 실행되지 않습니다.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>설정을 통해 비활성화할 수 있어요.</b> 앱이 실행되는 동안 알림이 표시되요.</string>
<string name="both_you_and_your_contact_can_send_disappearing">나와 대화 상대 모두 자동 삭제되는 메시지를 보낼 수 있어요.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR 코드 스캔</b>: QR 코드를 보여주는 사람과 연결해요.</string>
<string name="cannot_access_keychain">데이터베이스 암호를 저장하고 있는 암호키 저장소에 접근할 수 없습니다</string>
<string name="onboarding_notifications_mode_service_desc"><b>배터리 많이 사용</b>! 백그라운드에서 항상 실행돼요. 메시지를 수신하자마자 알림이 떠요.</string>
<string name="callstatus_ended">통화 종료됨 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_calling">전화 중…</string>
<string name="icon_descr_call_progress">전화 연결 중</string>
<string name="icon_descr_cancel_link_preview">링크 미리보기 취소</string>
<string name="icon_descr_cancel_image_preview">이미지 미리보기 취소</string>
<string name="rcv_group_event_changed_member_role">%s 역할에서 %s 역할로 변경되었습니다</string>
<string name="chat_database_section">채팅 데이터베이스</string>
<string name="alert_title_cant_invite_contacts">대화 상대를 초대할 수 없습니다!</string>
<string name="change_verb">변경</string>
<string name="chat_archive_header">채팅 기록 보관함</string>
<string name="change_role">역할 바꾸기</string>
<string name="chat_database_deleted">채팅 데이터베이스를 삭제했어요</string>
<string name="chat_is_stopped">채팅 기능이 멈췄어요</string>
<string name="chat_is_stopped_indication">채팅 기능이 멈췄어요</string>
<string name="chat_preferences">채팅 설정</string>
<string name="chat_with_developers">개발자와 대화</string>
<string name="connect_via_link_verb">연결</string>
<string name="display_name_connecting">연결 중…</string>
<string name="icon_descr_close_button">닫기 버튼</string>
<string name="connect_button">연결</string>
<string name="group_member_status_introduced">연결 중 (도입)</string>
<string name="connection_error">연결 오류</string>
<string name="connection_local_display_name">연결 <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="connect_via_contact_link">링크를 통해 연결하겠습니까\?</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">대화 상대와 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요!</string>
<string name="status_contact_has_e2e_encryption">대화 상대와 종단간 암호화됨</string>
<string name="alert_title_contact_connection_pending">대화 상대와 아직 연결되지 않았어요!</string>
<string name="core_build_timestamp">코어 빌드: %s</string>
<string name="archive_created_on_ts"><xliff:g id="archive_ts">%1$s</xliff:g>에 생성 완료</string>
<string name="create_one_time_link">일회용 초대 링크 생성</string>
<string name="create_secret_group_title">비밀 그룹 생성</string>
<string name="accept_contact_incognito_button">익명 수락</string>
<string name="chat_item_ttl_month">1개월</string>
<string name="chat_item_ttl_week">1주</string>
<string name="a_plus_b">a + b</string>
<string name="deleted_description">삭제됨</string>
<string name="simplex_link_mode_description">설명</string>
<string name="smp_server_test_delete_queue">대기열 삭제</string>
<string name="delete_verb">삭제</string>
<string name="delete_message__question">메시지를 삭제할까요\?</string>
<string name="for_me_only">나에게서만 삭제</string>
<string name="delete_member_message__question">멤버의 메시지를 삭제할까요\?</string>
<string name="maximum_supported_file_size">현재 지원되는 최대 파일 크기는 <xliff:g id="maxFileSize">%1$s</xliff:g>입니다.</string>
<string name="image_decoding_exception_title">디코딩 오류</string>
<string name="button_delete_contact">대화 상대 삭제</string>
<string name="delete_contact_question">연락처를 삭제할까요\?</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 데스크톱: <b>QR 코드 스캔</b>을 통해 앱에서 표시된 QR 코드를 스캔해 주세요.</string>
<string name="delete_contact_menu_action">삭제</string>
<string name="delete_group_menu_action">삭제</string>
<string name="delete_pending_connection__question">대기 중인 연결을 삭제할까요\?</string>
<string name="clear_verification">인증 취소</string>
<string name="database_passphrase_and_export">데이터베이스 비밀번호 &amp; 내보내기</string>
<string name="smp_servers_delete_server">서버 삭제</string>
<string name="delete_address">주소 삭제</string>
<string name="delete_address__question">주소를 삭제할까요\?</string>
<string name="delete_image">이미지 삭제</string>
<string name="decentralized">탈중앙화</string>
<string name="settings_section_title_develop">개발</string>
<string name="settings_developer_tools">개발자 도구</string>
<string name="settings_section_title_device">기기</string>
<string name="database_passphrase">데이터베이스 비밀번호</string>
<string name="delete_files_and_media_for_all_users">모든 채팅 프로필 파일 삭제</string>
<string name="database_error">데이터베이스 에러</string>
<string name="passphrase_is_different">데이터베이스 비밀번호가 암호 저장소에 저장된 것과 일치하지 않습니다.</string>
<string name="database_passphrase_is_required">채팅을 열려면 데이터베이스 비밀번호가 필요해요.</string>
<string name="delete_archive">보관된 채팅 삭제</string>
<string name="delete_chat_archive_question">보관된 채팅을 삭제할까요\?</string>
<string name="num_contacts_selected">%d 개의 연락처가 선택되었습니다.</string>
<string name="info_row_database_id">데이터베이스 아이디</string>
<string name="users_delete_profile_for">다음 채팅 프로필 삭제</string>
<string name="theme_dark">어둡게</string>
<string name="delete_after">다음 기간 이후 자동 삭제</string>
<string name="above_then_preposition_continuation">위 다음 :</string>
<string name="delete_database">데이터베이스 삭제</string>
<string name="set_password_to_export_desc">데이터베이스는 임의의 비밀번호로 암호화되었습니다. 내보내기 기능 사용 전 비밀번호를 변경해 주세요.</string>
<string name="delete_files_and_media_question">파일과 미디어를 삭제할까요\?</string>
<string name="current_passphrase">현재 비밀번호…</string>
<string name="database_encrypted">데이터베이스 암호화 완료!</string>
<string name="database_passphrase_will_be_updated">데이터베이스 비밀번호가 업데이트되요.</string>
<string name="encrypted_with_random_passphrase">데이터베이스는 임의의 비밀번호로 암호화되었고, 원하시면 비밀번호를 변경할 수 있어요.</string>
<string name="database_will_be_encrypted">데이터베이스는 암호화될 거예요.</string>
<string name="delete_messages">메시지 삭제</string>
<string name="delete_messages_after">다음 기간 이후 자동 삭제</string>
<string name="rcv_group_event_group_deleted">삭제된 그룹</string>
<string name="delete_link">링크 삭제</string>
<string name="delete_link_question">링크를 삭제할까요\?</string>
<string name="chat_preferences_default">기본값 (%s)</string>
<string name="ttl_day">%d일</string>
<string name="ttl_d">%d일</string>
<string name="ttl_days">%d일</string>
<string name="button_delete_group">그룹 삭제</string>
<string name="rcv_conn_event_switch_queue_phase_completed">주소가 변경되었습니다.</string>
<string name="database_encryption_will_be_updated">데이터베이스 비밀번호가 업데이트되고 암호 저장소에 보관됩니다.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">데이터베이스는 암호화되고, 비밀번호는 암호 저장소에 보관될 거에요.</string>
<string name="users_delete_question">채팅 프로필을 삭제할까요\?</string>
<string name="delete_files_and_media_all">모든 파일 삭제</string>
<string name="delete_chat_profile_question">채팅 프로필을 삭제할까요\?</string>
<string name="full_deletion">모두에게서 삭제</string>
<string name="delete_group_question">그룹을 삭제할까요\?</string>
<string name="failed_to_create_user_duplicate_title">표시 이름이 중복되어요!</string>
<string name="smp_server_test_disconnect">연결 끊기</string>
<string name="auth_device_authentication_is_disabled_turning_off">기기 인증이 비활성화되어 SimpleX 잠금 기능이 작동하지 않아요.</string>
<string name="auth_disable_simplex_lock">SimpleX 잠금 비활성화</string>
<string name="icon_descr_server_status_disconnected">연결 끊김</string>
<string name="add_contact">일회용 초대 링크</string>
<string name="add_contact_or_create_group">새로운 채팅 시작</string>
<string name="display_name__field">표시 이름</string>
<string name="display_name_cannot_contain_whitespace">표시 이름에는 공백문자가 쓰일 수 없어요.</string>
<string name="display_name">표시 이름</string>
<string name="encrypted_audio_call">종단간 암호화된 음성 전화</string>
<string name="encrypted_video_call">종단간 암호화된 영상 전화</string>
<string name="no_call_on_lock_screen">비활성화</string>
<string name="status_e2e_encrypted">종단간 암호화</string>
<string name="integrity_msg_duplicate">중복된 메시지</string>
<string name="accept_feature_set_1_day">1일로 설정</string>
<string name="v4_4_disappearing_messages">자동 삭제되는 메시지</string>
<string name="total_files_count_and_size">전체 크기가 %s인 %d개의 파일</string>
<string name="conn_level_desc_direct">다이렉트</string>
<string name="disappearing_prohibited_in_this_chat">이 채팅에서는 자동 삭제되는 메시지를 사용할 수 없어요.</string>
<string name="disappearing_messages_are_prohibited">이 그룹에서는 자동 삭제되는 메시지를 사용할 수 없어요.</string>
<string name="ttl_m">%d분</string>
<string name="ttl_months">%d 개월</string>
<string name="ttl_min">%d 분</string>
<string name="ttl_month">%d 개월</string>
<string name="ttl_week">%d 주</string>
<string name="downgrade_and_open_chat">다운그레이드하고 채팅 열기</string>
<string name="direct_messages">1:1 메시지</string>
<string name="timed_messages">자동 삭제되는 메시지</string>
<string name="direct_messages_are_prohibited_in_chat">이 그룹에서는 멤버들의 1:1 채팅이 금지되어 있어요.</string>
<string name="ttl_s">%d초</string>
<string name="ttl_sec">%d 초</string>
<string name="ttl_h">%d시</string>
<string name="ttl_mth">%d개월</string>
<string name="ttl_w">%d주</string>
<string name="ttl_weeks">%d 주</string>
<string name="confirm_database_upgrades">데이터베이스 업그레이드 확인</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">기기 인증을 하고 있지 않아요. 기기 인증을 켜면 설정에서 SimpleX 잠금 기능을 사용할 수 있어요.</string>
<string name="ttl_hour">%d 시간</string>
<string name="ttl_hours">%d 시간</string>
<string name="mtr_error_different">앱/데이터베이스의 다른 마이그레이션: %s / %s</string>
<string name="v4_5_multiple_chat_profiles_descr">다른 이름, 아바타 그리고 전송 격리.</string>
<string name="dont_show_again">다시 보지 않기</string>
<string name="connected_to_server_to_receive_messages_from_contact">이 대화 상대로부터의 메시지를 수신할 서버와 연결되었어요.</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="contact_developers">앱 업데이트 후 개발자에게 연락해 주세요.</string>
<string name="connection_error_auth_desc">대화 상대가 나갔거나 초대 링크가 이미 사용된 경우가 아니면 버그일 수 있어요. 이 경우 개발자에게 알려주세요.
\n대화 상대에게 다른 초대 링크 만들도록 부탁하고 네트워크 연결이 안정적인지 확인하세요.</string>
<string name="auth_enable_simplex_lock">SimpleX 잠금 활성화</string>
<string name="auth_log_in_using_credential">자격 증명으로 로그인</string>
<string name="auth_open_chat_console">채팅 콘솔 열기</string>
<string name="auth_stop_chat">채팅 중지하기</string>
<string name="auth_unlock">잠금 해제하기</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">앱을 사용하지 않는 지 30초가 지나면 다시 인증해야 해요.</string>
<string name="contact_wants_to_connect_with_you">님이 연결하고 싶어해요!</string>
<string name="callstate_starting">시작…</string>
<string name="callstate_waiting_for_answer">응답 대기 중…</string>
<string name="callstate_waiting_for_confirmation">확인 대기 중…</string>
<string name="alert_title_skipped_messages">읽지 않는 메시지</string>
<string name="alert_title_cant_invite_contacts_descr">이 그룹에서 익명 프로필을 사용하고 있어요. 내 원래 프로필이 노출되는 걸 방지하기 위해 대화 상대 초대가 허용되지 않아요.</string>
<string name="button_remove_member">멤버 삭제하기</string>
<string name="chat_item_ttl_seconds">%s 초</string>
<string name="alert_message_group_invitation_expired">이 링크로 참여할 수 없어요. 이미 삭제된 링크에요.</string>
<string name="alert_message_no_group">존재하지 않는 그룹이에요.</string>
<string name="alert_title_no_group">그룹을 찾을 수 없어요!</string>
<string name="button_add_members">맴버 초대하기</string>
<string name="button_welcome_message">환영 메시지</string>
<string name="button_edit_group_profile">그룹 프로필 수정</string>
<string name="button_leave_group">그룹 나가기</string>
<string name="button_send_direct_message">1:1 채팅 시작하기</string>
<string name="conn_stats_section_title_servers">서버</string>
<string name="conn_level_desc_indirect">인다이렉트 (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="chat_preferences_you_allow">허용함</string>
<string name="chat_preferences_off">꺼짐</string>
<string name="chat_preferences_on">켜짐</string>
<string name="chat_preferences_no">아니요</string>
<string name="ask_your_contact_to_enable_voice">대화 상대에게 음성 메시지 기능을 활성화 해달라고 부탁해보세요.</string>
<string name="chat_help_tap_button">탭 버튼</string>
<string name="connection_you_accepted_will_be_cancelled">수락한 연결이 취소됩니다!</string>
<string name="chat_lock">SimpleX 잠금</string>
<string name="callstatus_rejected">거절된 전화</string>
<string name="callstate_ended">종료됨</string>
<string name="call_connection_peer_to_peer">P2P</string>
<string name="call_connection_via_relay">릴레이를 경유</string>
<string name="alert_title_group_invitation_expired">만료된 초대 링크에요!</string>
<string name="chat_preferences_yes"></string>
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g>님이 다음을 통해 연결하려고 해요 :</string>
<string name="allow_accepting_calls_from_lock_screen">설정에서 잠금 화면에서 바로 전화를 받을 수 있도록 설정할 수 있어요.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">연결을 완료하려면 대화 상대가 온라인 상태여야 해요.
\n연결 요청을 취소하고 대화 상대를 삭제할 수 있어요 (그리고 새 링크로 재시도).</string>
<string name="alert_text_skipped_messages_it_can_happen_when">다음과 같은 경우에 발생할 수 있어요.
\n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우
\n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우
\n3. 침해된 연결의 경우
\n서버 업데이트를 받으려면 설정을 통해 개발자에게 연락해 주세요.
\n저희 개발팀은 메시지 손실을 방지하기 위해 중복된 서버를 추가할 예정이에요.</string>
<string name="auth_simplex_lock_turned_on">SimpleX 잠금 켜짐</string>
<string name="callstate_received_answer">응답됨…</string>
<string name="callstate_received_confirmation">확인 받음…</string>
<string name="callstatus_missed">부재 중 전화</string>
<string name="chat_item_ttl_none">사용 안 함</string>
<string name="chat_with_the_founder">질문이나 아이디어 보내기</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(스캔하거나 클립보드에서 붙여넣기)</string>
<string name="contact_sent_large_file">대화 상대가 현재 지원되는 최대 크기(<xliff:g id="maxFileSize">%1$s</xliff:g>)보다 큰 파일을 보냈습니다.</string>
<string name="display_name_invited_to_connect">초대를 받았어요.</string>
<string name="failed_to_create_user_title">프로필 생성 오류!</string>
<string name="description_via_group_link_incognito">그룹 링크로 익명 채팅</string>
<string name="description_via_group_link">그룹 링크로 채팅</string>
<string name="description_via_one_time_link">일회용 링크로 채팅</string>
<string name="description_you_shared_one_time_link_incognito">일회용 익명 연락처를 공유했어요.</string>
<string name="description_you_shared_one_time_link">일회용 프로필 연락처를 공유했어요.</string>
<string name="description_via_contact_address_link_incognito">상대의 연락처 링크로 익명 채팅</string>
<string name="description_via_contact_address_link">상대의 연락처 링크로 채팅</string>
<string name="description_via_one_time_link_incognito">일회용 연락처로 익명 채팅</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">SMP 서버 주소가 올바른 형식이고 줄로 구분되어 있고 중복이 없는지 확인해 주세요.</string>
<string name="error_saving_smp_servers">SMP 서버 저장 오류</string>
<string name="error_setting_network_config">네트워크 설정 업데이트 오류</string>
<string name="failed_to_active_user_title">프로필 변경 오류!</string>
<string name="failed_to_create_user_duplicate_desc">동일한 표시 이름을 가진 채팅 프로필이 있어요. 다른 이름을 선택해 주세요.</string>
<string name="failed_to_parse_chats_title">채팅 불러오기 실패</string>
<string name="failed_to_parse_chat_title">채팅 불러오기 실패</string>
<string name="error_adding_members">멤버 추가 오류</string>
<string name="error_joining_group">그룹 참여 오류</string>
<string name="error_sending_message">메시지 전송 오류</string>
<string name="error_creating_address">주소 생성 오류</string>
<string name="error_receiving_file">파일 다운로드 오류</string>
<string name="error_accepting_contact_request">상대 요청 수락 오류</string>
<string name="error_changing_address">주소 변경 오류</string>
<string name="error_deleting_contact">연락처 삭제 오류</string>
<string name="error_deleting_contact_request">대화 요청 삭제 오류</string>
<string name="error_deleting_group">그룹 삭제 오류</string>
<string name="error_deleting_pending_contact_connection">대기 중 대화 요청 삭제 오류</string>
<string name="error_smp_test_certificate">서버 주소의 인증서의 지문(fingerprint)이 잘못되었을 수도 있어요.</string>
<string name="error_smp_test_failed_at_step">테스트가 %s단계에서 실패했어요.</string>
<string name="error_smp_test_server_auth">서버는 대기열을 생성하고 비밀번호를 확인하려면 인증이 필요해요.</string>
<string name="enter_passphrase_notification_desc">알림을 받으려면 데이터베이스 암호를 입력해 주세요.</string>
<string name="enter_passphrase_notification_title">비밀번호가 필요해요.</string>
<string name="error_deleting_user">프로필 삭제 오류</string>
<string name="error_updating_user_privacy">사용자 개인정보 업데이트 오류</string>
<string name="database_initialization_error_desc">데이터베이스가 올바르게 작동하지 안하요. 자세히 알아보려면 탭하세요.</string>
<string name="edit_verb">수정하기</string>
<string name="delete_message_cannot_be_undone_warning">메시지가 삭제돼요. 삭제 후 복구할 수 없어요!</string>
<string name="delete_message_mark_deleted_warning">메시지가 삭제 표시될 거예요. 대화 상대는 여전히 삭제된 내용을 볼 수 있어요.</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICE 서버 주소가 올바른 형식이고 줄로 구분되고 중복이 없는지 확인해 주세요.</string>
<string name="enter_one_ICE_server_per_line">ICE 서버(한 줄에 하나씩)</string>
<string name="error_saving_ICE_servers">ICE 서버 저장 오류</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="exit_without_saving">저장하지 않고 나가기</string>
<string name="encrypt_database">암호화</string>
<string name="encrypted_database">암호화된 데이터베이스</string>
<string name="feature_off">꺼짐</string>
<string name="feature_enabled_for_you">나에게 켜짐</string>
<string name="feature_enabled_for_contact">대화 상대에게 켜짐</string>
<string name="feature_enabled">켜짐</string>
<string name="feature_received_prohibited">수신됨, 금지됨</string>
<string name="export_database">데이터베이스 내보내기</string>
<string name="enable_automatic_deletion_question">자동 삭제되는 메시지를 사용할까요\?</string>
<string name="error_changing_message_deletion">설정 변경 오류</string>
<string name="enable_automatic_deletion_message">이 작업은 되돌릴 수 없어요. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제돼요. 이는 몇 분 걸릴 수 있어요.</string>
<string name="error_with_info">오류: %s</string>
<string name="enter_correct_passphrase">올바른 비밀번호를 입력해 주세요.</string>
<string name="database_backup_can_be_restored">데이터베이스 비밀번호 변경이 완료되지 않았어요.</string>
<string name="database_restore_error">데이터베이스 오류 복구</string>
<string name="error_creating_link_for_group">그룹 링크 생성 오류</string>
<string name="error_updating_link_for_group">그룹 링크 업데이트 오류</string>
<string name="error_changing_role">역할 변경 오류</string>
<string name="error_removing_member">멤버 삭제 오류</string>
<string name="database_downgrade">데이터베이스 다운그레이드</string>
<string name="database_migrations">마이그레이션: %s</string>
<string name="delete_group_for_all_members_cannot_undo_warning">모든 멤버에게서 그룹이 삭제돼요. 삭제 후 복구할 수 없어요!</string>
<string name="delete_group_for_self_cannot_undo_warning">나에게서만 그룹이 삭제되요. 삭제 후 복구할 수 없어요!</string>
<string name="file_not_found">파일을 찾을 수 없음</string>
<string name="error_saving_user_password">사용자 비밀번호 저장 오류</string>
<string name="error_stopping_chat">채팅 정지하기 오류</string>
<string name="error_exporting_chat_database">채팅 데이터베이스 내보내기 오류</string>
<string name="database_is_not_encrypted">채팅 데이터베이스가 암호화되지 않았어요. 비밀번호를 설정하여 보호해 주세요.</string>
<string name="enter_passphrase">비밀번호를 입력해 주세요…</string>
<string name="enter_password_to_show">검색에 비밀번호 입력</string>
<string name="edit_image">이미지 수정하기</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">이 작업은 실행 취소될 수 없어요. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실돼요.</string>
<string name="error_importing_database">채팅 데이터베이스 가져오기 오류</string>
<string name="encrypt_database_question">데이터베이스를 암호화할까요\?</string>
<string name="developer_options">데이터베이스 ID 및 전송 격리 옵션.</string>
<string name="error_starting_chat">채팅 시작하기 오류</string>
<string name="error_encrypting_database">데이터베이스 암호화 오류</string>
<string name="enter_correct_current_passphrase">올바른 현재 비밀번호를 입력해 주세요.</string>
<string name="delete_chat_profile">채팅 프로필 삭제</string>
<string name="delete_profile">프로필 삭제</string>
<string name="database_downgrade_warning">경고: 일부 데이터가 손실될 수 있어요!</string>
<string name="database_upgrade">데이터베이스 업그레이드</string>
<string name="delete_files_and_media_desc">이 작업은 실행 취소될 수 없어요. 수신 및 전송된 모든 파일과 미디어가 삭제돼요. 저해상도 사진만 삭제되지 않아요.</string>
<string name="error_deleting_database">채팅 데이터베이스 삭제 오류</string>
<string name="error_deleting_link_for_group">그룹 링크 삭제 오류</string>
<string name="error_saving_file">파일 저장 오류</string>
<string name="error_saving_group_profile">그룹 프로필 저장 오류</string>
<string name="feature_offered_item">%s 제안</string>
<string name="feature_offered_item_with_param">%s 제안: %2s</string>
<string name="icon_descr_instant_notifications">즉시 알림</string>
<string name="hide_notification">숨김</string>
<string name="hide_verb">숨기기</string>
<string name="for_everybody">모두에게</string>
<string name="icon_descr_edited">수정됨</string>
<string name="icon_descr_image_snd_complete">이미지 보냄</string>
<string name="full_name__field">이름 :</string>
<string name="hide_profile">프로필 숨기기</string>
<string name="icon_descr_flip_camera">카메라 전환</string>
<string name="icon_descr_hang_up">전화 끊기</string>
<string name="file_with_path">파일 : %s</string>
<string name="group_invitation_tap_to_join">탭하여 참여</string>
<string name="group_invitation_tap_to_join_incognito">탭하여 익명으로 참여</string>
<string name="group_member_status_left">나감</string>
<string name="group_member_role_member">멤버</string>
<string name="group_member_role_owner">소유자</string>
<string name="group_member_status_group_deleted">그룹 삭제됨</string>
<string name="group_member_status_invited">초대됨</string>
<string name="group_member_status_removed">삭제됨</string>
<string name="icon_descr_expand_role">역할 선택지 펼치기</string>
<string name="files_and_media_section">파일 &amp; 미디어</string>
<string name="group_invitation_item_description">그룹으로 초대 <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="group_link">그룹 링크</string>
<string name="group_welcome_title">환영 메시지</string>
<string name="group_display_name_field">보여지는 그룹 이름</string>
<string name="group_full_name_field">그룹 이름 :</string>
<string name="group_is_decentralized">그룹은 완전히 탈중앙화되어 있으며 구성원만 그룹을 볼 수 있어요.</string>
<string name="group_unsupported_incognito_main_profile_sent">여기에서는 시크릿 모드가 지원되지 않아요. 기본 프로필이 그룹 멤버들에게 전송될 거예요.</string>
<string name="group_main_profile_sent">프로필이 그룹 구성원에게 전송될 거예요.</string>
<string name="group_profile_is_stored_on_members_devices">그룹 프로필은 서버가 아닌 멤버들의 기기에 저장되어요.</string>
<string name="group_preferences">그룹 설정</string>
<string name="group_members_can_send_disappearing">그룹 구성원은 자동 삭제되는 메시지를 보낼 수 있어요.</string>
<string name="group_members_can_send_dms">그룹 멤버들끼리 1:1 채팅을 할 수 있어요.</string>
<string name="icon_descr_add_members">멤버 초대하기</string>
<string name="icon_descr_group_inactive">비활성 그룹</string>
<string name="group_member_role_observer">관찰자</string>
<string name="group_info_member_you">나 : <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> 멤버</string>
<string name="file_saved">파일 저장됨</string>
<string name="file_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 파일이 저장되어요.</string>
<string name="file_will_be_received_when_contact_is_online">대화 상대가 온라인 상태일 때 파일이 전송되어요. 대화 상대가 온라인이 되기를 기다리거나 나중에 다시 확인해 주세요!</string>
<string name="icon_descr_record_voice_message">음성 메시지 녹화하기</string>
<string name="from_gallery_button">갤러리에서</string>
<string name="icon_descr_profile_image_placeholder">프로필 이미지 플레이스 홀더</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> 주소</string>
<string name="how_to">설명서</string>
<string name="how_to_use_your_servers">내 서버 사용법</string>
<string name="how_to_use_markdown">마크다운 사용법</string>
<string name="how_simplex_works"><xliff:g id="appName">SimpleX</xliff:g> 작동 방식</string>
<string name="group_invitation_expired">그룹 초대가 만료되었어요.</string>
<string name="group_members_can_delete">그룹 멤버는 보낸 메시지를 영구 삭제할 수 있어요.</string>
<string name="group_members_can_send_voice">그룹 멤버는 음성 메시지를 보낼 수 있어요.</string>
<string name="hidden_profile_password">숨긴 프로필 비밀번호</string>
<string name="full_name_optional__prompt">이름 (선택 사항)</string>
<string name="how_it_works">작동 방식</string>
<string name="hide_dev_options">숨기기 :</string>
<string name="cancel_file__question">파일 전송을 취소할까요\?</string>
<string name="file_transfer_will_be_cancelled_warning">파일 전송이 취소될 거예요. 이미 전송이 시작되었다면 중지될 거예요.</string>
<string name="icon_descr_sent_msg_status_sent">보냄</string>
<string name="group_preview_join_as">%s(으)로 참여</string>
<string name="group_preview_you_are_invited">그룹에 초대되었어요.</string>
<string name="icon_descr_received_msg_status_unread">잃지 않음</string>
<string name="icon_descr_sent_msg_status_send_failed">보내기 실패</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">비인증 전송</string>
<string name="icon_descr_help">도움말</string>
<string name="first_platform_without_user_ids">개인을 식별할 수 있는 어떠한 정보(임의의 숫자 포함)도 없는 첫 번째 플랫폼. 단순히 약속이 아니라 프로그램 설계상 완전한 익명성을 제공해요.</string>
<string name="icon_descr_call_rejected">거절된 전화</string>
<string name="icon_descr_call_pending_sent">대기 중인 전화</string>
<string name="icon_descr_call_missed">부재중 전화</string>
<string name="icon_descr_file">파일</string>
<string name="icon_descr_more_button">더 보기</string>
<string name="icon_descr_send_message">메시지 보내기</string>
<string name="icon_descr_server_status_error">오류</string>
<string name="how_to_use_simplex_chat">사용법</string>
<string name="icon_descr_email">이메일</string>
<string name="icon_descr_server_status_pending">대기 중</string>
<string name="icon_descr_settings">설정</string>
<string name="icon_descr_waiting_for_image">이미지 기다리는 중</string>
<string name="image_decoding_exception_desc">이미지를 디코딩할 수 없어요. 다른 이미지를 시도하거나 개발자에게 문의해 주세요.</string>
<string name="image_descr">이미지</string>
<string name="image_saved">갤러리에 사진 저장됨</string>
<string name="images_limit_desc">동시에 최대 10개까지만 이미지를 보낼 수 있어요.</string>
<string name="images_limit_title">이미지 수가 너무 많아요!</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">거절해도 상대에게 알림이 전송되지 않아요.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">직접 만날 수 없다면 <b>영상 통화에서 QR 코드를 보여주거나</b> 링크를 공유해 주세요.</string>
<string name="icon_descr_video_call">영상 전화</string>
<string name="icon_descr_video_off">영상 끄기</string>
<string name="icon_descr_speaker_on">스피커 켜기</string>
<string name="icon_descr_video_on">영상 켜기</string>
<string name="icon_descr_speaker_off">스피커 끄기</string>
<string name="import_database">데이터베이스 가져오기</string>
<string name="import_database_confirmation">가져오기</string>
<string name="incognito">익명 모드</string>
<string name="incognito_info_find">익명 채팅에 사용되는 프로필을 찾으려면 채팅 상단에 있는 연락처 또는 그룹 이름을 탭하세요.</string>
<string name="image_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 이미지가 수신될 거예요.</string>
<string name="image_descr_profile_image">프로필 이미지</string>
<string name="incognito_info_allows">하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요.</string>
<string name="immune_to_spam_and_abuse">스팸 및 남용에 면역</string>
<string name="ignore">무시하기</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser"><xliff:g id="appName">SimpleX Chat</xliff:g> 초대 링크를 받으면 브라우저에서 참여할 수 있어요 :</string>
<string name="image_descr_link_preview">링크 미리보기 이미지</string>
<string name="image_descr_qr_code">QR 코드</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">직접 만날 수 없다면 <b>영상 통화에서 QR 코드를 스캔</b>하거나 상대에게 초대 링크를 공유할 수 있어요.</string>
<string name="icon_descr_video_snd_complete">동영상 보내짐</string>
<string name="icon_descr_video_asked_to_receive">동영상 수신 요청됨</string>
<string name="icon_descr_waiting_for_video">동영상 기다리는 중</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> 로고</string>
<string name="image_will_be_received_when_contact_is_online">대화 상대가 온라인 상태일 때 이미지가 수신될 거예요. 기다리거나 나중에 확인하세요!</string>
<string name="import_database_question">채팅 데이터베이스를 가져올까요\?</string>
<string name="server_error">오류</string>
<string name="invalid_chat">유효하지 않는 채팅</string>
<string name="invalid_data">잘못된 데이터</string>
<string name="invalid_message_format">잘못된 메시지 형식</string>
<string name="invalid_connection_link">잘못된 연결 링크</string>
<string name="notifications">알림</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> 읽지 않은 메시지</string>
<string name="keychain_error">키체인 오류</string>
<string name="invite_to_group_button">그룹에 초대하기</string>
<string name="info_row_local_name">로컬 네임</string>
<string name="join_group_button">참여</string>
<string name="join_group_question">그룹에 참여할까요\?</string>
<string name="invite_prohibited_description">이 그룹에서는 기본 프로필을 사용하는 중인데 반해, 익명 프로필로 연락하고 있는 대화 상대를 초대하려고 하셨어요.</string>
<string name="initial_member_role">초기 역할</string>
<string name="info_row_group">그룹</string>
<string name="incompatible_database_version">호환되지 않는 데이터베이스 버전</string>
<string name="joining_group">그룹에 참여 중</string>
<string name="incognito_info_protects">익명 모드는 기본 프로필 이름과 사진과 같은 개인 정보를 보호해줘요. 새 대화 상대마다 새로운 랜덤 프로필이 만들어져요.</string>
<string name="is_verified">%s 은(는) 인증되었어요.</string>
<string name="italic">기울게</string>
<string name="incognito_info_share">익명 프로필 사용 중 초대받은 그룹에 참여하면, 그 그룹에서도 동일한 익명 프로필이 사용되어요.</string>
<string name="incognito_random_profile">내 랜덤 프로필</string>
<string name="incoming_audio_call">음성 전화 옴</string>
<string name="is_not_verified">%s은(는) 인증되지 않았어요.</string>
<string name="install_simplex_chat_for_terminal">터미널용 <xliff:g id="appNameFull">SimpleX Chat</xliff:g>를 설치하세요</string>
<string name="incoming_video_call">영상 전화 옴</string>
<string name="invalid_migration_confirmation">잘못된 마이그레이션 확인</string>
<string name="join_group_incognito_button">익명 모드로 참여</string>
<string name="invalid_QR_code">잘못된 QR 코드</string>
<string name="incorrect_code">잘못된 보안 코드!</string>
<string name="invalid_contact_link">잘못된 링크!</string>
</resources>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="chat_item_ttl_month">1 mėnuo</string>
<string name="chat_item_ttl_week">1 savaitė</string>
<string name="about_simplex_chat">Apie <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="chat_item_ttl_day">1 diena</string>
<string name="a_plus_b">a + b</string>
<string name="about_simplex">Apie SimpleX</string>
<string name="smp_servers_add">Pridėti serverį…</string>
<string name="v4_3_improved_server_configuration_desc">Pridėti serverius skenuojant QR kodus.</string>
<string name="appearance_settings">Išvaizda</string>
<string name="app_version_title">Programėlės versija</string>
<string name="app_version_name">Programėlės versija: v%s</string>
<string name="app_version_code">Programėlės darinys: %s</string>
<string name="accept_automatically">Automatiškai</string>
<string name="callstatus_calling">skambinama…</string>
<string name="callstatus_error">skambučio klaida</string>
<string name="call_already_ended">Skambutis jau baigtas!</string>
<string name="answer_call">Atsiliepti</string>
<string name="icon_descr_call_ended">Skambutis baigtas</string>
<string name="settings_section_title_calls">SKAMBUČIAI</string>
<string name="allow_your_contacts_irreversibly_delete">Leisti jūsų adresatams negrįžtamai ištrinti išsiųstas žinutes.</string>
<string name="back">Atgal</string>
<string name="settings_section_title_icon">PROGRAMĖLĖS PIKTOGRAMA</string>
<string name="incognito_random_profile_from_contact_description">Adresatui, iš kurio gavote šią nuorodą, bus išsiųstas atsitiktinis profilis</string>
<string name="chat_preferences_always">visada</string>
<string name="allow_your_contacts_to_send_voice_messages">Leisti jūsų adresatams siųsti balso žinutes.</string>
<string name="allow_irreversible_message_deletion_only_if">Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų adresatas jums tai leidžia.</string>
<string name="allow_voice_messages_only_if">Leisti balso žinutes tik tuo atveju, jei jūsų adresatas jas leidžia.</string>
<string name="allow_verb">Leisti</string>
<string name="allow_voice_messages_question">Leisti balso žinutes\?</string>
<string name="bold">pusjuodis</string>
<string name="callstatus_ended">skambutis baigtas <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="icon_descr_audio_call">garso skambutis</string>
<string name="settings_audio_video_calls">Garso ir vaizdo skambučiai</string>
<string name="integrity_msg_bad_hash">bloga žinutės maiša</string>
<string name="integrity_msg_bad_id">blogas žinutės ID</string>
<string name="incognito_random_profile_description">Jūsų adresatui bus išsiųstas atsitiktinis profilis</string>
<string name="allow_disappearing_messages_only_if">Leisti išnykstančias žinutes tik tuo atveju, jei jūsų adresatas jas leidžia.</string>
<string name="clear_chat_warning">Visos žinutės bus ištrintos to neįmanoma bus atšaukti! Žinutės bus ištrintos TIK jums.</string>
<string name="allow_to_delete_messages">Leisti negrįžtamai ištrinti išsiųstas žinutes.</string>
<string name="allow_to_send_disappearing">Leisti siųsti išnykstančias žinutes.</string>
<string name="allow_to_send_voice">Leisti siųsti balso žinutes.</string>
<string name="allow_direct_messages">Leisti siųsti tiesiogines žinutes nariams.</string>
<string name="v4_6_audio_video_calls">Garso ir vaizdo skambučiai</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Leisti jūsų adresatams siųsti išnykstančias žinutes.</string>
<string name="auth_unavailable">Tapatybės nustatymas neprieinamas</string>
<string name="impossible_to_recover_passphrase"><b>Turėkite omenyje</b>: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</string>
<string name="cancel_verb">Atsisakyti</string>
</resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.SimpleX" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:statusBarColor">@android:color/black</item>
<item name="android:windowBackground">@color/window_background_dark</item>
</style>
</resources>

File diff suppressed because it is too large Load Diff

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