Compare commits
237 Commits
v4.5.2
...
ep/contact
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7f5443920 | ||
|
|
ccff79aa59 | ||
|
|
ccb52e0acd | ||
|
|
d84b30c071 | ||
|
|
5ae0afe1fe | ||
|
|
d250e503b0 | ||
|
|
afb0ae3d03 | ||
|
|
1a3f0bed47 | ||
|
|
1e280fb7e1 | ||
|
|
6feac55380 | ||
|
|
93d8eac037 | ||
|
|
a11f99be3d | ||
|
|
da17639309 | ||
|
|
10301aa742 | ||
|
|
2148d50393 | ||
|
|
12fb2a4ec5 | ||
|
|
8085e5b85c | ||
|
|
4ba310ec16 | ||
|
|
865c56f400 | ||
|
|
c510e73256 | ||
|
|
73638129bc | ||
|
|
1a7a79d504 | ||
|
|
d3268e4a72 | ||
|
|
15a93014a5 | ||
|
|
e7735329bc | ||
|
|
3e222c68eb | ||
|
|
a596bd9011 | ||
|
|
21a49710a8 | ||
|
|
ce6fdb2558 | ||
|
|
0baee848a6 | ||
|
|
6f304bc9e6 | ||
|
|
1ca0dfffa0 | ||
|
|
1420084f5e | ||
|
|
3e03474437 | ||
|
|
95366e4d1b | ||
|
|
df1775a1e6 | ||
|
|
30ccea18ab | ||
|
|
4cd90d74ad | ||
|
|
7f1214688a | ||
|
|
aa89d0d156 | ||
|
|
787cd94362 | ||
|
|
ec61a7fc51 | ||
|
|
9b627534f5 | ||
|
|
400a3707b2 | ||
|
|
38a5676b37 | ||
|
|
f00cfa9108 | ||
|
|
afa24722b2 | ||
|
|
ea5cec53bc | ||
|
|
61dc649c70 | ||
|
|
b20824e16c | ||
|
|
39330fdce3 | ||
|
|
6b725a8ef7 | ||
|
|
cbcdeb2b43 | ||
|
|
4351610eca | ||
|
|
935d826a21 | ||
|
|
a8c8137ade | ||
|
|
7b33e1fba8 | ||
|
|
ade7bba97b | ||
|
|
08dd321311 | ||
|
|
67961180c9 | ||
|
|
1093892ede | ||
|
|
ef05fa4905 | ||
|
|
6a99a4f1ae | ||
|
|
4895f396a2 | ||
|
|
c3dffc5909 | ||
|
|
af73e5993d | ||
|
|
dfec1cbb02 | ||
|
|
8d8f7b2524 | ||
|
|
db7b81587f | ||
|
|
31bb744ba7 | ||
|
|
f4b349162f | ||
|
|
7f8adf8f03 | ||
|
|
1f4bb8a224 | ||
|
|
0c3dc8a6e9 | ||
|
|
1f15cf54af | ||
|
|
c96ba30018 | ||
|
|
ffea61917d | ||
|
|
f5c11b8faf | ||
|
|
48b4b23204 | ||
|
|
9df78c8ac8 | ||
|
|
450bfe2e17 | ||
|
|
c79eb36a7a | ||
|
|
a58b3a42db | ||
|
|
e344958224 | ||
|
|
05c4a6c682 | ||
|
|
b2aec6d6a7 | ||
|
|
09c4609b6c | ||
|
|
f6d2aa7aae | ||
|
|
92facf58f7 | ||
|
|
15c36c5a84 | ||
|
|
cea0543e98 | ||
|
|
c0bbe77788 | ||
|
|
9f8cbe140d | ||
|
|
d0cf550b51 | ||
|
|
a86725480f | ||
|
|
9ad22e1f6d | ||
|
|
a266bcbae7 | ||
|
|
1ba210fe77 | ||
|
|
7898395359 | ||
|
|
aeb732c2f6 | ||
|
|
b665dce383 | ||
|
|
f349f124d8 | ||
|
|
8212d7a00e | ||
|
|
36bcb1b26e | ||
|
|
8d6fe2be99 | ||
|
|
d9571c70f2 | ||
|
|
babbca48f8 | ||
|
|
8c4e2e57f9 | ||
|
|
8308651f44 | ||
|
|
ad881bd46a | ||
|
|
c037eb2d24 | ||
|
|
8b4353deba | ||
|
|
a2be0d35fb | ||
|
|
1909bdc702 | ||
|
|
63909defaf | ||
|
|
8544984b17 | ||
|
|
563984c0df | ||
|
|
6500ee5fc9 | ||
|
|
2a9c138a23 | ||
|
|
61d8fa02d4 | ||
|
|
0fac2187f0 | ||
|
|
1db61be860 | ||
|
|
06a0dbd0f2 | ||
|
|
47c6daf0cc | ||
|
|
bcdf502ce6 | ||
|
|
f9e2f4931a | ||
|
|
8929d15df0 | ||
|
|
60d6a47bdb | ||
|
|
2f529535b1 | ||
|
|
90c9eae283 | ||
|
|
added6105b | ||
|
|
2df39b5e24 | ||
|
|
3477dd9400 | ||
|
|
5282551f3d | ||
|
|
dcadaaf29b | ||
|
|
e9d6baa6ba | ||
|
|
6ae052a7a1 | ||
|
|
9f750c2516 | ||
|
|
1fe46834f2 | ||
|
|
3db85c7d37 | ||
|
|
1e4f1b8891 | ||
|
|
0fcd6d40ee | ||
|
|
aaa4ffe789 | ||
|
|
8c4720d0cb | ||
|
|
94b97f6097 | ||
|
|
85800d96c8 | ||
|
|
3b4c06111a | ||
|
|
548d695a82 | ||
|
|
c986a4b88b | ||
|
|
09940ccf8d | ||
|
|
cfc323862f | ||
|
|
d8cc867099 | ||
|
|
dce8a1dff9 | ||
|
|
5bc9e014c2 | ||
|
|
b0c9ba05f3 | ||
|
|
8a2876fca9 | ||
|
|
00d5f3b769 | ||
|
|
17f39ec6a0 | ||
|
|
498ffe8a71 | ||
|
|
858f0f2650 | ||
|
|
66ea2d5d71 | ||
|
|
3dd5b5d835 | ||
|
|
9127b1bbc6 | ||
|
|
1657bcf97d | ||
|
|
428db2f8f4 | ||
|
|
a8fa9b5e58 | ||
|
|
4cc59d9fbd | ||
|
|
c50306709b | ||
|
|
37d0bc2f14 | ||
|
|
2fda0454e3 | ||
|
|
be19af62d9 | ||
|
|
f915eb2a20 | ||
|
|
2bc1236a2c | ||
|
|
9db1924268 | ||
|
|
7a9f220290 | ||
|
|
8145387f77 | ||
|
|
063440e735 | ||
|
|
6724de09c9 | ||
|
|
f379fd0f8c | ||
|
|
34a3387830 | ||
|
|
809cc1f234 | ||
|
|
12200a74ff | ||
|
|
2643ea9066 | ||
|
|
840df89ca6 | ||
|
|
0404b020e6 | ||
|
|
fda41817e9 | ||
|
|
f48cabcc0a | ||
|
|
f123a905d5 | ||
|
|
9b7fbfd513 | ||
|
|
e21b4d4236 | ||
|
|
9ec6911005 | ||
|
|
bfc178faf3 | ||
|
|
c4c93f881d | ||
|
|
d7f9e17bcb | ||
|
|
13706c4f64 | ||
|
|
f2f4b26c35 | ||
|
|
1b7b9da07c | ||
|
|
2817306659 | ||
|
|
5f587c2104 | ||
|
|
f5670c39da | ||
|
|
c0105d135c | ||
|
|
f1a9814faa | ||
|
|
8f0e7512be | ||
|
|
7d49209f79 | ||
|
|
b2e285c2c7 | ||
|
|
54020250dc | ||
|
|
01acbb970a | ||
|
|
36cad35d46 | ||
|
|
41c9c84139 | ||
|
|
9e9ca521b0 | ||
|
|
1927862871 | ||
|
|
c80eaf8550 | ||
|
|
9e6a35bac3 | ||
|
|
62ffcf94a6 | ||
|
|
2b77920dcd | ||
|
|
38b7e4d4a4 | ||
|
|
3e4d4f04ef | ||
|
|
f6f3d17383 | ||
|
|
d5f6b76ec5 | ||
|
|
ae75be56ea | ||
|
|
50b90c4814 | ||
|
|
6eddb5f30f | ||
|
|
cee8f3a4b6 | ||
|
|
a2e5733be6 | ||
|
|
5075657c02 | ||
|
|
0450b1ace2 | ||
|
|
0ebf1da05d | ||
|
|
07ad3edbc2 | ||
|
|
b40ed2a7f3 | ||
|
|
29b074607c | ||
|
|
258a157e44 | ||
|
|
92d9a1f9f2 | ||
|
|
e5009a58df | ||
|
|
35a1ce4903 | ||
|
|
7c4c627ee9 | ||
|
|
b7575ec01d | ||
|
|
a0351d6f99 |
14
.github/workflows/build.yml
vendored
@@ -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
@@ -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
@@ -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!
|
||||
|
||||
[](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://www.reddit.com/r/SimpleXChat)
|
||||
[](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) [<img src="./images/privacy-guides.jpg" height="80">](https://www.privacyguides.org/en/real-time-communication/#simplex-chat) [<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)
|
||||
|
||||
[](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)||[](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|
||||
|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[](https://hosted.weblate.org/projects/simplex-chat/website/de/)||
|
||||
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|
||||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](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)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|
||||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|
||||
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|
||||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)|[](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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -83,6 +83,8 @@ fun TerminalLayout(
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = sendCommand,
|
||||
sendLiveMessage = null,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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 = { _ -> },
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 = { _ -> },
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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 -> {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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 != "") {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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) },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 43 KiB |
@@ -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>
|
||||
50
apps/android/app/src/main/res/values-ar/strings.xml
Normal 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>
|
||||
@@ -513,13 +513,15 @@
|
||||
<!-- Call settings -->
|
||||
<string name="settings_audio_video_calls">Audio- & 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>
|
||||
@@ -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 l’invitation 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
619
apps/android/app/src/main/res/values-ko/strings.xml
Normal 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">음성 & 영상 전화</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">데이터베이스 비밀번호 & 내보내기</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">파일 & 미디어</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>
|
||||
50
apps/android/app/src/main/res/values-lt/strings.xml
Normal 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>
|
||||
7
apps/android/app/src/main/res/values-night/themes.xml
Normal 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>
|
||||