Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0cf550b51 | |||
| a86725480f | |||
| 9ad22e1f6d | |||
| a266bcbae7 | |||
| b665dce383 | |||
| f349f124d8 | |||
| 8212d7a00e | |||
| 36bcb1b26e | |||
| d9571c70f2 | |||
| 8c4e2e57f9 | |||
| 8308651f44 | |||
| ad881bd46a | |||
| c037eb2d24 | |||
| 8b4353deba | |||
| a2be0d35fb | |||
| 1909bdc702 | |||
| 63909defaf | |||
| 8544984b17 | |||
| 563984c0df | |||
| 6500ee5fc9 | |||
| 61d8fa02d4 | |||
| 0fac2187f0 | |||
| 1db61be860 | |||
| 06a0dbd0f2 | |||
| bcdf502ce6 | |||
| f9e2f4931a | |||
| 8929d15df0 | |||
| 2f529535b1 | |||
| 90c9eae283 | |||
| added6105b | |||
| 2df39b5e24 | |||
| 3477dd9400 | |||
| 5282551f3d | |||
| dcadaaf29b | |||
| e9d6baa6ba | |||
| 6ae052a7a1 | |||
| 9f750c2516 | |||
| 1fe46834f2 | |||
| 3db85c7d37 | |||
| 1e4f1b8891 | |||
| 0fcd6d40ee | |||
| aaa4ffe789 | |||
| 8c4720d0cb | |||
| 94b97f6097 | |||
| 85800d96c8 | |||
| 3b4c06111a | |||
| 548d695a82 | |||
| c986a4b88b | |||
| 09940ccf8d | |||
| d8cc867099 | |||
| dce8a1dff9 | |||
| 5bc9e014c2 | |||
| 8a2876fca9 | |||
| 17f39ec6a0 | |||
| 498ffe8a71 | |||
| 66ea2d5d71 | |||
| 3dd5b5d835 | |||
| 9127b1bbc6 | |||
| 1657bcf97d | |||
| 428db2f8f4 | |||
| 4cc59d9fbd | |||
| c50306709b | |||
| 37d0bc2f14 | |||
| 2fda0454e3 | |||
| be19af62d9 | |||
| f915eb2a20 | |||
| 2bc1236a2c | |||
| 9db1924268 | |||
| 7a9f220290 | |||
| 8145387f77 | |||
| 063440e735 | |||
| 6724de09c9 | |||
| 809cc1f234 | |||
| 2643ea9066 | |||
| 840df89ca6 | |||
| 0404b020e6 | |||
| f48cabcc0a | |||
| f123a905d5 | |||
| 9ec6911005 | |||
| c4c93f881d | |||
| 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 | |||
| 6f68840b3a | |||
| 2eef858db1 | |||
| 434315fb08 | |||
| 9b495e576c | |||
| 5405f44f54 | |||
| 53b05974c9 | |||
| 3530022152 | |||
| dc6bab7ae6 | |||
| c9b4ce457e | |||
| bd3325a889 | |||
| 9e347484eb | |||
| 894af0602d | |||
| aa6011a196 | |||
| f24035a99d | |||
| c006b8150f | |||
| 9e4499de6d | |||
| a018e4a581 | |||
| d29fd93ea7 | |||
| b30c7af3a3 | |||
| 2798671d22 | |||
| 0339b399f7 | |||
| d048962959 | |||
| 4af91c4cae | |||
| 8a445ece90 | |||
| 5082f5b4a4 | |||
| c8fae0ec43 | |||
| d9a8d333f7 | |||
| 73f8c543e3 | |||
| 14945a9296 | |||
| 155ffd16ec | |||
| 1eb1e52912 | |||
| af173ee5c4 | |||
| 06a2f7e4da | |||
| 958299784d | |||
| 49b6979ff0 | |||
| 3c493db613 | |||
| 86cc85b3a5 | |||
| 0427d2e578 | |||
| c90d911d2a | |||
| 2473d14baa | |||
| a36f2147d8 | |||
| 3837e92556 | |||
| 76505afff2 | |||
| 89c9a01b20 | |||
| 68517cf852 | |||
| fbbad55a0f | |||
| bcca27bfdb | |||
| c55a7692c5 | |||
| a8aa829e4c | |||
| d5af03ce18 | |||
| 101ef7a81a | |||
| 71daeed81a | |||
| d44324eb4d | |||
| 93d2ef66cf | |||
| 8a78943e94 | |||
| d0f0013755 | |||
| f22ee1a6cf | |||
| 4a58ca60ac | |||
| b206868730 | |||
| bd4c10b224 | |||
| 46d15d1811 | |||
| ea64be55e1 | |||
| c2cd58f63d | |||
| f21fc76ced | |||
| 13bd51b97d | |||
| a1ed0a84b8 | |||
| 4815e447fa | |||
| d80cad57b6 | |||
| a58be6ebb6 | |||
| dfa0272065 |
@@ -91,6 +91,10 @@ 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'
|
||||
shell: bash
|
||||
@@ -109,7 +113,7 @@ jobs:
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
|
||||
@@ -42,12 +42,15 @@ stack.yaml.lock
|
||||
|
||||
# Temporary test files
|
||||
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
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
<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)
|
||||
|
||||
| 19/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)
|
||||
@@ -22,9 +38,87 @@
|
||||
- 🔐 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).
|
||||
|
||||
## 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 | |✓|✓|✓|✓|
|
||||
|🇨🇿 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 ||[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/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/)|||
|
||||
|
||||
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
|
||||
|
||||
@@ -36,15 +130,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)
|
||||
- [Contribute](#contribute)
|
||||
- [Help us with donations](#help-us-with-donations)
|
||||
- [Disclaimers, Security contact, License](#disclaimers)
|
||||
|
||||
## Why privacy matters
|
||||
@@ -75,16 +165,18 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release annoucement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
|
||||
1. _How SimpleX can deliver messages without any user identifiers?_ See [v2 release announcement](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) explaining how SimpleX works.
|
||||
|
||||
2. _Why should I not just use Signal?_ Signal is a centralised platform that uses phone numbers to identify its users and their contacts. It means that while the content of your messages on Signal is protected with robust end-to-end encryption, there is a large amount of meta-data visible to Signal - who you talk with and when.
|
||||
2. _Why should I not just use Signal?_ Signal is a centralized platform that uses phone numbers to identify its users and their contacts. It means that while the content of your messages on Signal is protected with robust end-to-end encryption, there is a large amount of meta-data visible to Signal - who you talk with and when.
|
||||
|
||||
3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identites?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages – it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
|
||||
3. _How is it different from Matrix, Session, Ricochet, Cwtch, etc., that also don't require user identities?_ Although these platforms do not require a _real identity_, they do rely on anonymous user identities to deliver messages – it can be, for example, an identity key or a random number. Using a persistent user identity, even anonymous, creates a risk that user's connection graph becomes known to the observers and/or service providers, and it can lead to de-anonymizing some users. If the same user profile is used to connect to two different people via any messenger other than SimpleX, these two people can confirm if they are connected to the same person - they would use the same user identifier in the messages. With SimpleX there is no meta-data in common between your conversations with different contacts - the quality that no other messaging platform has.
|
||||
|
||||
## News and updates
|
||||
|
||||
Recent updates:
|
||||
|
||||
[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).
|
||||
|
||||
[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).
|
||||
|
||||
[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).
|
||||
@@ -93,18 +185,8 @@ Recent updates:
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md).
|
||||
|
||||
[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
|
||||
@@ -137,7 +219,7 @@ SimpleX Chat is a work in progress – we are releasing improvements as they are
|
||||
|
||||
What is already implemented:
|
||||
|
||||
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notificaitons on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
|
||||
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
|
||||
2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
|
||||
3. [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with forward secrecy (each message is encrypted by its own ephemeral key), break-in recovery (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial key agreement, initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
|
||||
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
|
||||
@@ -147,10 +229,11 @@ What is already implemented:
|
||||
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
|
||||
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
|
||||
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
|
||||
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
||||
|
||||
We plan to add soon:
|
||||
|
||||
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
1. Automatic message queue rotation. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`.
|
||||
3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
|
||||
|
||||
@@ -160,7 +243,7 @@ You can:
|
||||
|
||||
- use SimpleX Chat library to integrate chat functionality into your mobile apps.
|
||||
- create chat bots and services in Haskell - see [simple](./apps/simplex-bot/) and more [advanced chat bot example](./apps/simplex-bot-advanced/).
|
||||
- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScipt chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
|
||||
- create chat bots and services in any language running SimpleX Chat terminal CLI as a local WebSocket server. See [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) and [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js).
|
||||
- run [simplex-chat terminal CLI](./docs/CLI.md) to execute individual chat commands, e.g. to send messages as part of shell script execution.
|
||||
|
||||
If you are considering developing with SimpleX platform please get in touch for any advice and support.
|
||||
@@ -195,16 +278,19 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- ✅ Disappearing messages (with recipient opt-in per-contact).
|
||||
- ✅ "Live" messages.
|
||||
- ✅ Contact verification via a separate out-of-band channel.
|
||||
- 🏗 Multiple user profiles in the same chat database.
|
||||
- 🏗 Optionally avoid re-using the same TCP session for multiple connections.
|
||||
- ✅ Multiple user profiles in the same chat database.
|
||||
- ✅ Optionally avoid re-using the same TCP session for multiple connections.
|
||||
- ✅ Preserve message drafts.
|
||||
- 🏗 File server to optimize for efficient and private sending of large files.
|
||||
- 🏗 Improved audio & video calls.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Reduced battery and traffic usage in large groups.
|
||||
- 🏗 Preserve message drafts.
|
||||
- 🏗 Support older Android OS and 32-bit CPUs.
|
||||
- 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).
|
||||
- Feeds/broadcasts.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
@@ -214,62 +300,9 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- the server doesn't have information about your contacts and groups.
|
||||
- Channels server for large groups and broadcast channels.
|
||||
|
||||
## Join a user group
|
||||
|
||||
You can join a general English-speaking group: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FcIS0gu1h0Y8pZpQkDaSz7HZGSHcKpMB9%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAKzzWAJYrVt1zdgRp4pD3FBst6eK7233DJeNElENLJRA%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%228mazMhefXoM5HxWBfZnvwQ%3D%3D%22%7D). Just bear in mind that it has ~300 members now, and that it is fully decentralized, so sending a message and connecting to all members in this group will take some time, only join it if you:
|
||||
- want to see how larger groups work.
|
||||
- traffic is not a concern (sending each message is ~5mb).
|
||||
|
||||
You can also join a new and smaller English-speaking group if you want to ask questions without too much traffic: [#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!
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love to have you join the development! You can contribute to SimpleX Chat with:
|
||||
|
||||
- translate UI to your language - we are using [Weblate](https://hosted.weblate.org/projects/simplex-chat/) to translate the interface, please get in touch if you want to contribute!
|
||||
- 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
|
||||
- Hosting server for large groups, communities and public channels.
|
||||
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- High capacity multi-node SMP relays.
|
||||
|
||||
## Disclaimers
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 95
|
||||
versionName "4.5-beta.1"
|
||||
versionCode 106
|
||||
versionName "4.6-beta.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
@@ -76,6 +76,11 @@ android {
|
||||
}
|
||||
jniLibs.useLegacyPackaging = compression_level != "0"
|
||||
}
|
||||
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
|
||||
// if (isRelease) {
|
||||
// Comma separated list of languages that will be included in the apk
|
||||
android.defaultConfig.resConfigs("en", "cs", "de", "es", "fr", "it", "nl", "ru", "zh-rCN")
|
||||
// }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,10 +22,12 @@ var TransformOperation;
|
||||
TransformOperation["Decrypt"] = "decrypt";
|
||||
})(TransformOperation || (TransformOperation = {}));
|
||||
let activeCall;
|
||||
let answerTimeout = 30000;
|
||||
const processCommand = (function () {
|
||||
const defaultIceServers = [
|
||||
{ urls: ["stun:stun.simplex.im:443"] },
|
||||
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||
{ urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||
{ urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||
];
|
||||
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
|
||||
return {
|
||||
@@ -100,9 +102,16 @@ const processCommand = (function () {
|
||||
const iceCandidates = getIceCandidates(pc, config);
|
||||
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
|
||||
await setupMediaStreams(call);
|
||||
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
|
||||
pc.addEventListener("connectionstatechange", connectionStateChange);
|
||||
return call;
|
||||
async function connectionStateChange() {
|
||||
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
|
||||
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
|
||||
if (pc.connectionState !== "failed")
|
||||
connectionHandler();
|
||||
}
|
||||
async function connectionHandler() {
|
||||
sendMessageToNative({
|
||||
resp: {
|
||||
type: "connection",
|
||||
@@ -115,6 +124,7 @@ const processCommand = (function () {
|
||||
},
|
||||
});
|
||||
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
||||
clearConnectionTimeout();
|
||||
pc.removeEventListener("connectionstatechange", connectionStateChange);
|
||||
if (activeCall) {
|
||||
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
|
||||
@@ -122,6 +132,7 @@ const processCommand = (function () {
|
||||
endCall();
|
||||
}
|
||||
else if (pc.connectionState == "connected") {
|
||||
clearConnectionTimeout();
|
||||
const stats = (await pc.getStats());
|
||||
for (const stat of stats.values()) {
|
||||
const { type, state } = stat;
|
||||
@@ -141,6 +152,12 @@ const processCommand = (function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
function clearConnectionTimeout() {
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
function serialize(x) {
|
||||
return LZString.compressToBase64(JSON.stringify(x));
|
||||
|
||||
@@ -30,6 +30,7 @@ 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) {
|
||||
@@ -85,3 +86,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()
|
||||
@@ -161,25 +159,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 +266,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 +324,7 @@ fun MainPage(
|
||||
}
|
||||
}
|
||||
onboarding == null || userCreated == null -> SplashView()
|
||||
!chatsAccessAuthorized -> {
|
||||
userAuthorized.value != true -> {
|
||||
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
|
||||
authView()
|
||||
} else {
|
||||
@@ -406,7 +402,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
if (chatId != null) {
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
chatModel.controller.changeActiveUser(userId)
|
||||
chatModel.controller.changeActiveUser(userId, null)
|
||||
}
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
chatModel.clearOverlays.value = true
|
||||
@@ -418,7 +414,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
Log.d(TAG, "processNotificationIntent: ShowChatsAction")
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
chatModel.controller.changeActiveUser(userId)
|
||||
chatModel.controller.changeActiveUser(userId, null)
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
|
||||
@@ -32,12 +32,15 @@ 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
|
||||
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||
@@ -101,8 +104,19 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
isAppOnForeground = true
|
||||
if (chatModel.chatRunning.value == true) {
|
||||
kotlin.runCatching {
|
||||
val chats = chatController.apiGetChats()
|
||||
chatModel.updateChats(chats)
|
||||
val currentUserId = chatModel.currentUser.value?.userId
|
||||
val chats = ArrayList(chatController.apiGetChats())
|
||||
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */
|
||||
if (chatModel.currentUser.value?.userId == currentUserId) {
|
||||
val currentChatId = chatModel.chatId.value
|
||||
val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
|
||||
if (oldStats != null) {
|
||||
val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
|
||||
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
|
||||
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
|
||||
}
|
||||
chatModel.updateChats(chats)
|
||||
}
|
||||
}.onFailure { Log.e(TAG, it.stackTraceToString()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -399,6 +425,13 @@ class ChatModel(val controller: ChatController) {
|
||||
|
||||
fun contactNetworkStatus(contact: Contact): NetworkStatus =
|
||||
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
|
||||
|
||||
fun addTerminalItem(item: TerminalItem) {
|
||||
if (terminalItems.size >= 500) {
|
||||
terminalItems.removeAt(0)
|
||||
}
|
||||
terminalItems.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@@ -415,13 +448,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,
|
||||
@@ -429,11 +468,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,
|
||||
@@ -479,6 +526,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
|
||||
@@ -956,11 +1021,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)
|
||||
@@ -1211,9 +1278,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() =
|
||||
@@ -1250,6 +1332,8 @@ data class ChatItem (
|
||||
is CIContent.SndGroupFeature -> showNtfDir
|
||||
is CIContent.RcvChatFeatureRejected -> showNtfDir
|
||||
is CIContent.RcvGroupFeatureRejected -> showNtfDir
|
||||
is CIContent.SndModerated -> true
|
||||
is CIContent.RcvModerated -> true
|
||||
is CIContent.InvalidJSON -> false
|
||||
}
|
||||
|
||||
@@ -1264,14 +1348,14 @@ data class ChatItem (
|
||||
status: CIStatus = CIStatus.SndNew(),
|
||||
quotedItem: CIQuote? = null,
|
||||
file: CIFile? = null,
|
||||
itemDeleted: Boolean = false,
|
||||
itemDeleted: CIDeleted? = null,
|
||||
itemEdited: Boolean = false,
|
||||
itemTimed: CITimed? = null,
|
||||
editable: Boolean = true
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = dir,
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, null, editable),
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, itemTimed, editable),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
|
||||
quotedItem = quotedItem,
|
||||
file = file
|
||||
@@ -1286,7 +1370,7 @@ data class ChatItem (
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
|
||||
meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead()),
|
||||
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile(text)),
|
||||
quotedItem = null,
|
||||
file = CIFile.getSample(fileName = fileName, fileSize = fileSize, fileStatus = fileStatus)
|
||||
@@ -1301,7 +1385,7 @@ data class ChatItem (
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = dir,
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted = false, itemEdited = false, editable = false),
|
||||
meta = CIMeta.getSample(id, ts, text, status),
|
||||
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
@@ -1310,7 +1394,7 @@ data class ChatItem (
|
||||
fun getGroupInvitationSample(status: CIGroupInvitationStatus = CIGroupInvitationStatus.Pending) =
|
||||
ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead()),
|
||||
content = CIContent.RcvGroupInvitation(groupInvitation = CIGroupInvitation.getSample(status = status), memberRole = GroupMemberRole.Admin),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
@@ -1319,7 +1403,7 @@ data class ChatItem (
|
||||
fun getGroupEventSample() =
|
||||
ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead()),
|
||||
content = CIContent.RcvGroupEventContent(rcvGroupEvent = RcvGroupEvent.MemberAdded(groupMemberId = 1, profile = Profile.sampleData)),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
@@ -1329,7 +1413,7 @@ data class ChatItem (
|
||||
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled, param = null)
|
||||
return ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead()),
|
||||
content = content,
|
||||
quotedItem = null,
|
||||
file = null
|
||||
@@ -1349,7 +1433,7 @@ data class ChatItem (
|
||||
itemStatus = CIStatus.RcvRead(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemDeleted = false,
|
||||
itemDeleted = null,
|
||||
itemEdited = false,
|
||||
itemTimed = null,
|
||||
itemLive = false,
|
||||
@@ -1369,7 +1453,7 @@ data class ChatItem (
|
||||
itemStatus = CIStatus.RcvRead(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemDeleted = false,
|
||||
itemDeleted = null,
|
||||
itemEdited = false,
|
||||
itemTimed = null,
|
||||
itemLive = true,
|
||||
@@ -1414,7 +1498,7 @@ data class CIMeta (
|
||||
val itemStatus: CIStatus,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
val itemDeleted: Boolean,
|
||||
val itemDeleted: CIDeleted?,
|
||||
val itemEdited: Boolean,
|
||||
val itemTimed: CITimed?,
|
||||
val itemLive: Boolean?,
|
||||
@@ -1439,7 +1523,7 @@ data class CIMeta (
|
||||
companion object {
|
||||
fun getSample(
|
||||
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
|
||||
itemDeleted: Boolean = false, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true
|
||||
itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true
|
||||
): CIMeta =
|
||||
CIMeta(
|
||||
itemId = id,
|
||||
@@ -1464,7 +1548,7 @@ data class CIMeta (
|
||||
itemStatus = CIStatus.SndNew(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemDeleted = false,
|
||||
itemDeleted = null,
|
||||
itemEdited = false,
|
||||
itemTimed = null,
|
||||
itemLive = false,
|
||||
@@ -1499,6 +1583,12 @@ sealed class CIStatus {
|
||||
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CIDeleted {
|
||||
@Serializable @SerialName("deleted") class Deleted: CIDeleted()
|
||||
@Serializable @SerialName("moderated") class Moderated(val byGroupMember: GroupMember): CIDeleted()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class CIDeleteMode(val deleteMode: String) {
|
||||
@SerialName("internal") cidmInternal("internal"),
|
||||
@@ -1534,6 +1624,8 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
|
||||
override val text: String get() = when (this) {
|
||||
@@ -1558,6 +1650,8 @@ sealed class CIContent: ItemContent {
|
||||
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
|
||||
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
is SndModerated -> generalGetString(R.string.moderated_description)
|
||||
is RcvModerated -> generalGetString(R.string.moderated_description)
|
||||
is InvalidJSON -> "invalid data"
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
private val msgNtfTimeoutMs = 30000L
|
||||
|
||||
init {
|
||||
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
|
||||
// Remove old channels since they can't be edited
|
||||
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
|
||||
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
|
||||
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
|
||||
}
|
||||
|
||||
enum class NotificationAction {
|
||||
@@ -84,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,
|
||||
@@ -95,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,
|
||||
@@ -105,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)
|
||||
@@ -156,7 +152,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
.setGroup(MessageGroup)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(chatPendingIntent(ShowChatsAction, user.userId))
|
||||
.setContentIntent(chatPendingIntent(ShowChatsAction, null))
|
||||
.build()
|
||||
|
||||
with(NotificationManagerCompat.from(context)) {
|
||||
@@ -250,7 +246,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
}
|
||||
|
||||
private fun chatPendingIntent(intentAction: String, userId: Long, chatId: String? = null, broadcast: Boolean = false): PendingIntent {
|
||||
private fun chatPendingIntent(intentAction: String, userId: Long?, chatId: String? = null, broadcast: Boolean = false): PendingIntent {
|
||||
Log.d(TAG, "chatPendingIntent for $intentAction")
|
||||
val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt()
|
||||
var intent = Intent(context, if (!broadcast) MainActivity::class.java else NtfActionReceiver::class.java)
|
||||
@@ -268,6 +264,21 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
|
||||
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
|
||||
* already saw such alert or when you want to trigger showing the alert.
|
||||
* On the first app launch the channels will be created after user profile is created. Subsequent calls will create new channels and delete
|
||||
* old ones if needed
|
||||
* */
|
||||
fun createNtfChannelsMaybeShowAlert() {
|
||||
manager.createNotificationChannel(NotificationChannel(MessageChannel, generalGetString(R.string.ntf_channel_messages), NotificationManager.IMPORTANCE_HIGH))
|
||||
manager.createNotificationChannel(callNotificationChannel(CallChannel, generalGetString(R.string.ntf_channel_calls)))
|
||||
// Remove old channels since they can't be edited
|
||||
manager.deleteNotificationChannel("chat.simplex.app.CALL_NOTIFICATION")
|
||||
manager.deleteNotificationChannel("chat.simplex.app.LOCK_SCREEN_CALL_NOTIFICATION")
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes every action specified by [NotificationCompat.Builder.addAction] that comes with [NotificationAction]
|
||||
* and [ChatInfo.id] as [ChatIdKey] in extra
|
||||
|
||||
@@ -88,7 +88,6 @@ class AppPreferences(val context: Context) {
|
||||
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
||||
val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true)
|
||||
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
|
||||
val privacyTransferImagesInline = mkBoolPreference(SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE, true)
|
||||
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
|
||||
private val _simplexLinkMode = mkStrPreference(SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE, SimplexLinkMode.default.name)
|
||||
val simplexLinkMode: SharedPreference<SimplexLinkMode> = SharedPreference(
|
||||
@@ -134,6 +133,9 @@ 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)
|
||||
@@ -215,6 +217,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"
|
||||
@@ -232,6 +235,8 @@ 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"
|
||||
@@ -260,6 +265,12 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
chatModel.incognito.value = appPrefs.incognito.get()
|
||||
}
|
||||
|
||||
private fun currentUserId(funcName: String): Long {
|
||||
val error = "$funcName: no current user"
|
||||
Log.e(TAG, error)
|
||||
return chatModel.currentUser.value?.userId ?: throw Exception(error)
|
||||
}
|
||||
|
||||
suspend fun startChat(user: User) {
|
||||
Log.d(TAG, "user: $user")
|
||||
try {
|
||||
@@ -291,20 +302,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() {
|
||||
@@ -343,7 +360,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return withContext(Dispatchers.IO) {
|
||||
val c = cmd.cmdString
|
||||
if (cmd !is CC.ApiParseMarkdown) {
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(cmd.obfuscated))
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(cmd.obfuscated))
|
||||
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
|
||||
}
|
||||
val json = chatSendCmd(ctrl, c)
|
||||
@@ -353,7 +370,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
Log.d(TAG, "sendCmd response json $json")
|
||||
}
|
||||
if (r.resp !is CR.ParsedMarkdown) {
|
||||
chatModel.terminalItems.add(TerminalItem.resp(r.resp))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(r.resp))
|
||||
}
|
||||
r.resp
|
||||
}
|
||||
@@ -379,11 +396,19 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateActiveUser(p: Profile): User {
|
||||
suspend fun apiCreateActiveUser(p: Profile): User? {
|
||||
val r = sendCmd(CC.CreateActiveUser(p))
|
||||
if (r is CR.ActiveUser) return r.user
|
||||
else if (
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName ||
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat && r.chatError.errorType is ChatErrorType.UserExists
|
||||
) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.failed_to_create_user_duplicate_title), generalGetString(R.string.failed_to_create_user_duplicate_desc))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.failed_to_create_user_title), r.details)
|
||||
}
|
||||
Log.d(TAG, "apiCreateActiveUser: ${r.responseType} ${r.details}")
|
||||
throw Error("user not created ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun listUsers(): List<UserInfo> {
|
||||
@@ -393,15 +418,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, viewPwd: String?): User =
|
||||
setUserPrivacy(CC.ApiMuteUser(userId, viewPwd))
|
||||
|
||||
suspend fun apiUnmuteUser(userId: Long, viewPwd: String?): User =
|
||||
setUserPrivacy(CC.ApiUnmuteUser(userId, viewPwd))
|
||||
|
||||
private suspend fun setUserPrivacy(cmd: CC): User {
|
||||
val r = sendCmd(cmd)
|
||||
if (r is CR.UserPrivacy) return r.user
|
||||
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}")
|
||||
@@ -462,10 +505,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}")
|
||||
@@ -509,11 +549,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}")
|
||||
@@ -521,10 +565,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
|
||||
@@ -540,8 +581,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 -> {
|
||||
@@ -552,14 +593,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}")
|
||||
@@ -743,10 +784,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}")
|
||||
@@ -754,10 +792,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
|
||||
@@ -787,10 +822,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
|
||||
@@ -804,10 +836,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}")
|
||||
@@ -815,10 +844,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
|
||||
@@ -830,10 +856,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
|
||||
@@ -926,7 +949,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(fileId: Long, inline: Boolean): AChatItem? {
|
||||
suspend fun apiReceiveFile(fileId: Long, inline: Boolean? = null): AChatItem? {
|
||||
val r = sendCmd(CC.ReceiveFile(fileId, inline))
|
||||
return when (r) {
|
||||
is CR.RcvFileAccepted -> r.chatItem
|
||||
@@ -953,10 +976,7 @@ 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
|
||||
}
|
||||
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}")
|
||||
@@ -1052,9 +1072,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)
|
||||
@@ -1064,6 +1084,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
|
||||
@@ -1076,9 +1108,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
|
||||
@@ -1143,7 +1175,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
|
||||
suspend fun processReceivedMsg(r: CR) {
|
||||
lastMsgReceivedTimestamp = System.currentTimeMillis()
|
||||
chatModel.terminalItems.add(TerminalItem.resp(r))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(r))
|
||||
when (r) {
|
||||
is CR.NewContactConnection -> {
|
||||
if (active(r.user)) {
|
||||
@@ -1177,7 +1209,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)
|
||||
}
|
||||
@@ -1263,7 +1299,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,
|
||||
@@ -1335,6 +1371,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(r.toGroup)
|
||||
}
|
||||
is CR.MemberRole ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(r.groupInfo)
|
||||
}
|
||||
is CR.RcvFileStart ->
|
||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
is CR.RcvFileComplete ->
|
||||
@@ -1354,8 +1394,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
removeFile(appContext, fileName)
|
||||
}
|
||||
}
|
||||
is CR.CallInvitation ->
|
||||
is CR.CallInvitation -> {
|
||||
chatModel.callManager.reportNewIncomingCall(r.callInvitation)
|
||||
}
|
||||
is CR.CallOffer -> {
|
||||
// TODO askConfirmation?
|
||||
// TODO check encryption is compatible
|
||||
@@ -1415,8 +1456,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun receiveFile(user: User, fileId: Long) {
|
||||
val inline = appPrefs.privacyTransferImagesInline.get()
|
||||
val chatItem = apiReceiveFile(fileId, inline)
|
||||
val chatItem = apiReceiveFile(fileId)
|
||||
if (chatItem != null) {
|
||||
chatItemSimpleUpdate(user, chatItem)
|
||||
}
|
||||
@@ -1432,8 +1472,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
private suspend fun chatItemSimpleUpdate(user: User, aChatItem: AChatItem) {
|
||||
val cInfo = aChatItem.chatInfo
|
||||
val cItem = aChatItem.chatItem
|
||||
if (!active(user) || chatModel.upsertChatItem(cInfo, cItem)) {
|
||||
ntfManager.notifyMessageReceived(chatModel.currentUser.value!!, cInfo, cItem)
|
||||
val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) }
|
||||
if (!active(user)) {
|
||||
notify()
|
||||
} else if (chatModel.upsertChatItem(cInfo, cItem)) {
|
||||
notify()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1719,8 +1762,12 @@ 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, val viewPwd: String?): CC()
|
||||
class ApiUnmuteUser(val userId: Long, val viewPwd: String?): CC()
|
||||
class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC()
|
||||
class StartChat(val expire: Boolean): CC()
|
||||
class ApiStopChat: CC()
|
||||
class SetFilesFolder(val filesFolder: String): CC()
|
||||
@@ -1734,6 +1781,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()
|
||||
@@ -1742,12 +1790,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()
|
||||
@@ -1786,7 +1835,7 @@ sealed class CC {
|
||||
class ApiRejectContact(val contactReqId: Long): 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 ReceiveFile(val fileId: Long, val inline: Boolean?): CC()
|
||||
class ShowVersion(): CC()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
@@ -1794,8 +1843,12 @@ 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${maybePwd(viewPwd)}"
|
||||
is ApiMuteUser -> "/_mute user $userId${maybePwd(viewPwd)}"
|
||||
is ApiUnmuteUser -> "/_unmute user $userId${maybePwd(viewPwd)}"
|
||||
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
|
||||
is StartChat -> "/_start subscribe=on expire=${onOff(expire)}"
|
||||
is ApiStopChat -> "/_stop"
|
||||
is SetFilesFolder -> "/_files_folder $filesFolder"
|
||||
@@ -1809,6 +1862,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"
|
||||
@@ -1817,12 +1871,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)}"
|
||||
@@ -1861,7 +1916,7 @@ sealed class CC {
|
||||
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||
is ReceiveFile -> "/freceive $fileId inline=${onOff(inline)}"
|
||||
is ReceiveFile -> if (inline == null) "/freceive $fileId" else "/freceive $fileId inline=${onOff(inline)}"
|
||||
is ShowVersion -> "/version"
|
||||
}
|
||||
|
||||
@@ -1871,6 +1926,10 @@ 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"
|
||||
@@ -1885,6 +1944,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"
|
||||
@@ -1894,11 +1954,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"
|
||||
@@ -1951,13 +2012,28 @@ 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, obfuscateOrNull(viewPwd))
|
||||
is ApiMuteUser -> ApiMuteUser(userId, obfuscateOrNull(viewPwd))
|
||||
is ApiUnmuteUser -> ApiUnmuteUser(userId, obfuscateOrNull(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}"
|
||||
|
||||
@@ -2810,6 +2886,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["chatCmdError"]))),
|
||||
corr = data["corr"]?.toString()
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error while parsing chat(s): " + e.stackTraceToString())
|
||||
@@ -2866,6 +2949,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): 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()
|
||||
@@ -2915,8 +2999,8 @@ 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()
|
||||
@@ -2939,8 +3023,8 @@ sealed class CR {
|
||||
@Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo): 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()
|
||||
|
||||
@@ -2969,6 +3053,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"
|
||||
@@ -3070,6 +3155,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, "")
|
||||
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)}")
|
||||
@@ -3119,8 +3205,8 @@ 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))
|
||||
@@ -3140,8 +3226,8 @@ sealed class CR {
|
||||
is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection))
|
||||
is VersionInfo -> json.encodeToString(versionInfo)
|
||||
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
|
||||
}
|
||||
@@ -3213,11 +3299,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
|
||||
@@ -3225,12 +3313,14 @@ sealed class ChatErrorType {
|
||||
val string: String get() = when (this) {
|
||||
is NoActiveUser -> "noActiveUser"
|
||||
is DifferentActiveUser -> "differentActiveUser"
|
||||
is UserExists -> "userExists"
|
||||
is InvalidConnReq -> "invalidConnReq"
|
||||
is FileAlreadyReceiving -> "fileAlreadyReceiving"
|
||||
is СommandError -> "commandError $message"
|
||||
}
|
||||
@Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType()
|
||||
@Serializable @SerialName("differentActiveUser") class DifferentActiveUser: ChatErrorType()
|
||||
@Serializable @SerialName("userExists") class UserExists(val contactName: String): ChatErrorType()
|
||||
@Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType()
|
||||
@Serializable @SerialName("fileAlreadyReceiving") class FileAlreadyReceiving: ChatErrorType()
|
||||
@Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType()
|
||||
@@ -3241,9 +3331,11 @@ sealed class StoreError {
|
||||
val string: String get() = when (this) {
|
||||
is UserContactLinkNotFound -> "userContactLinkNotFound"
|
||||
is GroupNotFound -> "groupNotFound"
|
||||
is DuplicateName -> "duplicateName"
|
||||
}
|
||||
@Serializable @SerialName("userContactLinkNotFound") class UserContactLinkNotFound: StoreError()
|
||||
@Serializable @SerialName("groupNotFound") class GroupNotFound: StoreError()
|
||||
@Serializable @SerialName("duplicateName") class DuplicateName: StoreError()
|
||||
}
|
||||
|
||||
@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
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
package chat.simplex.app.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.SystemClock
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
@@ -31,70 +23,18 @@ import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
|
||||
private val lastSuccessfulAuth: MutableState<Long?> = mutableStateOf(null)
|
||||
|
||||
@Composable
|
||||
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
||||
val lastSuccessfulAuth = remember { lastSuccessfulAuth }
|
||||
BackHandler(onBack = {
|
||||
lastSuccessfulAuth.value = null
|
||||
close()
|
||||
})
|
||||
val authorized = remember { !chatModel.controller.appPrefs.performLA.get() }
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(lastSuccessfulAuth.value) {
|
||||
if (!authorized && !authorizedPreviously(lastSuccessfulAuth)) {
|
||||
runAuth(lastSuccessfulAuth, context)
|
||||
}
|
||||
}
|
||||
if (authorized || authorizedPreviously(lastSuccessfulAuth)) {
|
||||
LaunchedEffect(Unit) {
|
||||
// Update auth each time user visits this screen in authenticated state just to prolong authorized time
|
||||
lastSuccessfulAuth.value = SystemClock.elapsedRealtime()
|
||||
}
|
||||
TerminalLayout(
|
||||
chatModel.terminalItems,
|
||||
remember { chatModel.terminalItems },
|
||||
composeState,
|
||||
sendCommand = { sendCommand(chatModel, composeState) },
|
||||
close
|
||||
)
|
||||
} else {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
||||
CloseSheetBar(close)
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
runAuth(lastSuccessfulAuth, context)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun authorizedPreviously(lastSuccessfulAuth: State<Long?>): Boolean =
|
||||
lastSuccessfulAuth.value?.let { SystemClock.elapsedRealtime() - it < 30_000 } ?: false
|
||||
|
||||
private fun runAuth(lastSuccessfulAuth: MutableState<Long?>, context: Context) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_open_chat_console),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
lastSuccessfulAuth.value = when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> SystemClock.elapsedRealtime()
|
||||
is LAResult.Error, LAResult.Failed -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) {
|
||||
@@ -103,8 +43,8 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||
val s = composeState.value.message
|
||||
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
|
||||
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty")))
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.terminalItems.add(TerminalItem.resp(resp))
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(resp))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
} else {
|
||||
withApi {
|
||||
@@ -143,6 +83,8 @@ fun TerminalLayout(
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = sendCommand,
|
||||
sendLiveMessage = null,
|
||||
@@ -175,6 +117,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
|
||||
val context = LocalContext.current
|
||||
LazyColumn(state = listState, reverseLayout = true) {
|
||||
items(reversedTerminalItems) { item ->
|
||||
Text(
|
||||
@@ -185,7 +128,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
ModalManager.shared.showModal {
|
||||
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
|
||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexService
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Profile
|
||||
import chat.simplex.app.ui.theme.*
|
||||
@@ -111,11 +111,12 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
|
||||
withApi {
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName, null)
|
||||
)
|
||||
) ?: return@withApi
|
||||
chatModel.currentUser.value = user
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
|
||||
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
chatModel.users.clear()
|
||||
|
||||
@@ -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, "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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,10 +351,10 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
|
||||
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
|
||||
InfoText(call.callState.text)
|
||||
|
||||
val connInfo =
|
||||
if (call.connectionInfo == null) ""
|
||||
else " (${call.connectionInfo.text})"
|
||||
InfoText(call.encryptionStatus + connInfo)
|
||||
val connInfo = call.connectionInfo
|
||||
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
|
||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
||||
InfoText(call.encryptionStatus + connInfoText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,8 +534,12 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Video,
|
||||
peerMedia = CallMediaType.Video,
|
||||
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
|
||||
)
|
||||
),
|
||||
speakerCanBeEnabled = true,
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
toggleVideo = {},
|
||||
@@ -501,8 +559,12 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Audio,
|
||||
peerMedia = CallMediaType.Audio,
|
||||
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
|
||||
)
|
||||
),
|
||||
speakerCanBeEnabled = true,
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
toggleVideo = {},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package chat.simplex.app.views.call
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.Contact
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
@@ -11,6 +12,8 @@ import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
data class Call(
|
||||
val contact: Contact,
|
||||
@@ -62,39 +65,39 @@ enum class CallState {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
|
||||
@Serializable
|
||||
sealed class WCallCommand {
|
||||
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
|
||||
@Serializable @SerialName("start") class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("end") object End: WCallCommand()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class WCallResponse {
|
||||
@Serializable @SerialName("capabilities") class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("answer") class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("capabilities") data class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("answer") data class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
||||
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
||||
@Serializable @SerialName("error") class Error(val message: String): WCallResponse()
|
||||
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
|
||||
}
|
||||
|
||||
@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
|
||||
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||
@Serializable class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||
@Serializable data class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
|
||||
@Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||
@Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
|
||||
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
|
||||
@@ -104,19 +107,32 @@ sealed class WCallResponse {
|
||||
CallMediaType.Audio -> R.string.incoming_audio_call
|
||||
})
|
||||
}
|
||||
@Serializable class CallCapabilities(val encryption: Boolean)
|
||||
@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||
val text: String @Composable get() = when {
|
||||
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
|
||||
stringResource(R.string.call_connection_peer_to_peer)
|
||||
localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay ->
|
||||
stringResource(R.string.call_connection_via_relay)
|
||||
else ->
|
||||
"${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}"
|
||||
@Serializable data class CallCapabilities(val encryption: Boolean)
|
||||
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||
val text: String @Composable get() {
|
||||
val local = localCandidate?.candidateType
|
||||
val remote = remoteCandidate?.candidateType
|
||||
return when {
|
||||
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
|
||||
stringResource(R.string.call_connection_peer_to_peer)
|
||||
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
|
||||
stringResource(R.string.call_connection_via_relay)
|
||||
else ->
|
||||
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
||||
}
|
||||
}
|
||||
|
||||
val protocolText: String get() {
|
||||
val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
||||
val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown"
|
||||
val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
||||
val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)"
|
||||
return if (local == remote) localText else "$localText / $remote"
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
|
||||
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
||||
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||
|
||||
@@ -151,7 +167,7 @@ enum class VideoCamera {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ConnectionState(
|
||||
data class ConnectionState(
|
||||
val connectionState: String,
|
||||
val iceConnectionState: String,
|
||||
val iceGatheringState: String,
|
||||
@@ -159,20 +175,22 @@ class ConnectionState(
|
||||
)
|
||||
|
||||
// the servers are expected in this format:
|
||||
// stun:stun.simplex.im:443
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443
|
||||
// stun:stun.simplex.im:443?transport=tcp
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
|
||||
fun parseRTCIceServer(str: String): RTCIceServer? {
|
||||
var s = replaceScheme(str, "stun:")
|
||||
s = replaceScheme(s, "turn:")
|
||||
s = replaceScheme(s, "turns:")
|
||||
val u = runCatching { URI(s) }.getOrNull()
|
||||
if (u != null) {
|
||||
val scheme = u.scheme
|
||||
val host = u.host
|
||||
val port = u.port
|
||||
if (u.path == "" && (scheme == "stun" || scheme == "turn")) {
|
||||
if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) {
|
||||
val userInfo = u.userInfo?.split(":")
|
||||
val query = if (u.query == null || u.query == "") "" else "?${u.query}"
|
||||
return RTCIceServer(
|
||||
urls = listOf("$scheme:$host:$port"),
|
||||
urls = listOf("$scheme:$host:$port$query"),
|
||||
username = userInfo?.getOrNull(0),
|
||||
credential = userInfo?.getOrNull(1)
|
||||
)
|
||||
|
||||
@@ -152,9 +152,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,19 +198,34 @@ 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -674,10 +694,18 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
||||
.distinctUntilChanged()
|
||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
||||
.collect {
|
||||
if (listState.firstVisibleItemIndex == 0) {
|
||||
listState.animateScrollToItem(0)
|
||||
} else {
|
||||
listState.animateScrollBy(scrollDistance)
|
||||
try {
|
||||
if (listState.firstVisibleItemIndex == 0) {
|
||||
listState.animateScrollToItem(0)
|
||||
} else {
|
||||
listState.animateScrollBy(scrollDistance)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
/**
|
||||
* When you tap and hold a finger on a lazy column with chatItems, and then you receive a message,
|
||||
* this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll.
|
||||
* Which breaks auto-scrolling to bottom. So just ignoring the exception
|
||||
* */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
@@ -32,6 +33,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.simplex.app.*
|
||||
@@ -645,6 +647,9 @@ fun ComposeView(
|
||||
chatModel.sharedContent.value = null
|
||||
}
|
||||
|
||||
val userCanSend = rememberUpdatedState(chat.userCanSend)
|
||||
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
|
||||
|
||||
Column {
|
||||
contextItemView()
|
||||
when {
|
||||
@@ -656,11 +661,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 +703,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 +745,8 @@ fun ComposeView(
|
||||
needToAllowVoiceToContact,
|
||||
allowedVoiceByPrefs,
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
userIsObserver = userIsObserver.value,
|
||||
userCanSend = userCanSend.value,
|
||||
sendMessage = {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
|
||||
@@ -60,6 +60,8 @@ fun SendMsgView(
|
||||
liveMessageAlertShown: SharedPreference<Boolean>,
|
||||
needToAllowVoiceToContact: Boolean,
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
userIsObserver: Boolean,
|
||||
userCanSend: Boolean,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
sendMessage: () -> Unit,
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
@@ -74,10 +76,18 @@ fun SendMsgView(
|
||||
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 +109,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 +128,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 +183,7 @@ private fun NativeKeyboard(
|
||||
composeState: MutableState<ComposeState>,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit
|
||||
) {
|
||||
val cs = composeState.value
|
||||
@@ -253,15 +264,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 +340,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 +472,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 +488,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 +589,8 @@ fun PreviewSendMsgView() {
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
@@ -599,6 +619,8 @@ fun PreviewSendMsgViewEditing() {
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
@@ -627,6 +649,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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -174,14 +174,14 @@ fun CIFileView(
|
||||
class ChatItemProvider: PreviewParameterProvider<ChatItem> {
|
||||
private val sentFile = ChatItem(
|
||||
chatDir = CIDirection.DirectSnd(),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemDeleted = false, itemEdited = true, editable = false),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemEdited = true),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCFile("")),
|
||||
quotedItem = null,
|
||||
file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete)
|
||||
)
|
||||
private val fileChatItemWtFile = ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), ),
|
||||
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile("")),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -91,6 +91,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(
|
||||
@@ -98,7 +106,7 @@ fun ChatItemView(
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
if (!cItem.meta.itemDeleted && !live) {
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
@@ -140,7 +148,7 @@ fun ChatItemView(
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
if (cItem.meta.itemDeleted && revealed.value) {
|
||||
if (cItem.meta.itemDeleted != null && revealed.value) {
|
||||
ItemAction(
|
||||
stringResource(R.string.hide_verb),
|
||||
Icons.Outlined.VisibilityOff,
|
||||
@@ -153,6 +161,10 @@ fun ChatItemView(
|
||||
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 +175,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)
|
||||
}
|
||||
}
|
||||
@@ -178,10 +192,10 @@ fun ChatItemView(
|
||||
@Composable
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted && !revealed.value) {
|
||||
if (cItem.meta.itemDeleted != null && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else if (cItem.quotedItem == null && !cItem.meta.itemDeleted && !cItem.meta.isLive) {
|
||||
} else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MsgContentItemDropdownMenu()
|
||||
@@ -238,6 +252,8 @@ 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 -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
@@ -262,6 +278,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) {
|
||||
@@ -306,6 +340,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),
|
||||
|
||||
@@ -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
|
||||
@@ -19,9 +18,10 @@ import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
val sent = ci.chatDir.sent
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = ReceivedColorLight,
|
||||
color = if (sent) SentColorLight else ReceivedColorLight,
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
|
||||
@@ -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
|
||||
@@ -28,6 +30,7 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.min
|
||||
|
||||
val SentColorLight = Color(0x1E45B8FF)
|
||||
val ReceivedColorLight = Color(0x20B1B0B5)
|
||||
@@ -74,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) {
|
||||
@@ -95,6 +95,8 @@ fun FramedItemView(
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -164,8 +166,12 @@ fun FramedItemView(
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
Column(Modifier.width(IntrinsicSize.Max)) {
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
if (ci.meta.itemDeleted) {
|
||||
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
|
||||
if (ci.meta.itemDeleted != null) {
|
||||
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)
|
||||
}
|
||||
@@ -241,6 +247,12 @@ fun CIMarkdownText(
|
||||
}
|
||||
|
||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
/**
|
||||
* Equal to [androidx.compose.ui.unit.Constraints.MaxFocusMask], which is 0x3FFFF - 1
|
||||
* Other values make a crash `java.lang.IllegalArgumentException: Can't represent a width of 123456 and height of 9909 in Constraints`
|
||||
* See [androidx.compose.ui.unit.Constraints.createConstraints]
|
||||
* */
|
||||
const val MAX_SAFE_WIDTH = 0x3FFFF - 1
|
||||
|
||||
@Composable
|
||||
fun PriorityLayout(
|
||||
@@ -248,6 +260,17 @@ fun PriorityLayout(
|
||||
priorityLayoutId: String,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
/**
|
||||
* Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints]
|
||||
* */
|
||||
fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31
|
||||
width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height
|
||||
width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height
|
||||
width < 0xFFFF /*MinFocusMask*/ -> 0x7FFF - 1 /* MinFocusMask */ // 16 bits width + 15 bits height
|
||||
width < 0x3FFFF /*MaxFocusMask*/ -> 0x1FFF - 1 /* MaxNonFocusMask */ // 18 bits width + 13 bits height
|
||||
else -> 0x1FFF // shouldn't happen since width is limited already
|
||||
}
|
||||
|
||||
Layout(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
@@ -259,9 +282,11 @@ fun PriorityLayout(
|
||||
if (it.layoutId == priorityLayoutId)
|
||||
imagePlaceable!!
|
||||
else
|
||||
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: constraints.maxWidth)) }
|
||||
// Limit width for every other element to width of important element and height for a sum of all elements
|
||||
layout(imagePlaceable?.measuredWidth ?: placeables.maxOf { it.width }, placeables.sumOf { it.height }) {
|
||||
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: min(MAX_SAFE_WIDTH, constraints.maxWidth))) }
|
||||
// Limit width for every other element to width of important element and height for a sum of all elements.
|
||||
val width = imagePlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH, placeables.maxOf { it.width })
|
||||
val height = minOf(maxSafeHeight(width), placeables.sumOf { it.height })
|
||||
layout(width, height) {
|
||||
var y = 0
|
||||
placeables.forEach {
|
||||
it.place(0, y)
|
||||
|
||||
@@ -10,10 +10,12 @@ 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
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CIDeleted
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
@@ -29,19 +31,30 @@ 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)
|
||||
)
|
||||
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,
|
||||
@@ -51,7 +64,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
|
||||
fun PreviewMarkedDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = true),
|
||||
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val switchingUsers = rememberSaveable { mutableStateOf(false) }
|
||||
Scaffold(
|
||||
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
|
||||
scaffoldState = scaffoldState,
|
||||
@@ -99,7 +100,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
ChatList(chatModel, search = searchInList)
|
||||
} else {
|
||||
} else if (!switchingUsers.value) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
OnboardingButtons(showNewChatSheet)
|
||||
@@ -113,9 +114,17 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
if (searchInList.isEmpty()) {
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
UserPicker(chatModel, userPickerState) {
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
}
|
||||
if (switchingUsers.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -123,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)
|
||||
@@ -201,7 +210,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.toList() } }
|
||||
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) {
|
||||
@@ -267,6 +276,17 @@ private fun BoxScope.unreadBadge(text: String? = "") {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -145,12 +145,12 @@ fun ChatPreviewView(
|
||||
if (ci != null) {
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
|
||||
!ci.meta.itemDeleted -> ci.text to null
|
||||
ci.meta.itemDeleted == null -> ci.text to null
|
||||
else -> generalGetString(R.string.marked_deleted_description) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
!ci.meta.itemDeleted -> ci.formattedText
|
||||
ci.meta.itemDeleted == null -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
MarkdownText(
|
||||
|
||||
@@ -9,15 +9,17 @@ 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
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
@@ -32,20 +34,17 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, openSettings: () -> Unit) {
|
||||
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, openSettings: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
val users by remember { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } }
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
if (progressIndicator) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProgressIndicator()
|
||||
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 {
|
||||
userPickerState.collect {
|
||||
@@ -105,28 +104,28 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
|
||||
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
users.forEachIndexed { i, u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount) {
|
||||
users.forEach { u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
chatModel.chats.clear()
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
progressIndicator = true
|
||||
switchingUsers.value = true
|
||||
}
|
||||
chatModel.controller.changeActiveUser(u.user.userId)
|
||||
chatModel.controller.changeActiveUser(u.user.userId, null)
|
||||
job.cancel()
|
||||
progressIndicator = false
|
||||
switchingUsers.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i != users.lastIndex) {
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
SettingsPickerItem {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
@@ -136,45 +135,71 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, onLongClick, padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProfileImage(
|
||||
image = u.image,
|
||||
size = 54.dp
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = 46.dp)
|
||||
.combinedClickable(
|
||||
onClick = if (u.activeUser) openSettings else onClick,
|
||||
onLongClick = onLongClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (!u.activeUser) LocalIndication.current else null
|
||||
)
|
||||
Text(
|
||||
u.chatViewName,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp)
|
||||
)
|
||||
}
|
||||
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
UserProfileRow(u)
|
||||
if (u.activeUser) {
|
||||
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.primary)
|
||||
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) {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
} else {
|
||||
Row {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.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),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
}
|
||||
} 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) {
|
||||
@@ -186,14 +211,3 @@ private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -206,7 +206,7 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref
|
||||
|
||||
@Composable
|
||||
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
|
||||
DatabaseKeyField(
|
||||
PassphraseField(
|
||||
text,
|
||||
generalGetString(R.string.enter_passphrase),
|
||||
isValid = ::validKey,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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
|
||||
@@ -11,7 +12,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: () -> Unit) {
|
||||
fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -20,9 +21,19 @@ fun CloseSheetBar(close: () -> Unit) {
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.width(TitleInsetWithIcon - AppBarHorizontalPadding)
|
||||
.padding(top = 4.dp), // Like in DefaultAppBar
|
||||
content = { NavigationButtonBack(close) }
|
||||
content = {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NavigationButtonBack(close)
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,15 @@ fun NavigationButtonBack(onButtonClicked: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareButton(onButtonClicked: () -> Unit) {
|
||||
IconButton(onButtonClicked) {
|
||||
Icon(
|
||||
Icons.Outlined.Share, stringResource(R.string.share_verb), tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.*
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.*
|
||||
import android.graphics.ImageDecoder.DecodeException
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.util.Base64
|
||||
@@ -205,21 +206,22 @@ fun GetImageBottomSheet(
|
||||
val processPickedImage = { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val source = ImageDecoder.createSource(context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
imageBitmap.value = uri
|
||||
onImageChange(bitmap)
|
||||
}
|
||||
}
|
||||
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it) }
|
||||
val galleryLauncherFallback = rememberGetContentLauncher { processPickedImage(it) }
|
||||
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
imageBitmap.value = uri
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
onImageChange(bitmap)
|
||||
try {
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
imageBitmap.value = uri
|
||||
onImageChange(bitmap)
|
||||
} catch (e: DecodeException) {
|
||||
Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.image_decoding_exception_title),
|
||||
text = generalGetString(R.string.image_decoding_exception_desc)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage)
|
||||
val galleryLauncherFallback = rememberGetContentLauncher(processPickedImage)
|
||||
val cameraLauncher = rememberCameraLauncher(processPickedImage)
|
||||
val permissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
cameraLauncher.launchWithFallback()
|
||||
|
||||
@@ -20,12 +20,13 @@ fun ModalView(
|
||||
close: () -> Unit,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
modifier: Modifier = Modifier,
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.background(background)) {
|
||||
CloseSheetBar(close)
|
||||
CloseSheetBar(close, endButtons)
|
||||
Box(modifier) { content() }
|
||||
}
|
||||
}
|
||||
@@ -37,9 +38,9 @@ class ModalManager {
|
||||
private val toRemove = mutableSetOf<Int>()
|
||||
private var oldViewChanging = AtomicBoolean(false)
|
||||
|
||||
fun showModal(settings: Boolean = false, content: @Composable () -> Unit) {
|
||||
fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
|
||||
showCustomModal { close ->
|
||||
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, content = content)
|
||||
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, endButtons = endButtons, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,11 +1,16 @@
|
||||
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.net.Uri
|
||||
import android.os.FileUtils
|
||||
import android.os.*
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
@@ -28,8 +33,7 @@ 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.model.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
@@ -374,6 +378,24 @@ fun saveAnimImage(context: Context, uri: Uri): String? {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? {
|
||||
return try {
|
||||
val ext = if (asPng) "png" else "jpg"
|
||||
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
|
||||
return File(tmpDir.absolutePath + File.separator + generateNewFileName(SimplexApp.context, "IMG", ext)).apply {
|
||||
outputStream().use { out ->
|
||||
image.compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out)
|
||||
out.flush()
|
||||
}
|
||||
deleteOnExit()
|
||||
SimplexApp.context.chatModel.filesToDelete.add(this)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun saveFileFromUri(context: Context, uri: Uri): String? {
|
||||
return try {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
@@ -482,3 +504,42 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,100 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import boofcv.alg.fiducial.qrcode.QrCodeEncoder
|
||||
import boofcv.alg.fiducial.qrcode.QrCodeGeneratorImage
|
||||
import androidx.core.graphics.*
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import boofcv.alg.drawing.FiducialImageEngine
|
||||
import boofcv.alg.fiducial.qrcode.*
|
||||
import boofcv.android.ConvertBitmap
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun QRCode(connReq: String, modifier: Modifier = Modifier) {
|
||||
Image(
|
||||
bitmap = qrCodeBitmap(connReq, 1024).asImageBitmap(),
|
||||
contentDescription = stringResource(R.string.image_descr_qr_code),
|
||||
modifier = modifier
|
||||
)
|
||||
fun QRCode(
|
||||
connReq: String,
|
||||
modifier: Modifier = Modifier,
|
||||
tintColor: Color = Color(0xff062d56),
|
||||
withLogo: Boolean = true
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
BoxWithConstraints {
|
||||
val maxWidthInPx = with(LocalDensity.current) { maxWidth.roundToPx() }
|
||||
val qr = remember(maxWidthInPx, connReq, tintColor, withLogo) {
|
||||
qrCodeBitmap(connReq, maxWidthInPx).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
|
||||
.let { if (withLogo) it.addLogo() else it }
|
||||
.asImageBitmap()
|
||||
}
|
||||
Image(
|
||||
bitmap = qr,
|
||||
contentDescription = stringResource(R.string.image_descr_qr_code),
|
||||
modifier
|
||||
.clickable {
|
||||
scope.launch {
|
||||
val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb())
|
||||
.let { if (withLogo) it.addLogo() else it }
|
||||
val file = saveTempImageUncompressed(image, false)
|
||||
if (file != null) {
|
||||
shareFile(context, "", file.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun qrCodeBitmap(content: String, size: Int): Bitmap {
|
||||
val qrCode = QrCodeEncoder().addAutomatic(content).fixate()
|
||||
val renderer = QrCodeGeneratorImage(5)
|
||||
fun qrCodeBitmap(content: String, size: Int = 1024): Bitmap {
|
||||
val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate()
|
||||
/** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */
|
||||
val numModules = QrCode.totalModules(qrCode.version)
|
||||
val borderModule = 1
|
||||
// val calculatedFinalWidth = (pixelsPerModule * numModules) + 2 * (borderModule * pixelsPerModule)
|
||||
// size = (x * numModules) + 2 * (borderModule * x)
|
||||
// size / x = numModules + 2 * borderModule
|
||||
// x = size / (numModules + 2 * borderModule)
|
||||
val pixelsPerModule = size / (numModules + 2 * borderModule)
|
||||
// + 1 to make it with better quality
|
||||
val renderer = QrCodeGeneratorImage(pixelsPerModule + 1)
|
||||
renderer.borderModule = borderModule
|
||||
renderer.render(qrCode)
|
||||
return ConvertBitmap.grayToBitmap(renderer.gray, Bitmap.Config.RGB_565)
|
||||
return ConvertBitmap.grayToBitmap(renderer.gray, Bitmap.Config.RGB_565).scale(size, size)
|
||||
}
|
||||
|
||||
fun Bitmap.replaceColor(from: Int, to: Int): Bitmap {
|
||||
val pixels = IntArray(width * height)
|
||||
getPixels(pixels, 0, width, 0, 0, width, height)
|
||||
var i = 0
|
||||
while (i < pixels.size) {
|
||||
if (pixels[i] == from) {
|
||||
pixels[i] = to
|
||||
}
|
||||
i++
|
||||
}
|
||||
setPixels(pixels, 0, width, 0, 0, width, height)
|
||||
return this
|
||||
}
|
||||
|
||||
fun Bitmap.addLogo(): Bitmap = applyCanvas {
|
||||
val radius = (width * 0.16f) / 2
|
||||
val paint = android.graphics.Paint()
|
||||
paint.color = android.graphics.Color.WHITE
|
||||
drawCircle(width / 2f, height / 2f, radius, paint)
|
||||
val logo = SimplexApp.context.resources.getDrawable(R.mipmap.icon_foreground, null).toBitmap()
|
||||
val logoSize = (width * 0.24).toInt()
|
||||
translate((width - logoSize) / 2f, (height - logoSize) / 2f)
|
||||
drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -226,10 +226,89 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
icon = Icons.Outlined.VerifiedUser,
|
||||
titleId = R.string.v4_4_verify_connection_security,
|
||||
descrId = R.string.v4_4_verify_connection_security_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v4_4_french_interface,
|
||||
descrId = R.string.v4_4_french_interface_descr
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.5",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.ManageAccounts,
|
||||
titleId = R.string.v4_5_multiple_chat_profiles,
|
||||
descrId = R.string.v4_5_multiple_chat_profiles_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.EditNote,
|
||||
titleId = R.string.v4_5_message_draft,
|
||||
descrId = R.string.v4_5_message_draft_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.SafetyDivider,
|
||||
titleId = R.string.v4_5_transport_isolation,
|
||||
descrId = R.string.v4_5_transport_isolation_descr,
|
||||
link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Task,
|
||||
titleId = R.string.v4_5_private_filenames,
|
||||
descrId = R.string.v4_5_private_filenames_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Battery2Bar,
|
||||
titleId = R.string.v4_5_reduced_battery_usage,
|
||||
descrId = R.string.v4_5_reduced_battery_usage_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v4_5_italian_interface,
|
||||
descrId = R.string.v4_5_italian_interface_descr,
|
||||
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
|
||||
)
|
||||
)
|
||||
),
|
||||
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
|
||||
@@ -24,17 +32,19 @@ import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
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),
|
||||
@@ -42,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) {
|
||||
@@ -64,12 +74,8 @@ fun AppearanceView() {
|
||||
|
||||
AppearanceLayout(
|
||||
appIcon,
|
||||
m.controller.appPrefs.appLanguage,
|
||||
changeIcon = ::setAppIcon,
|
||||
showThemeSelector = {
|
||||
ModalManager.shared.showModal(true) {
|
||||
ThemeSelectorView()
|
||||
}
|
||||
},
|
||||
editPrimaryColor = { primary ->
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ColorEditor(primary, close)
|
||||
@@ -80,8 +86,8 @@ fun AppearanceView() {
|
||||
|
||||
@Composable fun AppearanceLayout(
|
||||
icon: MutableState<AppIcon>,
|
||||
languagePref: SharedPreference<String?>,
|
||||
changeIcon: (AppIcon) -> Unit,
|
||||
showThemeSelector: () -> Unit,
|
||||
editPrimaryColor: (Color) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
@@ -89,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 ->
|
||||
@@ -115,8 +152,12 @@ fun AppearanceView() {
|
||||
SectionSpacer()
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
SectionView(stringResource(R.string.settings_section_title_themes)) {
|
||||
SectionItemViewSpaceBetween(showThemeSelector) {
|
||||
Text(generalGetString(R.string.theme))
|
||||
SectionItemViewSpaceBetween {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val state = remember { derivedStateOf { currentTheme.second } }
|
||||
ThemeSelector(state) {
|
||||
ThemeManager.applyTheme(it.name, darkTheme)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }) {
|
||||
@@ -183,6 +224,50 @@ 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()
|
||||
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second to it.third }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.theme),
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
//private fun 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(
|
||||
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
|
||||
@@ -195,8 +280,8 @@ fun PreviewAppearanceSettings() {
|
||||
SimpleXTheme {
|
||||
AppearanceLayout(
|
||||
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
|
||||
languagePref = SharedPreference({ null }, {}),
|
||||
changeIcon = {},
|
||||
showThemeSelector = {},
|
||||
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.*
|
||||
@@ -9,6 +10,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -42,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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +122,7 @@ fun SharedPreferenceToggleWithIcon(
|
||||
) {
|
||||
val prefState = preferenceState ?: remember { mutableStateOf(preference.get()) }
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text, Modifier.padding(end = 4.dp))
|
||||
Text(text, Modifier.padding(end = 4.dp), color = if (stopped) HighOrLowlight else Color.Unspecified)
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
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 confirmValid by remember { derivedStateOf { confirmHidePassword.value == "" || hidePassword.value == confirmHidePassword.value } }
|
||||
val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || confirmHidePassword.value == "" || !confirmValid } }
|
||||
SectionView(stringResource(R.string.hidden_profile_password).uppercase()) {
|
||||
SectionItemView {
|
||||
PassphraseField(hidePassword, generalGetString(R.string.password_to_show), isValid = { true }, 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
|
||||
@@ -51,8 +50,6 @@ fun PrivacySettingsView(
|
||||
SectionView(stringResource(R.string.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.ImageAspectRatio, stringResource(R.string.transfer_images_faster), chatModel.controller.appPrefs.privacyTransferImagesInline)
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
SectionDivider()
|
||||
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.icu.util.VersionInfo
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
@@ -13,18 +14,21 @@ 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
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.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.*
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -45,6 +49,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
MaintainIncognitoState(chatModel)
|
||||
|
||||
if (user != null) {
|
||||
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state }
|
||||
val context = LocalContext.current
|
||||
SettingsLayout(
|
||||
profile = user.profile,
|
||||
stopped,
|
||||
@@ -56,8 +62,27 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
setPerformLA = setPerformLA,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
|
||||
showSettingsModalWithSearch = { modalView ->
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
val search = mutableStateOf("")
|
||||
var showSearch by remember { mutableStateOf(false) }
|
||||
ModalView(
|
||||
{ if (showSearch) { showSearch = false } else close() },
|
||||
if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight,
|
||||
endButtons = {
|
||||
if (!showSearch) {
|
||||
IconButton({ showSearch = true }) {
|
||||
Icon(Icons.Outlined.Search, stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
} else {
|
||||
BackHandler { showSearch = false }
|
||||
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go)) { search.value = it }
|
||||
}
|
||||
},
|
||||
content = { modalView(chatModel, search) })
|
||||
}
|
||||
},
|
||||
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } },
|
||||
showVersion = {
|
||||
withApi {
|
||||
val info = chatModel.controller.apiGetVersion()
|
||||
@@ -65,7 +90,36 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
ModalManager.shared.showModal { VersionInfoView(info) }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
withAuth = { block ->
|
||||
if (!requireAuth.value) {
|
||||
block()
|
||||
} else {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
val onFinishAuth = { success: Boolean ->
|
||||
if (success) {
|
||||
close()
|
||||
block()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
runAuth(context, onFinishAuth)
|
||||
}
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
runAuth(context, onFinishAuth)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,16 +127,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
val simplexTeamUri =
|
||||
"simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
|
||||
|
||||
// TODO pass close
|
||||
//fun showSectionedModal(chatModel: ChatModel, modalView: (@Composable (ChatModel) -> Unit)) {
|
||||
// ModalManager.shared.showCustomModal { close ->
|
||||
// ModalView(close = close, modifier = Modifier,
|
||||
// background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) {
|
||||
// modalView(chatModel)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
profile: LocalProfile,
|
||||
@@ -95,9 +139,10 @@ fun SettingsLayout(
|
||||
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),
|
||||
showTerminal: () -> Unit,
|
||||
showVersion: () -> Unit
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (block: () -> Unit) -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Surface(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
||||
@@ -121,26 +166,27 @@ fun SettingsLayout(
|
||||
ProfilePreview(profile, stopped = stopped)
|
||||
}
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.HowToReg, stringResource(R.string.your_chat_profiles), 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()
|
||||
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
ChatPreferencesItem(showCustomModal)
|
||||
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -153,9 +199,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()
|
||||
|
||||
@@ -173,7 +219,7 @@ fun SettingsLayout(
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
|
||||
SectionDivider()
|
||||
if (devTools.value) {
|
||||
ChatConsoleItem(showTerminal)
|
||||
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
@@ -250,17 +296,18 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit))) {
|
||||
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.ToggleOn,
|
||||
stringResource(R.string.chat_preferences),
|
||||
click = {
|
||||
click = if (stopped) null else ({
|
||||
withApi {
|
||||
showCustomModal { m, close ->
|
||||
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}),
|
||||
disabled = stopped
|
||||
)
|
||||
}
|
||||
|
||||
@@ -292,7 +339,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",
|
||||
@@ -305,8 +352,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(
|
||||
@@ -320,7 +367,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",
|
||||
@@ -344,7 +391,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable private fun InstallTerminalAppItem(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",
|
||||
@@ -418,7 +465,7 @@ fun SettingsPreferenceItemWithInfo(
|
||||
pref: SharedPreference<Boolean>,
|
||||
prefState: MutableState<Boolean>? = null
|
||||
) {
|
||||
SectionItemView(onClickInfo) {
|
||||
SectionItemView(if (stopped) null else onClickInfo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, text, tint = if (stopped) HighOrLowlight else iconTint)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
@@ -479,6 +526,17 @@ fun PreferenceToggleWithIcon(
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_open_chat_console),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
@@ -499,9 +557,10 @@ fun PreviewSettingsLayout() {
|
||||
setPerformLA = {},
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showSettingsModalWithSearch = { },
|
||||
showCustomModal = { {} },
|
||||
showTerminal = {},
|
||||
showVersion = {}
|
||||
showVersion = {},
|
||||
withAuth = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionViewSelectable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun ThemeSelectorView() {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val allThemes by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { ValueTitleDesc(it.second, it.third, "") }) }
|
||||
|
||||
ThemeSelectorLayout(
|
||||
allThemes,
|
||||
onSelectTheme = {
|
||||
ThemeManager.applyTheme(it.name, darkTheme)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeSelectorLayout(
|
||||
allThemes: List<ValueTitleDesc<DefaultTheme>>,
|
||||
onSelectTheme: (DefaultTheme) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.settings_section_title_themes).lowercase().capitalize(Locale.current))
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
val state = remember { derivedStateOf { currentTheme.second } }
|
||||
SectionViewSelectable(null, state, allThemes, onSelectTheme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
@@ -10,6 +11,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -17,18 +19,28 @@ 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.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 +48,72 @@ fun UserProfilesView(m: ChatModel) {
|
||||
},
|
||||
activateUser = { user ->
|
||||
withBGApi {
|
||||
m.controller.changeActiveUser(user.userId)
|
||||
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value))
|
||||
}
|
||||
},
|
||||
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)
|
||||
}) {
|
||||
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)
|
||||
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)
|
||||
}) {
|
||||
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
removeUser(m, user, users, false, searchTextOrPassword.value)
|
||||
}
|
||||
) {
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
unhideUser = { user ->
|
||||
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, userViewPassword(user, searchTextOrPassword.value)) }
|
||||
},
|
||||
muteUser = { user ->
|
||||
setUserPrivacy(m, onSuccess = { if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert) }) {
|
||||
m.controller.apiMuteUser(user.userId, userViewPassword(user, searchTextOrPassword.value))
|
||||
}
|
||||
},
|
||||
unmuteUser = { user ->
|
||||
setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId, userViewPassword(user, searchTextOrPassword.value)) }
|
||||
},
|
||||
showHiddenProfile = { user ->
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
HiddenProfileView(m, user) {
|
||||
profileHidden.value = true
|
||||
withBGApi {
|
||||
delay(10_000)
|
||||
profileHidden.value = false
|
||||
}
|
||||
close()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -75,9 +121,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 +140,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.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 +203,103 @@ 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.Notifications, onClick = {
|
||||
showDropdownMenu = false
|
||||
muteUser(user)
|
||||
})
|
||||
} else {
|
||||
ItemAction(stringResource(R.string.user_unmute), Icons.Outlined.NotificationsOff, 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) {
|
||||
private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
|
||||
val s = searchTextOrPassword.trim()
|
||||
val lower = s.lowercase()
|
||||
return m.users.filter { u ->
|
||||
if ((u.user.activeUser || u.user.viewPwdHash == null) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) {
|
||||
true
|
||||
} else if (u.user.viewPwdHash != null) {
|
||||
s != "" && chatPasswordHash(s, u.user.viewPwdHash.salt) == u.user.viewPwdHash.hash
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}.map { it.user }
|
||||
}
|
||||
|
||||
private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size
|
||||
|
||||
private fun userViewPassword(user: User, searchTextOrPassword: String): String? =
|
||||
if (user.activeUser || !user.hidden) null else searchTextOrPassword
|
||||
|
||||
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, searchTextOrPassword: String) {
|
||||
if (users.size < 2) return
|
||||
|
||||
withBGApi {
|
||||
suspend fun deleteUser(user: User) {
|
||||
m.controller.apiDeleteUser(user.userId, delSMPQueues, userViewPassword(user, searchTextOrPassword))
|
||||
m.removeUser(user)
|
||||
}
|
||||
try {
|
||||
if (user.activeUser) {
|
||||
val newActive = users.first { !it.activeUser }
|
||||
m.controller.changeActiveUser_(newActive.userId)
|
||||
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
|
||||
if (newActive != null) {
|
||||
m.controller.changeActiveUser_(newActive.userId, null)
|
||||
deleteUser(user)
|
||||
}
|
||||
} else {
|
||||
deleteUser(user)
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUserPrivacy(m: ChatModel, onSuccess: (() -> Unit)? = null, api: suspend () -> User) {
|
||||
withBGApi {
|
||||
try {
|
||||
m.updateUser(api())
|
||||
onSuccess?.invoke()
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.error_updating_user_privacy),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private 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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -24,6 +24,7 @@ fun VersionInfoView(info: CoreVersionInfo) {
|
||||
Text(String.format(stringResource(R.string.app_version_code), BuildConfig.VERSION_CODE))
|
||||
Text(String.format(stringResource(R.string.core_version), info.version))
|
||||
Text(String.format(stringResource(R.string.core_build_timestamp), info.buildTimestamp))
|
||||
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, info.simplexmqCommit.substring(startIndex = 0, endIndex = 7)))
|
||||
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
|
||||
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
|
||||
}
|
||||
}
|
||||
|
||||
|
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,29 @@
|
||||
<?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>
|
||||
</resources>
|
||||
@@ -328,11 +328,12 @@
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Zeigen Sie Ihrem Kontakt den QR-Code aus der App zum Scannen.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs anzeigen</b> oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird\nan Ihren Kontakt gesendet.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird
|
||||
\nan Ihren Kontakt gesendet</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.</string>
|
||||
<string name="share_invitation_link">Einladungslink teilen</string>
|
||||
<string name="paste_connection_link_below_to_connect">Fügen Sie den erhaltenen Link in das Feld unten ein, um sich mit Ihrem Kontakt zu verbinden.</string>
|
||||
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet.</string>
|
||||
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet</string>
|
||||
<!-- PasteToConnect.kt -->
|
||||
<string name="connect_via_link">Über einen Link verbinden</string>
|
||||
<string name="connect_button">Verbinden</string>
|
||||
@@ -426,7 +427,7 @@
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Angezeigter Name:</string>
|
||||
<string name="full_name__field">"Vollständiger Name:</string>
|
||||
<string name="your_current_profile">Mein Chat-Profil</string>
|
||||
<string name="your_current_profile">Mein aktuelles Chat-Profil</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
|
||||
<string name="edit_image">Bild bearbeiten</string>
|
||||
<string name="delete_image">Bild löschen</string>
|
||||
@@ -512,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>
|
||||
@@ -558,7 +561,6 @@
|
||||
<string name="your_privacy">Meine Privatsphäre</string>
|
||||
<string name="protect_app_screen">App-Bildschirm schützen</string>
|
||||
<string name="auto_accept_images">Bilder automatisch akzeptieren</string>
|
||||
<string name="transfer_images_faster">Bilder schneller übertragen</string>
|
||||
<string name="send_link_previews">Link-Vorschau senden</string>
|
||||
<string name="full_backup">App-Datensicherung</string>
|
||||
<!-- Settings sections -->
|
||||
@@ -598,14 +600,15 @@
|
||||
<string name="error_stopping_chat">Fehler beim Beenden des Chats</string>
|
||||
<string name="error_exporting_chat_database">Fehler beim Exportieren der Chat-Datenbank</string>
|
||||
<string name="import_database_question">Chat-Datenbank importieren?</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.
|
||||
\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="import_database_confirmation">Importieren</string>
|
||||
<string name="error_deleting_database">Fehler beim Löschen der Chat-Datenbank</string>
|
||||
<string name="error_importing_database">Fehler beim Importieren der Chat-Datenbank</string>
|
||||
<string name="chat_database_imported">Chat-Datenbank importiert</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden.</string>
|
||||
<string name="delete_chat_profile_question">Chat-Profil löschen?</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="chat_database_deleted">Chat-Datenbank gelöscht</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Starten Sie die App neu, um ein neues Chat-Profil zu erstellen.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</string>
|
||||
@@ -991,4 +994,107 @@
|
||||
<string name="feature_offered_item">beginne %s</string>
|
||||
<string name="feature_offered_item_with_param">beginne %s: %2s</string>
|
||||
<string name="feature_cancelled_item">beende %s</string>
|
||||
<string name="core_simplexmq_version">simplexmq Version: v%s (%2s)</string>
|
||||
<string name="delete_files_and_media_all">Alle Dateien löschen</string>
|
||||
<string name="messages_section_title">Nachrichten</string>
|
||||
<string name="app_version_code">App Build: %s</string>
|
||||
<string name="app_version_title">App Version</string>
|
||||
<string name="app_version_name">App Version: v%s</string>
|
||||
<string name="core_build_timestamp">Core übersetzt am: %s</string>
|
||||
<string name="core_version">Core Version: v%s</string>
|
||||
<string name="users_add">Profil hinzufügen</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="users_delete_profile_for">Chat-Profil löschen für</string>
|
||||
<string name="network_option_ping_count">PING Zähler</string>
|
||||
<string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string>
|
||||
<string name="smp_servers_per_user">Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil</string>
|
||||
<string name="files_and_media_section">Dateien & Medien</string>
|
||||
<string name="network_session_mode_transport_isolation">Transport-Isolation</string>
|
||||
<string name="users_delete_question">Chat-Profil löschen\?</string>
|
||||
<string name="error_deleting_user">Fehler beim Löschen des Benutzerprofils</string>
|
||||
<string name="your_chat_profiles">Meine Chat-Profile</string>
|
||||
<string name="network_session_mode_entity">Verbindung</string>
|
||||
<string name="network_session_mode_user">Chat-Profil</string>
|
||||
<string name="delete_files_and_media_for_all_users">Dateien für alle Chat-Profile löschen</string>
|
||||
<string name="network_session_mode_entity_description"><b>Für jeden Kontakt und jedes Gruppenmitglied</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.
|
||||
\n
|
||||
\n<b>Bitte beachten Sie</b>: Wenn Sie viele Verbindung haben, kann der Batterieverbrauch und die Datennutzung wesentlich höher sein und einige Verbindungen können scheitern.</string>
|
||||
<string name="network_session_mode_user_description"><b>Für jedes von Ihnen in der App genutzte Chat-Profil</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.</string>
|
||||
<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>
|
||||
<string name="failed_to_create_user_duplicate_desc">Sie haben schon ein Chat-Profil mit dem gleichen Anzeigenamen. Bitte wählen Sie einen anderen Namen aus.</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Mehrere Chat-Profile</string>
|
||||
<string name="v4_5_reduced_battery_usage">Reduzierter Batterieverbrauch</string>
|
||||
<string name="v4_5_private_filenames">Neutrale Dateinamen</string>
|
||||
<string name="v4_5_message_draft_descr">Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren.</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per Chat-Profil (Voreinstellung) oder per Verbindung (BETA).</string>
|
||||
<string name="v4_5_italian_interface">Italienische Bedienoberfläche</string>
|
||||
<string name="v4_4_french_interface">Französische Bedienoberfläche</string>
|
||||
<string name="v4_5_message_draft">Nachrichtenentwurf</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Unterschiedliche Namen, Avatare und Transport-Isolation.</string>
|
||||
<string name="v4_5_transport_isolation">Transport-Isolation</string>
|
||||
<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">Bestätigen Sie das Passwort</string>
|
||||
<string name="cant_delete_user_profile">Das Benutzerprofil kann nicht gelöscht werden!</string>
|
||||
<string name="dont_show_again">Nicht nochmals anzeigen</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Chinesische und spanische Bedienoberfläche</string>
|
||||
<string name="v4_6_audio_video_calls">Audio- und Videoanrufe</string>
|
||||
<string name="button_add_welcome_message">Fügen Sie eine Begrüßungsmeldung hinzu</string>
|
||||
<string name="error_updating_user_privacy">Fehler beim Aktualisieren der Benutzer-Privatsphäre</string>
|
||||
<string name="smp_save_servers_question">Alle Server speichern\?</string>
|
||||
<string name="hide_profile">Verberge das Profil</string>
|
||||
<string name="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">Geben Sie oben das Passwort für die Anzeige an!</string>
|
||||
<string name="make_profile_private">Erzeugen Sie ein privates Profil!</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">Legen Sie die Nachricht fest, die neuen Mitgliedern angezeigt werden soll!</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
|
||||
<string name="v4_6_group_moderation">Gruppenmoderation</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Verborgene Chat-Profile</string>
|
||||
<string name="user_hide">Verberge</string>
|
||||
<string name="save_and_update_group_profile">Sichern und aktualisieren des Gruppen-Profils</string>
|
||||
<string name="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>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -143,7 +143,7 @@
|
||||
<string name="group_preview_join_as">rejoindre en tant que %s</string>
|
||||
<string name="group_preview_you_are_invited">vous êtes invité·e au groupe</string>
|
||||
<string name="chat_with_developers">Discuter avec les développeurs</string>
|
||||
<string name="tap_to_start_new_chat">Appuyez pour commencer un nouveau chat</string>
|
||||
<string name="tap_to_start_new_chat">Appuyez ici pour démarrer une nouvelle discussion</string>
|
||||
<string name="you_have_no_chats">Vous n\'avez aucune discussion</string>
|
||||
<string name="images_limit_title">Trop d’images !</string>
|
||||
<string name="share_file">Partager le fichier…</string>
|
||||
@@ -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>
|
||||
@@ -623,7 +625,6 @@
|
||||
<string name="privacy_and_security">Vie privée et sécurité</string>
|
||||
<string name="protect_app_screen">Protéger l\'écran de l\'app</string>
|
||||
<string name="auto_accept_images">Images auto-acceptées</string>
|
||||
<string name="transfer_images_faster">Transfert d\'images plus rapide</string>
|
||||
<string name="full_backup">Sauvegarde des données de l\'app</string>
|
||||
<string name="settings_section_title_you">VOUS</string>
|
||||
<string name="settings_section_title_help">AIDE</string>
|
||||
@@ -927,4 +928,99 @@
|
||||
<string name="core_build_timestamp">Cœur compilé le : %s</string>
|
||||
<string name="core_version">Version du cœur : v%s</string>
|
||||
<string name="network_option_ping_count">Nombre de PING</string>
|
||||
<string name="users_delete_all_chats_deleted">Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !</string>
|
||||
<string name="delete_files_and_media_all">Effacer tous les fichiers</string>
|
||||
<string name="users_delete_profile_for">Supprimer le profil de chat pour</string>
|
||||
<string name="network_session_mode_user_description">Une connexion TCP distincte (et un identifiant SOCKS) sera utilisée <b>pour chaque profil de chat que vous avez dans l\'application</b>.</string>
|
||||
<string name="users_delete_question">Supprimer le profil du chat \?</string>
|
||||
<string name="files_and_media_section">Fichiers & médias</string>
|
||||
<string name="messages_section_title">Messages</string>
|
||||
<string name="smp_servers_per_user">Les serveurs pour les nouvelles connexions de votre profil de chat actuel</string>
|
||||
<string name="messages_section_description">Ce paramètre s\'applique aux messages de votre profil de chat actuel</string>
|
||||
<string name="network_session_mode_entity">Connexion</string>
|
||||
<string name="delete_files_and_media_for_all_users">Effacer les fichiers de tous les profils de chat</string>
|
||||
<string name="users_delete_with_connections">Profil et connexions au serveur</string>
|
||||
<string name="network_session_mode_transport_isolation">Isolement du transport</string>
|
||||
<string name="update_network_session_mode_question">Mettre à jour le mode d\'isolation du transport \?</string>
|
||||
<string name="your_chat_profiles_stored_locally">Vos profils de chat sont stockés localement, uniquement sur votre appareil</string>
|
||||
<string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>.
|
||||
\n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.</string>
|
||||
<string name="network_session_mode_user">Profil de chat</string>
|
||||
<string name="users_add">Ajouter un profil</string>
|
||||
<string name="users_delete_data_only">Données de profil local uniquement</string>
|
||||
<string name="error_deleting_user">Erreur lors de la suppression du profil utilisateur</string>
|
||||
<string name="your_chat_profiles">Vos profils de chat</string>
|
||||
<string name="failed_to_active_user_title">Erreur lors du changement de profil !</string>
|
||||
<string name="failed_to_create_user_title">Erreur lors de la création du profil !</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Vous avez déjà un profil de chat avec ce même nom affiché. Veuillez choisir un autre nom.</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Nom d\'affichage en double !</string>
|
||||
<string name="v4_4_french_interface">Interface en français</string>
|
||||
<string name="v4_5_transport_isolation_descr">Par profil de chat (par défaut) ou par connexion (BETA).</string>
|
||||
<string name="v4_5_italian_interface">Interface en italien</string>
|
||||
<string name="v4_5_message_draft">Brouillon de message</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Plus d\'améliorations à venir !</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Différents profils de chat</string>
|
||||
<string name="v4_5_message_draft_descr">Conserver le brouillon du dernier message, avec les pièces jointes.</string>
|
||||
<string name="v4_5_reduced_battery_usage">Réduction de la consommation de batterie</string>
|
||||
<string name="v4_5_private_filenames">Noms de fichiers privés</string>
|
||||
<string name="v4_5_italian_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
<string name="v4_4_french_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
<string name="v4_5_private_filenames_descr">Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC.</string>
|
||||
<string name="v4_5_transport_isolation">Isolation du transport</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Différents noms, avatars et mode d\'isolation de transport.</string>
|
||||
<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 ci-dessus pour continuer !</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 le profil privé !</string>
|
||||
<string name="user_mute">Mute</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Protégez vos profils de chat par un mot de passe !</string>
|
||||
<string name="tap_to_activate_profile">Appuyez pour activer le profil.</string>
|
||||
<string name="save_and_update_group_profile">Sauvegarder et mettre à jour le profil du groupe</string>
|
||||
<string name="save_profile_password">Enregistrer le mot de passe du profil</string>
|
||||
<string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez un mot de passe complet dans le champ de recherche de la page Profils de chat.</string>
|
||||
<string name="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>
|
||||
</resources>
|
||||
@@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="share_image">चित्र साझा करें…</string>
|
||||
<string name="chat_preferences_off">बंद</string>
|
||||
<string name="accept_feature_set_1_day">1 दिन निर्धारित करें</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">क्यूआर संहिता स्कैन करके सर्वर जोड़ें।</string>
|
||||
<string name="group_preview_you_are_invited">आपको समूह में आमंत्रित किया जाता है</string>
|
||||
<string name="icon_descr_server_status_connected">जुड़े हुए</string>
|
||||
<string name="use_camera_button">कैमरे का प्रयोग करें</string>
|
||||
<string name="above_then_preposition_continuation">ऊपर,तब:</string>
|
||||
<string name="accept_contact_button">स्वीकार करना</string>
|
||||
<string name="connect_button">जुडिये</string>
|
||||
<string name="your_contact_address">आपका संपर्क पता</string>
|
||||
<string name="smp_servers_add_to_another_device">दूसरे उपकरण में जोड़ें</string>
|
||||
<string name="bold">निडर</string>
|
||||
<string name="answer_call">कॉल का उत्तर दें</string>
|
||||
<string name="settings_section_title_you">तुम</string>
|
||||
<string name="settings_section_title_settings">समायोजन</string>
|
||||
<string name="chat_item_ttl_month">1 महीना</string>
|
||||
<string name="rcv_group_event_member_connected">जुड़े हुए</string>
|
||||
<string name="group_member_role_admin">व्यवस्थापक</string>
|
||||
<string name="all_group_members_will_remain_connected">समूह के सभी सदस्य जुड़े रहेंगे।</string>
|
||||
<string name="change_verb">परिवर्तन</string>
|
||||
<string name="sending_via">माध्यम से भेजा जा रहा है</string>
|
||||
<string name="feature_off">बंद</string>
|
||||
<string name="whats_new">नया क्या है</string>
|
||||
<string name="v4_2_group_links_desc">व्यवस्थापक समूहों में शामिल होने के लिए लिंक बना सकते हैं।</string>
|
||||
<string name="chat_item_ttl_day">1 दिन</string>
|
||||
<string name="chat_item_ttl_week">1 सप्ताह</string>
|
||||
<string name="about_simplex">सिंपलएक्स के बारे में</string>
|
||||
<string name="about_simplex_chat">बारे में <xliff:g id="appNameFull">सिंप्लेक्स चैट</xliff:g></string>
|
||||
<string name="accept_call_on_lock_screen">स्वीकार करना</string>
|
||||
<string name="accept">स्वीकार करना</string>
|
||||
<string name="accept_feature">स्वीकार करना</string>
|
||||
<string name="accept_connection_request__question">संबंध अनुरोध स्वीकार करें\?</string>
|
||||
<string name="callstatus_accepted">स्वीकृत कॉल</string>
|
||||
<string name="accept_contact_incognito_button">गुप्त स्वीकार करें</string>
|
||||
<string name="accept_requests">निवेदन स्वीकार करो</string>
|
||||
<string name="smp_servers_preset_add">पूर्वनिर्धारित सर्वर जोड़ें</string>
|
||||
<string name="users_add">प्रोफ़ाइल जोड़ें</string>
|
||||
<string name="smp_servers_add">सर्वर जोड़े…</string>
|
||||
<string name="notifications_mode_service">हमेशा बने रहें</string>
|
||||
<string name="attach">संलग्न करना</string>
|
||||
<string name="network_settings">उन्नत संजाल समायोजन</string>
|
||||
<string name="users_delete_all_chats_deleted">सभी बातचीत और संदेश हटा दिए जाएंगे - इसे पूर्ववत नहीं किया जा सकता!</string>
|
||||
<string name="chat_preferences_always">हमेशा</string>
|
||||
<string name="allow_verb">अनुमति देना</string>
|
||||
<string name="appearance_settings">दिखावट</string>
|
||||
<string name="cancel_verb">रद्द करना</string>
|
||||
<string name="icon_descr_cancel_file_preview">फ़ाइल पूर्वावलोकन रद्द करें</string>
|
||||
<string name="icon_descr_cancel_image_preview">छवि पूर्वावलोकन रद्द करें</string>
|
||||
<string name="clear_verb">साफ़</string>
|
||||
<string name="colored">रंगीन</string>
|
||||
<string name="callstate_connected">जुड़े हुए</string>
|
||||
<string name="smp_server_test_connect">जुडिये</string>
|
||||
<string name="connect_via_link_verb">जुडिये</string>
|
||||
<string name="server_connected">जुड़े हुए</string>
|
||||
<string name="group_member_role_owner">स्वामी</string>
|
||||
<string name="group_member_status_connected">जुड़े हुए</string>
|
||||
<string name="notification_contact_connected">जुड़े हुए</string>
|
||||
<string name="you_joined_this_group">आप इस समूह में शामिल हो गए</string>
|
||||
<string name="group_info_member_you">तुम: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="you_are_invited_to_group">आपको समूह में आमंत्रित किया जाता है</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">तुमने पता बदल लिया</string>
|
||||
<string name="snd_group_event_user_left">आप चले गए</string>
|
||||
<string name="unknown_error">अज्ञात त्रुटि</string>
|
||||
<string name="chat_preferences_you_allow">आप आज्ञा दें</string>
|
||||
<string name="welcome">स्वागत!</string>
|
||||
<string name="la_notice_turn_on">चालू करो</string>
|
||||
<string name="section_title_welcome_message">स्वागत संदेश</string>
|
||||
<string name="unknown_message_format">अज्ञात संदेश प्रारूप</string>
|
||||
<string name="personal_welcome">स्वागत <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="callstate_starting">शुरुआत</string>
|
||||
<string name="send_verb">भेजना</string>
|
||||
<string name="save_color">रंग बचाओ</string>
|
||||
<string name="share_verb">साझा करना</string>
|
||||
<string name="reject_contact_button">अस्वीकार</string>
|
||||
<string name="network_use_onion_hosts_required">आवश्यक</string>
|
||||
<string name="reject">अस्वीकार</string>
|
||||
<string name="open_verb">खुला</string>
|
||||
<string name="group_member_status_removed">निकाला गया</string>
|
||||
<string name="rcv_group_event_member_deleted">निकाला गया <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="reply_verb">जवाब दे दो</string>
|
||||
<string name="leave_group_button">छोड़ना</string>
|
||||
<string name="mark_read">पढ़ा हुआ चिह्नित करें</string>
|
||||
<string name="icon_descr_more_button">अधिक</string>
|
||||
<string name="network_use_onion_hosts_no">नहीं</string>
|
||||
<string name="chat_item_ttl_none">कभी नहीं</string>
|
||||
<string name="group_member_status_invited">आमंत्रित</string>
|
||||
<string name="delete_after">बाद मिटा दें</string>
|
||||
<string name="display_name_invited_to_connect">जुड़ने के लिए आमंत्रित किया</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">आपके समूह लिंक के माध्यम से आमंत्रित किया गया</string>
|
||||
<string name="rcv_group_event_member_added">आमंत्रित <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="icon_descr_add_members">सदस्यों को आमंत्रित करो</string>
|
||||
<string name="v4_3_irreversible_message_deletion">अपरिवर्तनीय संदेश विलोपन</string>
|
||||
<string name="button_add_members">सदस्यों को आमंत्रित करो</string>
|
||||
<string name="invite_to_group_button">समूह में आमंत्रित करें</string>
|
||||
<string name="message_deletion_prohibited_in_chat">इस समूह में अपरिवर्तनीय संदेश हटाना प्रतिबंधित है।</string>
|
||||
<string name="italic">तिरछा</string>
|
||||
<string name="join_group_button">प्रवेश लेना</string>
|
||||
<string name="join_group_incognito_button">गुप्त में शामिल हों</string>
|
||||
<string name="joining_group">समूह में शामिल होना</string>
|
||||
<string name="join_group_question">समूह में शामिल हों\?</string>
|
||||
<string name="thousand_abbreviation">क</string>
|
||||
<string name="keychain_error">चाबी का गुच्छा त्रुटि</string>
|
||||
<string name="button_leave_group">समूह छोड़ दें</string>
|
||||
<string name="leave_group_question">समूह छोड़ दें</string>
|
||||
<string name="rcv_group_event_member_left">बाएं</string>
|
||||
<string name="group_member_status_left">बाएं</string>
|
||||
<string name="theme_light">प्रकाश</string>
|
||||
<string name="info_row_local_name">स्थानीय नाम</string>
|
||||
<string name="users_delete_data_only">केवल स्थानीय प्रोफ़ाइल डेटा</string>
|
||||
<string name="auth_log_in_using_credential">अपने क्रेडेंशियल का उपयोग करके लॉग इन करें</string>
|
||||
<string name="make_private_connection">एक निजी संबंध बनाओ</string>
|
||||
<string name="marked_deleted_description">मिटाया हुआ चिह्नित किया गया</string>
|
||||
<string name="mark_unread">अपठित को चिह्नित करें</string>
|
||||
<string name="v4_3_voice_messages_desc">अधिकतम 40 सेकंड, तुरन्त प्राप्त हुआ।</string>
|
||||
<string name="you_sent_group_invitation">आपने समूह आमंत्रण भेजा</string>
|
||||
<string name="message_delivery_error_desc">सबसे अधिक संभावना है कि इस संपर्क ने आपके साथ संबंध हटा दिया है।</string>
|
||||
<string name="mute_chat">मूक</string>
|
||||
<string name="network_status">नेटवर्क की स्थिति</string>
|
||||
<string name="notification_new_contact_request">नया संपर्क अनुरोध</string>
|
||||
<string name="delete_files_and_media_all">सभी फाइलों को मिटा दें</string>
|
||||
<string name="delete_archive">संग्रह हटा देना</string>
|
||||
<string name="new_database_archive">नया डेटाबेस संग्रह</string>
|
||||
<string name="new_member_role">नए सदस्य की भूमिका</string>
|
||||
<string name="settings_notifications_mode_title">अधिसूचना सेवा</string>
|
||||
<string name="notification_preview_new_message">नया सन्देश</string>
|
||||
<string name="no_contacts_to_add">जोड़ने के लिए कोई संपर्क नहीं है</string>
|
||||
<string name="chat_preferences_no">नहीं</string>
|
||||
<string name="no_contacts_selected">कोई संपर्क नहीं चुना गया</string>
|
||||
<string name="no_details">कोई विवरण नहीं</string>
|
||||
<string name="settings_notification_preview_title">अधिसूचना पूर्वावलोकन</string>
|
||||
<string name="notifications">सूचनाएं</string>
|
||||
<string name="full_deletion">सभी के लिए हटाएं</string>
|
||||
<string name="delete_chat_archive_question">लिखचीत संग्रह हटा दे\?</string>
|
||||
<string name="delete_chat_profile_question">चैट प्रोफ़ाइल हटाएं\?</string>
|
||||
<string name="users_delete_question">चैट प्रोफ़ाइल हटाएं\?</string>
|
||||
<string name="users_delete_profile_for">के लिए चैट प्रोफ़ाइल हटाएं</string>
|
||||
<string name="button_delete_contact">संपर्क मिटा दें</string>
|
||||
<string name="deleted_description">हटाए गए</string>
|
||||
<string name="delete_contact_question">संपर्क मिटा दें\?</string>
|
||||
<string name="rcv_group_event_group_deleted">हटाए गए समूह</string>
|
||||
<string name="delete_image">छवि हटाएं</string>
|
||||
<string name="button_delete_group">समूह हटाएं</string>
|
||||
<string name="for_me_only">मेरे लिए हटाएं</string>
|
||||
<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>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></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>
|
||||
@@ -663,7 +663,7 @@
|
||||
<string name="section_title_welcome_message">MESSAGGIO DI BENVENUTO</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Puoi usare il markdown per formattare i messaggi:</string>
|
||||
<string name="you_control_your_chat">Sei tu a controllare la tua chat!</string>
|
||||
<string name="your_current_profile">Il tuo profilo di chat</string>
|
||||
<string name="your_current_profile">Il tuo profilo attuale</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.
|
||||
\n
|
||||
@@ -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>
|
||||
@@ -742,7 +744,6 @@
|
||||
<string name="settings_section_title_support">SUPPORTA SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_themes">TEMI</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</string>
|
||||
<string name="transfer_images_faster">Trasferisci immagini più velocemente</string>
|
||||
<string name="settings_section_title_you">TU</string>
|
||||
<string name="your_chat_database">Il tuo database della chat</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Il tuo attuale database di chat verrà ELIMINATO e SOSTITUITO con quello importato.
|
||||
@@ -927,4 +928,100 @@
|
||||
<string name="app_version_code">Build dell\'app: %s</string>
|
||||
<string name="app_version_name">Versione app: v%s</string>
|
||||
<string name="core_build_timestamp">Core compilato il: %s</string>
|
||||
<string name="smp_servers_per_user">I server per le nuove connessioni del profilo di chat attuale</string>
|
||||
<string name="users_add">Aggiungi profilo</string>
|
||||
<string name="users_delete_question">Eliminare il profilo di chat\?</string>
|
||||
<string name="network_session_mode_user_description">Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni profilo di chat presente nell\'app</b>.</string>
|
||||
<string name="delete_files_and_media_all">Elimina tutti i file</string>
|
||||
<string name="users_delete_data_only">Solo dati del profilo locale</string>
|
||||
<string name="messages_section_title">Messaggi</string>
|
||||
<string name="files_and_media_section">File e multimediali</string>
|
||||
<string name="update_network_session_mode_question">Aggiornare la modalità di isolamento del trasporto\?</string>
|
||||
<string name="users_delete_all_chats_deleted">Tutte le chat e i messaggi verranno eliminati. Non è reversibile!</string>
|
||||
<string name="network_session_mode_user">Profilo di chat</string>
|
||||
<string name="network_session_mode_entity_description">Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni contatto e membro del gruppo </b>.
|
||||
\n<b> Nota: </b>: se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.</string>
|
||||
<string name="network_session_mode_entity">Connessione</string>
|
||||
<string name="messages_section_description">Questa impostazione si applica ai messaggi del profilo di chat attuale</string>
|
||||
<string name="network_session_mode_transport_isolation">Isolamento del trasporto</string>
|
||||
<string name="users_delete_profile_for">Elimina il profilo di chat per</string>
|
||||
<string name="delete_files_and_media_for_all_users">Elimina i file per tutti i profili di chat</string>
|
||||
<string name="failed_to_active_user_title">Errore nel cambio di profilo!</string>
|
||||
<string name="failed_to_create_user_title">Errore nella creazione del profilo!</string>
|
||||
<string name="your_chat_profiles_stored_locally">I tuoi profili di chat sono memorizzati localmente, solo sul tuo dispositivo</string>
|
||||
<string name="error_deleting_user">Errore nell\'eliminazione del profilo utente</string>
|
||||
<string name="users_delete_with_connections">Profilo e connessioni al server</string>
|
||||
<string name="your_chat_profiles">I tuoi profili di chat</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Hai già un profilo chat con lo stesso nome da mostrare. Scegli un altro nome.</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Nome da mostrare doppio!</string>
|
||||
<string name="v4_5_italian_interface_descr">Grazie agli utenti – contribuite via Weblate!</string>
|
||||
<string name="v4_4_french_interface">Interfaccia francese</string>
|
||||
<string name="v4_5_italian_interface">Interfaccia italiana</string>
|
||||
<string name="v4_5_message_draft">Bozza dei messaggi</string>
|
||||
<string name="v4_5_message_draft_descr">Conserva la bozza dell\'ultimo messaggio, con gli allegati.</string>
|
||||
<string name="v4_5_private_filenames">Nomi di file privati</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per profilo di chat (predefinito) o per connessione (BETA).</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Altri miglioramenti sono in arrivo!</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Profili di chat multipli</string>
|
||||
<string name="v4_5_reduced_battery_usage">Consumo di batteria ridotto</string>
|
||||
<string name="v4_4_french_interface_descr">Grazie agli utenti – contribuite via Weblate!</string>
|
||||
<string name="v4_5_transport_isolation">Isolamento del trasporto</string>
|
||||
<string name="v4_5_private_filenames_descr">Per proteggere il fuso orario, i file immagine/vocali usano UTC.</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Nomi e avatar diversi, isolamento del trasporto.</string>
|
||||
<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 la password sopra per mostrare!</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.
|
||||
\nSimpleX Lock deve essere attivato.</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>
|
||||
</resources>
|
||||
@@ -0,0 +1,964 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="chat_item_ttl_day">1日</string>
|
||||
<string name="chat_item_ttl_week">1週間</string>
|
||||
<string name="callstatus_accepted">受けた通話</string>
|
||||
<string name="smp_servers_preset_add">既存サーバを追加</string>
|
||||
<string name="group_member_role_admin">管理者</string>
|
||||
<string name="v4_2_group_links_desc">管理者はグループの参加リンクを発行できます。</string>
|
||||
<string name="network_settings">ネットワーク詳細設定</string>
|
||||
<string name="chat_item_ttl_month">1ヶ月</string>
|
||||
<string name="about_simplex">SimpleXについて</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>について</string>
|
||||
<string name="color_primary">アクセント色</string>
|
||||
<string name="accept_contact_button">承諾</string>
|
||||
<string name="accept_connection_request__question">繋がりを承諾しますか?</string>
|
||||
<string name="accept">承諾</string>
|
||||
<string name="accept_feature">承諾</string>
|
||||
<string name="accept_call_on_lock_screen">承諾</string>
|
||||
<string name="accept_contact_incognito_button">シークレットモードで承諾</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">QRコードでサーバを追加</string>
|
||||
<string name="smp_servers_add_to_another_device">別の端末に追加</string>
|
||||
<string name="users_add">プロフィールを追加</string>
|
||||
<string name="smp_servers_add">サーバを追加…</string>
|
||||
<string name="network_enable_socks_info">SOCKSプロキシ(ポート9050)経由で接続しますか?(※設定する前にプロキシ起動が必要※)</string>
|
||||
<string name="users_delete_all_chats_deleted">全チャットとメッセージが削除されます(※元に戻せません※)!</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">送信相手からの音声メッセージを許可する。</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">あなたと連絡相手が音声メッセージを送信できます。</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>電池省エネに良い</b>:バックグラウンド機能で10分毎に新着メッセージを確認します。通話と緊急メッセージを見逃す可能性があります。</string>
|
||||
<string name="icon_descr_audio_off">音声オフ</string>
|
||||
<string name="attach">添付する</string>
|
||||
<string name="app_version_code">アプリ・ビルド番号: %s</string>
|
||||
<string name="all_your_contacts_will_remain_connected">あなたの連絡先が繋がったまま継続します。</string>
|
||||
<string name="accept_requests">リクエストを承諾</string>
|
||||
<string name="accept_automatically">自動的に</string>
|
||||
<string name="icon_descr_audio_on">音声オン</string>
|
||||
<string name="integrity_msg_bad_hash">メッセージのハッシュ値問題</string>
|
||||
<string name="integrity_msg_bad_id">メッセージIDの問題</string>
|
||||
<string name="allow_verb">許可</string>
|
||||
<string name="allow_voice_messages_question">音声メッセージを許可しますか?</string>
|
||||
<string name="back">戻る</string>
|
||||
<string name="appearance_settings">見た目</string>
|
||||
<string name="app_version_title">アプリのバージョン</string>
|
||||
<string name="app_version_name">アプリのバージョン: v%s</string>
|
||||
<string name="network_session_mode_user_description"><b>アプリ内の各チャットプロフィールに、</b>.連絡先毎にそれぞれのTCP接続(とSOCKS資格情報)が使われます。</string>
|
||||
<string name="network_session_mode_entity_description"><b>各連絡先とグループに、</b>それぞれのTCP接続(とSOCKS資格情報)が使われます。
|
||||
\n<b>※注意※</b> 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。</string>
|
||||
<string name="bold">太文字</string>
|
||||
<string name="icon_descr_audio_call">音声通話</string>
|
||||
<string name="settings_audio_video_calls">音声とビデオ通話</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>※注意※</b>:喪失したら、パスフレーズの回復・変更ができません。</string>
|
||||
<string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。</string>
|
||||
<string name="allow_voice_messages_only_if">送信相手も音声メッセージを許可する時のみに許可する。</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">あなたと連絡相手が消えるメッセージを送信できます。</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">連絡先を自動的に承諾</string>
|
||||
<string name="chat_preferences_always">常に</string>
|
||||
<string name="notifications_mode_service">常にオン</string>
|
||||
<string name="clear_chat_warning">全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ</string>
|
||||
<string name="allow_disappearing_messages_only_if">送信相手も消えるメッセージ機能を許可する時のみに許可する。</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">送信相手も永久メッセージ削除を許可する時のみに許可する。</string>
|
||||
<string name="allow_to_delete_messages">送信済みメッセージの永久削除を許可</string>
|
||||
<string name="allow_to_send_disappearing">消えるメッセージの送信を許可</string>
|
||||
<string name="allow_direct_messages">メンバーへのダイレクトメッセージを許可</string>
|
||||
<string name="allow_to_send_voice">音声メッセージの送信を許可</string>
|
||||
<string name="notifications_mode_off_desc">アクティブの時のみに通知が出ます。バックグラウンド通知サービスは起動されません。</string>
|
||||
<string name="keychain_is_storing_securely">Androidキーストアはパスフレーズの保管に使われます。通知機能に必要です。</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">再起動時とパスフレーズ変更時にAndroidキーストアがパスフレーズの保管に使われます。通知機能に必要です。</string>
|
||||
<string name="answer_call">通話に応答</string>
|
||||
<string name="settings_section_title_icon">アプリのアイコン</string>
|
||||
<string name="full_backup">アプリデータのバックアップ</string>
|
||||
<string name="incognito_random_profile_from_contact_description">このリンクの送信元にランダムなプロフィール(ダミー)が送られます。</string>
|
||||
<string name="incognito_random_profile_description">連絡先にランダムなプロフィール(ダミー)が送られます。</string>
|
||||
<string name="audio_call_no_encryption">音声通話 (エンドツーエンド暗号化なし)</string>
|
||||
<string name="icon_descr_asked_to_receive">画像受信を依頼しました。</string>
|
||||
<string name="auth_unavailable">認証不可能</string>
|
||||
<string name="auto_accept_images">画像を自動的に受信</string>
|
||||
<string name="notifications_mode_service_desc">バックグラウンド機能が常にオンで、メッセージが到着次第に通知が出ます。</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>新しい連絡先を追加</b>:使い捨てのQRコードを発行</string>
|
||||
<string name="turning_off_service_and_periodic">電池省エネをオンに、バックグラウンド機能と定期的な受信依頼をオフにします。設定メニューにて変更できます。</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>電池消費が最少</b>:アプリがアクティブ時のみに通知が出ます。</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>設定メニューにてオフにできます。</b> アプリがアクティブ時に通知が出ます。</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QRコードを読み込み</b>:連絡相手のQRコードをスキャンすると繋がります。</string>
|
||||
<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"><xliff:g id="num_contacts">%1$s</xliff:g>連絡先が選択中</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="your_chat_profiles_stored_locally">チャットプロフィールはローカルであなたの端末だけに保存されます。</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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,438 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="about_simplex_chat">Sobre o <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="about_simplex">Sobre o SimpleX</string>
|
||||
<string name="chat_item_ttl_day">1 dia</string>
|
||||
<string name="chat_item_ttl_week">1 semana</string>
|
||||
<string name="chat_item_ttl_month">1 mês</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="alert_title_cant_invite_contacts">Não é possível convidar contatos!</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">mudando endereço…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">mudando endereço para %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">"mudando endereço…"</string>
|
||||
<string name="change_role">Mudar regra</string>
|
||||
<string name="change_verb">Mudar</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Um perfil aleatório será enviado para o contato do qual você recebeu este link</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Você e seu contato podem excluir mensagens enviadas de forma irreversível.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Pode ser desativado nas configurações</b> – as notificações ainda serão exibidas enquanto o aplicativo estiver em execução.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Você e seu contato podem enviar mensagens de voz.</string>
|
||||
<string name="notifications_mode_off_desc">O aplicativo pode receber notificações apenas quando estiver em execução, nenhum serviço em segundo plano será iniciado</string>
|
||||
<string name="notifications_mode_service">Sempre On</string>
|
||||
<string name="notifications_mode_periodic_desc">Verifica novas mensagens a cada 10 minutos por até 1 minuto</string>
|
||||
<string name="auth_unavailable">Autenticação indisponível</string>
|
||||
<string name="icon_descr_cancel_file_preview">Cancelar visualização do arquivo</string>
|
||||
<string name="icon_descr_asked_to_receive">Pediu para receber a imagem</string>
|
||||
<string name="icon_descr_cancel_live_message">Cancelar mensagem ao vivo</string>
|
||||
<string name="back">Voltar</string>
|
||||
<string name="choose_file">Selecione o arquivo</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Adicionar novo contato</b>: para criar seu QR code único para seu contato.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Escanear \u0020QR code</b>: para se conectar ao seu contato que mostra o código QR para você.</string>
|
||||
<string name="accept_contact_button">Aceitar</string>
|
||||
<string name="clear_chat_question">Limpar bate-papo\?</string>
|
||||
<string name="clear_verb">Limpar</string>
|
||||
<string name="clear_chat_button">Limpar bate-papo</string>
|
||||
<string name="clear_chat_menu_action">Limpar</string>
|
||||
<string name="icon_descr_cancel_link_preview">cancelar pré-visualização do link</string>
|
||||
<string name="feature_cancelled_item">cancelado %s</string>
|
||||
<string name="app_version_name">Versão do App: v%s</string>
|
||||
<string name="callstatus_calling">chamando…</string>
|
||||
<string name="callstatus_in_progress">chamada em andamento</string>
|
||||
<string name="accept">Aceitar</string>
|
||||
<string name="call_already_ended">Chamada já encerrada!</string>
|
||||
<string name="icon_descr_call_progress">Chamada em andamento</string>
|
||||
<string name="icon_descr_call_ended">Chamada finalizada</string>
|
||||
<string name="answer_call">Atender ligação</string>
|
||||
<string name="integrity_msg_bad_hash">hash de mensagem ruim</string>
|
||||
<string name="integrity_msg_bad_id">ID de mensagem incorreta</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Observação</b>: você NÃO poderá recuperar ou alterar a senha se a perder.</string>
|
||||
<string name="cannot_receive_file">Não é possível receber o arquivo</string>
|
||||
<string name="icon_descr_cancel_image_preview">Cancelar visualização da imagem</string>
|
||||
<string name="icon_descr_close_button">Botão Fechar</string>
|
||||
<string name="clear_verification">Limpar verificação</string>
|
||||
<string name="app_version_title">Versão do App</string>
|
||||
<string name="accept_automatically">Automaticamente</string>
|
||||
<string name="bold">negrito</string>
|
||||
<string name="callstatus_error">erro de chamada</string>
|
||||
<string name="settings_audio_video_calls">Chamadas de áudio e vídeo</string>
|
||||
<string name="accept_call_on_lock_screen">Aceitar</string>
|
||||
<string name="call_on_lock_screen">Chamadas na tela de bloqueio:</string>
|
||||
<string name="icon_descr_audio_on">Áudio ligado</string>
|
||||
<string name="chat_database_imported">Banco de dados de bate-papo importado</string>
|
||||
<string name="keychain_is_storing_securely">"Android Keystore é usado para armazenar passphrase com segurança - permite que o serviço de notificação funcione."</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">"O Android Keystore será usado para armazenar passphrase com segurança depois que você reiniciar o aplicativo ou alterar a senha - isso permitirá o recebimento de notificações."</string>
|
||||
<string name="cannot_access_keychain">Não é possível acessar o Keystore para salvar a senha do banco de dados</string>
|
||||
<string name="chat_archive_section">ARQUIVO DE BATE-PAPO</string>
|
||||
<string name="chat_is_stopped_indication">O bate-papo está parado</string>
|
||||
<string name="clear_contacts_selection_button">Limpar</string>
|
||||
<string name="incognito_random_profile_description">Um perfil aleatório será enviado para o seu contato</string>
|
||||
<string name="chat_preferences">Preferências de bate-papo</string>
|
||||
<string name="network_session_mode_user">perfil de bate-papo</string>
|
||||
<string name="accept_requests">Aceitar pedidos</string>
|
||||
<string name="icon_descr_audio_off">Áudio desligado</string>
|
||||
<string name="auto_accept_images">Aceitar imagens automaticamente</string>
|
||||
<string name="chat_database_deleted">Banco de dados de bate-papo excluído</string>
|
||||
<string name="invite_prohibited">Não é possível convidar o contato!</string>
|
||||
<string name="turning_off_service_and_periodic">A otimização da bateria está ativa, desligando o serviço em segundo plano e as solicitações periódicas de novas mensagens. Você pode reativá-los através das configurações.</string>
|
||||
<string name="database_initialization_error_title">Não é possível inicializar o banco de dados</string>
|
||||
<string name="attach">Anexar</string>
|
||||
<string name="cancel_verb">Cancelar</string>
|
||||
<string name="chat_console">Console de bate-papo</string>
|
||||
<string name="smp_servers_check_address">Verifique o endereço do servidor e tente novamente.</string>
|
||||
<string name="network_session_mode_user_description">Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada perfil de bate-papo que você tiver no aplicativo</b>.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Melhor para bateria</b>. Você receberá notificações apenas quando o aplicativo estiver em execução, o serviço em segundo plano NÃO será usado.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Consome mais bateria</b>! O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis.</string>
|
||||
<string name="settings_section_title_chats">CONVERSAS</string>
|
||||
<string name="settings_section_title_icon">ÍCONE DO APP</string>
|
||||
<string name="chat_database_section">BANCO DE DADOS DE BATE-PAPO</string>
|
||||
<string name="chat_is_running">O bate-papo está em execução</string>
|
||||
<string name="chat_is_stopped">O bate-papo está parado</string>
|
||||
<string name="change_database_passphrase_question">Alterar passphrase do banco de dados\?</string>
|
||||
<string name="chat_archive_header">Arquivo de bate-papo</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">endereço alterado para você</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Você e seu contato podem enviar mensagens que desaparecem.</string>
|
||||
<string name="full_backup">Backup de dados do aplicativo</string>
|
||||
<string name="settings_section_title_calls">CHAMADAS</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Aceitar solicitações de contato automaticamente</string>
|
||||
<string name="appearance_settings">Aparência</string>
|
||||
<string name="notifications_mode_service_desc">O serviço em segundo plano está sempre em execução - as notificações serão exibidas assim que as mensagens estiverem disponíveis.</string>
|
||||
<string name="network_session_mode_entity_description">Uma conexão TCP separada (e credencial SOCKS) será usada <b>para cada contato e membro do grupo</b>. <b>Observação</b>: se você tiver muitas conexões, o consumo de bateria e tráfego pode ser substancialmente maior e algumas conexões podem falhar.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Bom para bateria</b>. O serviço em segundo plano verifica novas mensagens a cada 10 minutos. Você pode perder chamadas e mensagens urgentes.</string>
|
||||
<string name="callstatus_ended">chamada finalizada <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="chat_with_developers">Bate-papo com os desenvolvedores</string>
|
||||
<string name="create_group_link">Criar link de grupo</string>
|
||||
<string name="button_create_group_link">Criar link</string>
|
||||
<string name="create_secret_group_title">Criar grupo secreto</string>
|
||||
<string name="theme_dark">Escuro</string>
|
||||
<string name="connect_via_invitation_link">Conectar via link de convite\?</string>
|
||||
<string name="connect_via_contact_link">Conectar via link de contato\?</string>
|
||||
<string name="smp_server_test_create_queue">Criar fila</string>
|
||||
<string name="notification_preview_mode_contact">Nome de contato</string>
|
||||
<string name="notification_preview_somebody">Contato oculto:</string>
|
||||
<string name="copy_verb">Copiar</string>
|
||||
<string name="allow_verb">Permitir</string>
|
||||
<string name="allow_to_send_disappearing">Permitir enviar mensagens que desaparecem.</string>
|
||||
<string name="allow_direct_messages">Permitir o envio de mensagens diretas aos membros.</string>
|
||||
<string name="connect_via_link_or_qr">Conectar via link/ QR Code</string>
|
||||
<string name="clear_chat_warning">"Todas as mensagens serão excluídas - isso não pode ser desfeito! As mensagens serão excluídas APENAS para você."</string>
|
||||
<string name="smp_servers_preset_add">Adicionar servidores predefinidos</string>
|
||||
<string name="smp_servers_add">Adicionar servidor…</string>
|
||||
<string name="create_your_profile">Crie seu perfil</string>
|
||||
<string name="icon_descr_context">Ícone de contexto</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contato e todas as mensagens serão excluídas - isso não pode ser desfeito!</string>
|
||||
<string name="copied">Copiado para a área de transferência</string>
|
||||
<string name="accept_connection_request__question">Aceitar solicitação de conexão\?</string>
|
||||
<string name="network_settings">Configurações de rede avançadas</string>
|
||||
<string name="contact_requests">Solicitações de contato</string>
|
||||
<string name="create_address">Criar endereço</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Todos os seus contatos permanecerão conectados.</string>
|
||||
<string name="callstatus_accepted">chamada aceita</string>
|
||||
<string name="status_contact_has_e2e_encryption">Contato tem criptografia e2e</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">contato não tem criptografia e2e</string>
|
||||
<string name="contact_preferences">Preferências de contato</string>
|
||||
<string name="allow_to_delete_messages">Permite excluir irreversivelmente as mensagens enviadas.</string>
|
||||
<string name="chat_preferences_always">sempre</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Adicione servidores digitalizando QR code.</string>
|
||||
<string name="allow_to_send_voice">Permitir enviar mensagens de voz.</string>
|
||||
<string name="create_group">Criar grupo secreto</string>
|
||||
<string name="always_use_relay">Conectar via relay</string>
|
||||
<string name="users_add">Adicionar perfil</string>
|
||||
<string name="connect_via_link">Conectar via link</string>
|
||||
<string name="create_profile">Criar perfil</string>
|
||||
<string name="database_encrypted">Banco de dados criptografado!</string>
|
||||
<string name="group_member_status_creator">criador</string>
|
||||
<string name="users_delete_all_chats_deleted">Todos os bate-papos e mensagens serão excluídos - isso não pode ser desfeito!</string>
|
||||
<string name="accept_feature">Aceitar</string>
|
||||
<string name="allow_disappearing_messages_only_if">Permitir mensagens que desaparecem apenas se o seu contato permitir.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Permita a exclusão irreversível da mensagem somente se o seu contato permitir.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Permitir que seus contatos enviem mensagens que desaparecem.</string>
|
||||
<string name="allow_voice_messages_only_if">Permitir mensagens de voz somente se o seu contato permitir.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Permitir que seus contatos enviem mensagens de voz.</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
<string name="all_group_members_will_remain_connected">Todos os membros do grupo permanecerão conectados.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">"Contatos podem marcar mensagens para exclusão; você será capaz de visualizá-los."</string>
|
||||
<string name="connect_via_group_link">Conectar via link do grupo\?</string>
|
||||
<string name="contact_already_exists">Contato já existe</string>
|
||||
<string name="icon_descr_contact_checked">Contato verificado</string>
|
||||
<string name="alert_title_contact_connection_pending">Contato ainda não está conectado!</string>
|
||||
<string name="contribute">Contribuir</string>
|
||||
<string name="create_profile_button">Criar</string>
|
||||
<string name="network_enable_socks_info">"Acessar os servidores via proxy SOCKS na porta 9050\? O proxy deve ser iniciado antes de habilitar esta opção."</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Permitir que seus contatos excluam de forma irreversível as mensagens enviadas.</string>
|
||||
<string name="smp_servers_add_to_another_device">Adicionar a outro dispositivo</string>
|
||||
<string name="v4_2_group_links_desc">Os administradores podem criar os links para ingressar em grupos.</string>
|
||||
<string name="allow_voice_messages_question">Permitir mensagens de voz\?</string>
|
||||
<string name="button_delete_group">Excluir grupo</string>
|
||||
<string name="info_row_connection">Conexão</string>
|
||||
<string name="users_delete_question">Excluir perfil de bate-papo\?</string>
|
||||
<string name="full_deletion">Excluir para todos</string>
|
||||
<string name="connect_via_link_verb">Conectar</string>
|
||||
<string name="server_connected">conectado</string>
|
||||
<string name="server_connecting">conectando</string>
|
||||
<string name="deleted_description">excluído</string>
|
||||
<string name="smp_server_test_connect">Conectar</string>
|
||||
<string name="delete_group_menu_action">Excluir</string>
|
||||
<string name="connect_button">Conectar</string>
|
||||
<string name="callstatus_connecting">conectando chamada…</string>
|
||||
<string name="delete_chat_profile_question">Excluir perfil de bate-papo\?</string>
|
||||
<string name="delete_files_and_media_for_all_users">Excluir arquivos de todos os perfis de bate-papo</string>
|
||||
<string name="display_name_connecting">conectando…</string>
|
||||
<string name="connection_error">Erro de conexão</string>
|
||||
<string name="button_delete_contact">Excluir contato</string>
|
||||
<string name="configure_ICE_servers">Configurar servidores ICE</string>
|
||||
<string name="delete_address__question">Excluir endereço\?</string>
|
||||
<string name="decentralized">Descentralizado</string>
|
||||
<string name="delete_database">Excluir banco de dados</string>
|
||||
<string name="set_password_to_export_desc">"O banco de dados é criptografado usando um passphrase aleatório. Por favor, altere-o antes de exportar."</string>
|
||||
<string name="confirm_new_passphrase">Confirmar nova passphrase…</string>
|
||||
<string name="current_passphrase">Passphrase atual…</string>
|
||||
<string name="database_passphrase_is_required">Passphrase do banco de dados é necessária para abrir o chat.</string>
|
||||
<string name="delete_archive">Excluir arquivo</string>
|
||||
<string name="delete_chat_archive_question">Excluir arquivo de bate-papo\?</string>
|
||||
<string name="rcv_group_event_changed_member_role">regra alterada de %s para %s</string>
|
||||
<string name="rcv_group_event_member_connected">conectado</string>
|
||||
<string name="delete_link">Excluir link</string>
|
||||
<string name="delete_link_question">Excluir link\?</string>
|
||||
<string name="chat_preferences_default">padrão (%s)</string>
|
||||
<string name="connection_request_sent">Solicitação de conexão enviada!</string>
|
||||
<string name="group_member_status_connecting">conectando</string>
|
||||
<string name="contact_connection_pending">conectando…</string>
|
||||
<string name="for_me_only">Excluir para mim</string>
|
||||
<string name="group_connection_pending">conectando…</string>
|
||||
<string name="delete_contact_question">Excluir contato\?</string>
|
||||
<string name="confirm_verb">confirmar</string>
|
||||
<string name="database_passphrase_and_export">Passphrase e exportação do banco de dados</string>
|
||||
<string name="icon_descr_call_connecting">Conectando chamada</string>
|
||||
<string name="delete_messages">Excluir mensagens</string>
|
||||
<string name="database_passphrase_will_be_updated">Passphrase de criptografia do banco de dados será atualizada.</string>
|
||||
<string name="database_error">Erro de banco de dados</string>
|
||||
<string name="passphrase_is_different">Passphrase do banco de dados é diferente da salva no Keystore.</string>
|
||||
<string name="group_member_status_complete">completo</string>
|
||||
<string name="info_row_database_id">ID do banco de dados</string>
|
||||
<string name="colored">colorido</string>
|
||||
<string name="callstate_connected">conectado</string>
|
||||
<string name="notification_contact_connected">Conectado</string>
|
||||
<string name="icon_descr_server_status_connected">Conectado</string>
|
||||
<string name="audio_call_no_encryption">chamada de áudio (não criptografada em e2e)</string>
|
||||
<string name="change_member_role_question">Alterar a regra do grupo\?</string>
|
||||
<string name="icon_descr_audio_call">chamada de áudio</string>
|
||||
<string name="rcv_group_event_changed_your_role">mudou sua regra para %s</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Compare os códigos de segurança com seus contatos.</string>
|
||||
<string name="auth_confirm_credential">Confirme sua credencial</string>
|
||||
<string name="callstate_connecting">conectando…</string>
|
||||
<string name="group_member_status_announced">conectando (anunciado)</string>
|
||||
<string name="network_session_mode_entity">Conexão</string>
|
||||
<string name="connection_error_auth">Erro de conexão (AUTH)</string>
|
||||
<string name="display_name_connection_established">conexão estabelecida</string>
|
||||
<string name="connection_local_display_name">conexão <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="archive_created_on_ts">Criado em <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="maximum_supported_file_size">Atualmente, o tamanho máximo de arquivo suportado é <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="delete_verb">Excluir</string>
|
||||
<string name="database_encryption_will_be_updated">Passphase de criptografia do banco de dados será atualizada e armazenada no Keystore.</string>
|
||||
<string name="delete_address">Excluir endereço</string>
|
||||
<string name="encrypted_with_random_passphrase">"O banco de dados é criptografado usando um passphrase aleatório, você pode alterá-la."</string>
|
||||
<string name="database_passphrase">Passphrase do banco de dados</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">O banco de dados será criptografado e o passphrase armazenado no Keystore.</string>
|
||||
<string name="database_will_be_encrypted">O banco de dados será criptografado.</string>
|
||||
<string name="ttl_day">%d dia</string>
|
||||
<string name="image_decoding_exception_title">Erro de decodificação</string>
|
||||
<string name="delete_contact_menu_action">Excluir</string>
|
||||
<string name="ttl_days">%d dias</string>
|
||||
<string name="delete_files_and_media_all">Excluir todos os arquivos</string>
|
||||
<string name="delete_message__question">Excluir mensagem\?</string>
|
||||
<string name="delete_after">Excluir depois</string>
|
||||
<string name="users_delete_profile_for">Excluir perfil de bate-papo para</string>
|
||||
<string name="rcv_group_event_group_deleted">grupo excluído</string>
|
||||
<string name="delete_group_question">Excluir grupo\?</string>
|
||||
<string name="delete_image">Excluir imagem</string>
|
||||
<string name="delete_files_and_media_question">Excluir arquivos e mídia\?</string>
|
||||
<string name="group_member_status_connected">conectado</string>
|
||||
<string name="group_member_status_accepted">conectando (aceito)</string>
|
||||
<string name="ttl_d">%dd</string>
|
||||
<string name="v4_5_transport_isolation_descr">Por perfil de bate-papo (padrão) ou por conexão (BETA).</string>
|
||||
<string name="accept_contact_incognito_button">Aceitar anônimo</string>
|
||||
<string name="delete_messages_after">Excluir mensagens após</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: Scaneie o QR code exibido no aplicativo, via <b>Scan QR code</b></string>
|
||||
<string name="delete_pending_connection__question">Excluir conexão pendente\?</string>
|
||||
<string name="simplex_link_mode_description">Descrição</string>
|
||||
<string name="smp_servers_delete_server">Excluir servidor</string>
|
||||
<string name="group_member_status_intro_invitation">conectando (introduction invitation)</string>
|
||||
<string name="connection_timeout">Tempo de conexão esgotado</string>
|
||||
<string name="delete_member_message__question">Excluir mensagem do membro\?</string>
|
||||
<string name="smp_server_test_delete_queue">Excluir fila</string>
|
||||
<string name="settings_section_title_device">DISPOSITIVO</string>
|
||||
<string name="settings_developer_tools">Ferramentas de desenvolvimento</string>
|
||||
<string name="group_member_status_introduced">conectando (introduced)</string>
|
||||
<string name="color_primary">Realçar</string>
|
||||
<string name="error_removing_member">Erro ao remover membro</string>
|
||||
<string name="error_changing_role">Erro ao alterar regra</string>
|
||||
<string name="conn_level_desc_direct">direto</string>
|
||||
<string name="server_error">erro</string>
|
||||
<string name="failed_to_parse_chat_title">Falha ao carregar o bate-papo</string>
|
||||
<string name="error_setting_network_config">Erro ao atualizar a configuração de rede</string>
|
||||
<string name="error_sending_message">Erro ao enviar mensagem</string>
|
||||
<string name="error_adding_members">Erro ao adicionar membro(s)</string>
|
||||
<string name="smp_server_test_disconnect">Desconectar</string>
|
||||
<string name="error_deleting_user">Erro ao excluir perfil do usuário</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_months">%d meses</string>
|
||||
<string name="ttl_weeks">%d semanas</string>
|
||||
<string name="encrypt_database_question">Criptografar banco de dados\?</string>
|
||||
<string name="error_receiving_file">Erro ao receber arquivo</string>
|
||||
<string name="error_creating_address">Erro ao criar endereço</string>
|
||||
<string name="display_name__field">Nome de exibição:</string>
|
||||
<string name="error_starting_chat">Erro ao iniciar o bate-papo</string>
|
||||
<string name="error_deleting_database">Erro ao excluir banco de dados de bate-papo</string>
|
||||
<string name="encrypt_database">Criptografar</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Ativar TCP keep-alive</string>
|
||||
<string name="failed_to_create_user_title">Erro ao criar perfil!</string>
|
||||
<string name="error_joining_group">Erro ao ingressar no grupo</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Nome de exibição duplicado!</string>
|
||||
<string name="error_deleting_contact">Erro ao excluir contato</string>
|
||||
<string name="error_changing_address">Erro ao alterar endereço</string>
|
||||
<string name="error_deleting_pending_contact_connection">Erro ao excluir conexão de contato pendente</string>
|
||||
<string name="edit_verb">Editar</string>
|
||||
<string name="enable_automatic_deletion_question">Ativar exclusão automática de mensagens\?</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="error_saving_smp_servers">Erro ao salvar servidores SMP</string>
|
||||
<string name="error_accepting_contact_request">Erro ao aceitar solicitação de contato</string>
|
||||
<string name="error_deleting_contact_request">Erro ao excluir solicitação de contato</string>
|
||||
<string name="failed_to_active_user_title">Erro ao trocar de perfil!</string>
|
||||
<string name="auth_disable_simplex_lock">Desativar Bloqueio SimpleX</string>
|
||||
<string name="auth_enable_simplex_lock">Ativar Bloqueio SimpleX</string>
|
||||
<string name="icon_descr_edited">editado</string>
|
||||
<string name="icon_descr_server_status_error">Erro</string>
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="error_saving_ICE_servers">Erro ao salvar servidores ICE</string>
|
||||
<string name="exit_without_saving">Sair sem salvar</string>
|
||||
<string name="display_name">Nome de exibição</string>
|
||||
<string name="encrypted_video_call">chamada de vídeo criptografada e2e</string>
|
||||
<string name="integrity_msg_duplicate">mensagem duplicada</string>
|
||||
<string name="status_e2e_encrypted">criptografado e2e</string>
|
||||
<string name="export_database">Exportar banco de dados</string>
|
||||
<string name="total_files_count_and_size">%d arquivo(s) com tamanho total de %s</string>
|
||||
<string name="error_exporting_chat_database">Erro ao exportar banco de dados de bate-papo</string>
|
||||
<string name="error_importing_database">Erro ao importar banco de dados de bate-papo</string>
|
||||
<string name="error_stopping_chat">Erro ao interromper o bate-papo</string>
|
||||
<string name="error_changing_message_deletion">Erro ao alterar configuração</string>
|
||||
<string name="error_encrypting_database">Erro ao criptografar o banco de dados</string>
|
||||
<string name="encrypted_database">Banco de dados criptografado</string>
|
||||
<string name="enter_correct_passphrase">Digite o passphrase correto.</string>
|
||||
<string name="enter_passphrase">Digite o passphrase…</string>
|
||||
<string name="error_with_info">Erro: %s</string>
|
||||
<string name="button_edit_group_profile">Editar perfil do grupo</string>
|
||||
<string name="icon_descr_expand_role">Expandir seleção de regra</string>
|
||||
<string name="error_saving_group_profile">Erro ao salvar o perfil do grupo</string>
|
||||
<string name="direct_messages">Mensagens diretas</string>
|
||||
<string name="feature_enabled">habilitado</string>
|
||||
<string name="feature_enabled_for_contact">habilitado para contato</string>
|
||||
<string name="feature_enabled_for_you">ativado para você</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_month">%d mês</string>
|
||||
<string name="ttl_week">%d semana</string>
|
||||
<string name="ttl_hour">%d hora</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">A autenticação do dispositivo não está habilitada. Você pode ativar o Bloqueio SimpleX em Configurações, depois de ativar a autenticação do dispositivo.</string>
|
||||
<string name="no_call_on_lock_screen">Desativar</string>
|
||||
<string name="icon_descr_server_status_disconnected">Desconectado</string>
|
||||
<string name="disappearing_messages_are_prohibited">Mensagens que desaparecem(temporárias) são proibidas neste grupo.</string>
|
||||
<string name="error_saving_file">Erro ao salvar arquivo</string>
|
||||
<string name="display_name_cannot_contain_whitespace">O nome de exibição não pode conter espaços em branco.</string>
|
||||
<string name="encrypted_audio_call">chamada de áudio criptografada e2e</string>
|
||||
<string name="edit_image">Editar imagem</string>
|
||||
<string name="smp_servers_enter_manually">Insira o servidor manualmente</string>
|
||||
<string name="error_deleting_group">Erro ao excluir grupo</string>
|
||||
<string name="settings_experimental_features">Funcionalidades experimentais</string>
|
||||
<string name="error_creating_link_for_group">Erro ao criar o link de grupo</string>
|
||||
<string name="error_deleting_link_for_group">Erro ao excluir o link de grupo</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Mensagens diretas entre membros são proibidas neste grupo.</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="ttl_hours">%d horas</string>
|
||||
<string name="description_via_contact_address_link_incognito">anônimo via link de endereço de contato</string>
|
||||
<string name="description_via_one_time_link_incognito">anônimo via link único</string>
|
||||
<string name="hide_verb">Esconder</string>
|
||||
<string name="from_gallery_button">Da Galeria</string>
|
||||
<string name="group_members_can_send_disappearing">Os membros do grupo podem enviar mensagens que desaparecem.</string>
|
||||
<string name="icon_descr_file">Arquivo</string>
|
||||
<string name="full_name__field">Nome completo:</string>
|
||||
<string name="incoming_audio_call">Chamada de áudio recebida</string>
|
||||
<string name="v4_4_disappearing_messages">Mensagens que desaparecem</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">"Ocultar aplicativo nos aplicativos recentes."</string>
|
||||
<string name="icon_descr_image_snd_complete">Imagem enviada</string>
|
||||
<string name="group_link">Link do grupo</string>
|
||||
<string name="import_database">Importar banco de dados</string>
|
||||
<string name="alert_message_group_invitation_expired">O convite de grupo não é mais válido, foi removido pelo remetente.</string>
|
||||
<string name="icon_descr_group_inactive">Grupo inativo</string>
|
||||
<string name="alert_title_no_group">Grupo não encontrado!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">O grupo será excluído para você - isso não pode ser desfeito!</string>
|
||||
<string name="info_row_group">Grupo</string>
|
||||
<string name="conn_level_desc_indirect">indireto (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="incognito">Anônimo</string>
|
||||
<string name="timed_messages">Mensagens que desaparecem</string>
|
||||
<string name="group_preferences">Preferências de grupo</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Mensagens que desaparecem são proibidas nesse bate-papo.</string>
|
||||
<string name="group_members_can_send_dms">Os membros do grupo podem enviar mensagens diretas.</string>
|
||||
<string name="ttl_mth">%dmês</string>
|
||||
<string name="simplex_link_mode_full">Link completo</string>
|
||||
<string name="hide_notification">Esconder</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">A autenticação do dispositivo está desativada. Desativando o SimpleX Lock.</string>
|
||||
<string name="for_everybody">Para todos</string>
|
||||
<string name="notification_preview_mode_hidden">Escondido</string>
|
||||
<string name="create_one_time_link">Gerar um link de convite único.</string>
|
||||
<string name="how_to_use_your_servers">Como usar seus servidores</string>
|
||||
<string name="import_database_confirmation">Importar</string>
|
||||
<string name="import_database_question">Importar banco de dados de bate-papo\?</string>
|
||||
<string name="group_display_name_field">Nome de exibição do grupo:</string>
|
||||
<string name="group_full_name_field">Nome completo do grupo:</string>
|
||||
<string name="v4_2_group_links">Links de grupo</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Privacidade e segurança aprimoradas</string>
|
||||
<string name="failed_to_parse_chats_title">Falha ao carregar bate-papos</string>
|
||||
<string name="file_with_path">Arquivo: %s</string>
|
||||
<string name="file_saved">Arquivo salvo</string>
|
||||
<string name="group_members_can_send_voice">Os membros do grupo podem enviar mensagens de voz.</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">O grupo será excluído para todos os membros - isso não pode ser desfeito!</string>
|
||||
<string name="settings_section_title_help">AJUDA</string>
|
||||
<string name="notification_display_mode_hidden_desc">Ocultar contato e mensagem</string>
|
||||
<string name="how_to_use_simplex_chat">Como usar</string>
|
||||
<string name="how_to_use_markdown">Como usar markdown</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Se você não puder se encontrar pessoalmente, <b>mostre o QR code na videochamada</b> ou compartilhe o link.</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Se você não puder encontrar pessoalmente, você pode <b>escanear o QR code na videochamada</b> ou seu contato pode compartilhar um link de convite.</string>
|
||||
<string name="network_disable_socks_info">Se você confirmar, os servidores de mensagens poderão ver seu endereço IP e seu provedor - e quais servidores você está se conectando.</string>
|
||||
<string name="image_descr">Imagem</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Se você recebeu o link de convite <xliff:g id="appName">SimpleX Chat</xliff:g>, você pode abri-lo em seu navegador:</string>
|
||||
<string name="image_saved">Imagem salva na galeria</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">O modo de navegação anônima não é suportado aqui - seu perfil principal será enviado aos membros do grupo</string>
|
||||
<string name="description_via_group_link_incognito">anônimo via link do grupo</string>
|
||||
<string name="incoming_video_call">Chamada de vídeo recebida</string>
|
||||
<string name="turn_off_battery_optimization">Para usá-lo, por favor <b>desative a otimização da bateria</b> para <xliff:g id="appName">SimpleX</xliff:g> na próxima caixa de diálogo. Caso contrário, as notificações serão desativadas.</string>
|
||||
<string name="share_one_time_link">Gerar um link de convite único</string>
|
||||
<string name="file_not_found">Arquivo não encontrado</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Se você optar por rejeitar o remetente NÃO será notificado.</string>
|
||||
<string name="incorrect_code">Código de segurança incorreto!</string>
|
||||
<string name="install_simplex_chat_for_terminal">Instale o <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para o terminal</string>
|
||||
<string name="full_name_optional__prompt">Nome Completo (opcional)</string>
|
||||
<string name="how_it_works">Como funciona</string>
|
||||
<string name="immune_to_spam_and_abuse">Imune a spam e abuso</string>
|
||||
<string name="icon_descr_flip_camera">Vire a câmera</string>
|
||||
<string name="icon_descr_hang_up">Desligar</string>
|
||||
<string name="settings_section_title_incognito">Modo anônimo</string>
|
||||
<string name="initial_member_role">Regra inicial</string>
|
||||
<string name="snd_group_event_group_profile_updated">perfil do grupo atualizado</string>
|
||||
<string name="group_member_status_group_deleted">Grupo excluído</string>
|
||||
<string name="incognito_info_protects">O modo de navegação anônima protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato, um novo perfil aleatório é criado.</string>
|
||||
<string name="group_members_can_delete">Os membros do grupo podem excluir mensagens enviadas de forma irreversível.</string>
|
||||
<string name="ttl_w">%dsemana</string>
|
||||
<string name="v4_3_improved_server_configuration">Configuração de servidor aprimorada</string>
|
||||
<string name="v4_4_french_interface">Interface francesa</string>
|
||||
<string name="callstate_ended">terminou</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Ative as chamadas pela tela de bloqueio nas Configurações.</string>
|
||||
<string name="files_and_media_section">Arquivos & mídia</string>
|
||||
<string name="error_updating_link_for_group">Erro ao atualizar o link do grupo</string>
|
||||
<string name="group_invitation_expired">O convite do grupo expirou</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">O arquivo será recebido quando seu contato estiver online, aguarde ou verifique mais tarde!</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">O perfil do grupo é armazenado nos dispositivos dos membros, não nos servidores.</string>
|
||||
<string name="icon_descr_help">ajuda</string>
|
||||
<string name="how_simplex_works">Como <xliff:g id="appName">SimpleX</xliff:g> funciona</string>
|
||||
<string name="enter_one_ICE_server_per_line">Servidores ICE (um por linha)</string>
|
||||
<string name="ignore">Ignorar</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">A imagem será recebida quando seu contato estiver online, aguarde ou verifique mais tarde!</string>
|
||||
</resources>
|
||||
@@ -423,7 +423,7 @@
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Имя профиля:</string>
|
||||
<string name="full_name__field">"Полное имя:</string>
|
||||
<string name="your_current_profile">Ваш профиль</string>
|
||||
<string name="your_current_profile">Ваш активный профиль</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профиль хранится на вашем устройстве и отправляется только вашим контактам.\n\n<xliff:g id="appName">SimpleX</xliff:g> серверы не могут получить доступ к вашему профилю.</string>
|
||||
<string name="edit_image">Поменять аватар</string>
|
||||
<string name="delete_image">Удалить аватар</string>
|
||||
@@ -514,13 +514,15 @@
|
||||
<!-- Call settings -->
|
||||
<string name="settings_audio_video_calls">Аудио- и видеозвонки</string>
|
||||
<string name="your_calls">Ваши звонки</string>
|
||||
<string name="connect_calls_via_relay">Соединяться через сервер (relay)</string>
|
||||
<string name="always_use_relay">Всегда соединяться через relay</string>
|
||||
<string name="call_on_lock_screen">Звонки на экране блокировки:</string>
|
||||
<string name="accept_call_on_lock_screen">Принимать</string>
|
||||
<string name="show_call_on_lock_screen">Показывать</string>
|
||||
<string name="no_call_on_lock_screen">Выключить</string>
|
||||
<string name="your_ice_servers">Ваши ICE серверы</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE серверы</string>
|
||||
<string name="relay_server_protects_ip">Relay сервер защищает ваш IP адрес, но может отслеживать продолжительность звонка.</string>
|
||||
<string name="relay_server_if_necessary">Relay сервер используется только при необходимости. Другая сторона может видеть ваш IP адрес.</string>
|
||||
<!-- Call Lock Screen -->
|
||||
<string name="open_simplex_chat_to_accept_call">Откройте <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\nчтобы принять звонок</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Вы можете разрешить принимать звонки на экране блокировки через Настройки.</string>
|
||||
@@ -560,7 +562,6 @@
|
||||
<string name="your_privacy">Конфиденциальность</string>
|
||||
<string name="protect_app_screen">Защитить экран приложения</string>
|
||||
<string name="auto_accept_images">Автоприем изображений</string>
|
||||
<string name="transfer_images_faster">Передавать изображения быстрее</string>
|
||||
<string name="send_link_previews">Отправлять картинки ссылок</string>
|
||||
<string name="full_backup">Резервная копия данных</string>
|
||||
<!-- Settings sections -->
|
||||
@@ -993,4 +994,105 @@
|
||||
<string name="feature_offered_item">предложил(a) %s</string>
|
||||
<string name="feature_offered_item_with_param">предложил(a) %s: %2s</string>
|
||||
<string name="feature_cancelled_item">отменил(a) %s</string>
|
||||
<string name="icon_descr_cancel_live_message">Отменить живое сообщение</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="network_option_ping_count">Количество PING</string>
|
||||
<string name="users_delete_with_connections">Профиль и соединения на сервере</string>
|
||||
<string name="app_version_title">Версия приложения</string>
|
||||
<string name="network_session_mode_user">Профиль чата</string>
|
||||
<string name="network_session_mode_entity">Соединение</string>
|
||||
<string name="users_add">Добавить профиль</string>
|
||||
<string name="error_deleting_user">Ошибка удаления профиля пользователя</string>
|
||||
<string name="files_and_media_section">Файлы и медиа</string>
|
||||
<string name="users_delete_data_only">Только локальные данные профиля</string>
|
||||
<string name="messages_section_title">Сообщения</string>
|
||||
<string name="smp_servers_per_user">Серверы для новых соединений вашего текущего профиля чата</string>
|
||||
<string name="your_chat_profiles_stored_locally">Ваши профили чата хранятся локально, только на вашем устройстве</string>
|
||||
<string name="your_chat_profiles">Ваши профили чата</string>
|
||||
<string name="users_delete_all_chats_deleted">Все чаты и сообщения будут удалены - это нельзя отменить!</string>
|
||||
<string name="app_version_code">Сборка приложения: %s</string>
|
||||
<string name="app_version_name">Версия приложения: v%s</string>
|
||||
<string name="network_session_mode_entity_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого контакта и члена группы</b>.
|
||||
\n<b>Обратите внимание</b>: если у вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.</string>
|
||||
<string name="network_session_mode_user_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого профиля чата, который вы имеете в приложении</b>.</string>
|
||||
<string name="core_build_timestamp">Ядро скомпилировано: %s</string>
|
||||
<string name="core_version">Версия ядра: v%s</string>
|
||||
<string name="users_delete_question">Удалить профиль чата\?</string>
|
||||
<string name="users_delete_profile_for">Удалить профиль чата для</string>
|
||||
<string name="messages_section_description">Эта настройка применяется к сообщениям в вашем текущем профиле чата</string>
|
||||
<string name="network_session_mode_transport_isolation">Отдельные сессии для</string>
|
||||
<string name="update_network_session_mode_question">Обновить режим отдельных сессий\?</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Имя профиля уже используется</string>
|
||||
<string name="failed_to_create_user_title">Ошибка создания профиля!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">У вас уже есть профиль с таким именем. Пожалуйста, выберите другое имя.</string>
|
||||
<string name="failed_to_active_user_title">Ошибка выбора профиля!</string>
|
||||
<string name="v4_5_transport_isolation_descr">По профилю чата или по соединению (БЕТА)</string>
|
||||
<string name="v4_4_french_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Разные имена, аватары и транспортные сессии.</string>
|
||||
<string name="v4_5_italian_interface">Итальянский интерфейс</string>
|
||||
<string name="v4_5_message_draft">Черновик сообщения</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Много профилей чата</string>
|
||||
<string name="v4_5_message_draft_descr">Сохранить последний черновик, вместе с вложениями.</string>
|
||||
<string name="v4_5_private_filenames">Защищенные имена файлов</string>
|
||||
<string name="v4_5_italian_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
|
||||
<string name="v4_5_private_filenames_descr">Чтобы защитить ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC.</string>
|
||||
<string name="v4_4_french_interface">Французский интерфейс</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string>
|
||||
<string name="v4_5_reduced_battery_usage">Уменьшенное потребление батареи</string>
|
||||
<string name="v4_5_transport_isolation">Отдельные транспортные сессии</string>
|
||||
<string name="moderated_description">удалено</string>
|
||||
<string name="moderated_item_description">удалено %s</string>
|
||||
<string name="delete_member_message__question">Удалить сообщение участника\?</string>
|
||||
<string name="moderate_verb">Модерировать</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">Сообщение будет удалено для всех членов группы.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Сообщение будет помечено как удаленное для всех членов группы.</string>
|
||||
<string name="observer_cant_send_message_desc">Пожалуйста, свяжитесь с админом группы.</string>
|
||||
<string name="observer_cant_send_message_title">Вы не можете отправлять сообщения!</string>
|
||||
<string name="you_are_observer">только чтение сообщений</string>
|
||||
<string name="group_member_role_observer">читатель</string>
|
||||
<string name="initial_member_role">Роль при вступлении</string>
|
||||
<string name="error_updating_link_for_group">Ошибка обновления ссылки группы</string>
|
||||
<string name="language_system">Системный</string>
|
||||
<string name="v4_6_audio_video_calls">Аудио и видео звонки</string>
|
||||
<string name="error_saving_user_password">Ошибка при сохранении пароля пользователя</string>
|
||||
<string name="smp_save_servers_question">Сохранить серверы\?</string>
|
||||
<string name="should_be_at_least_one_profile">Должен быть хотя бы один профиль пользователя.</string>
|
||||
<string name="should_be_at_least_one_visible_profile">Должен быть хотя бы один открытый профиль пользователя.</string>
|
||||
<string name="to_reveal_profile_enter_password">Чтобы показать Ваш скрытый профиль, введите пароль в поле поиска на странице Ваши профили чата.</string>
|
||||
<string name="user_unmute">Уведомлять</string>
|
||||
<string name="group_welcome_title">Приветственное сообщение</string>
|
||||
<string name="confirm_password">Подтвердить пароль</string>
|
||||
<string name="button_add_welcome_message">Добавить приветственное сообщение</string>
|
||||
<string name="button_welcome_message">Приветственное сообщение</string>
|
||||
<string name="save_and_update_group_profile">Сохранить сообщение и обновить группу</string>
|
||||
<string name="muted_when_inactive">Без звука, когда не активный!</string>
|
||||
<string name="cant_delete_user_profile">Нельзя удалить профиль пользователя!</string>
|
||||
<string name="enter_password_to_show">Введите пароль в поиске!</string>
|
||||
<string name="save_profile_password">Сохранить пароль профиля</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Китайский и Испанский интерфейс</string>
|
||||
<string name="dont_show_again">Не показывать</string>
|
||||
<string name="user_hide">Скрыть</string>
|
||||
<string name="v4_6_reduced_battery_usage">Уменьшенное потребление батареи</string>
|
||||
<string name="v4_6_group_moderation">Модерация группы</string>
|
||||
<string name="hidden_profile_password">Пароль скрытого профиля</string>
|
||||
<string name="password_to_show">Пароль чтобы раскрыть</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Скрытые профили чата</string>
|
||||
<string name="error_updating_user_privacy">Ошибка при обновлении конфиденциальности</string>
|
||||
<string name="v4_6_group_welcome_message">Приветственное сообщение группы</string>
|
||||
<string name="hide_profile">Скрыть профиль</string>
|
||||
<string name="user_mute">Без звука</string>
|
||||
<string name="make_profile_private">Сделайте профиль конфиденциальным!</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string>
|
||||
<string name="v4_6_group_moderation_descr">Теперь админы могут:
|
||||
\n- удалять сообщения членов.
|
||||
\n- приостанавливать членов (роль \"наблюдатель\")</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Защитите ваши профили чата паролем!</string>
|
||||
<string name="user_unhide">Раскрыть</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Поддержка bluetooth и другие улучшения.</string>
|
||||
<string name="save_welcome_message_question">Сохранить приветственное сообщение\?</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Установить сообщение для новых членов группы!</string>
|
||||
<string name="tap_to_activate_profile">Нажмите, чтобы сделать профиль активным.</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Вы все равно получите звонки и уведомления в профилях без звука, когда они активные.</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Вы можете скрыть профиль или выключить уведомления - подержите, чтобы увидеть меню.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="window_background_dark">#121212</color>
|
||||
</resources>
|
||||
@@ -22,12 +22,14 @@
|
||||
<!-- Item Content - ChatModel.kt -->
|
||||
<string name="deleted_description">deleted</string>
|
||||
<string name="marked_deleted_description">marked deleted</string>
|
||||
<string name="moderated_item_description">moderated by %s</string>
|
||||
<string name="sending_files_not_yet_supported">sending files is not supported yet</string>
|
||||
<string name="receiving_files_not_yet_supported">receiving files is not supported yet</string>
|
||||
<string name="sender_you_pronoun">you</string>
|
||||
<string name="unknown_message_format">unknown message format</string>
|
||||
<string name="invalid_message_format">invalid message format</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="moderated_description">moderated</string>
|
||||
<string name="invalid_chat">invalid chat</string>
|
||||
<string name="invalid_data">invalid data</string>
|
||||
|
||||
@@ -63,6 +65,10 @@
|
||||
<string name="failed_to_parse_chat_title">Failed to load chat</string>
|
||||
<string name="failed_to_parse_chats_title">Failed to load chats</string>
|
||||
<string name="contact_developers">Please update the app and contact developers.</string>
|
||||
<string name="failed_to_create_user_title">Error creating profile!</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
|
||||
<string name="failed_to_active_user_title">Error switching profile!</string>
|
||||
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Connection timeout</string>
|
||||
@@ -97,6 +103,7 @@
|
||||
<string name="smp_server_test_delete_queue">Delete queue</string>
|
||||
<string name="smp_server_test_disconnect">Disconnect</string>
|
||||
<string name="error_deleting_user">Error deleting user profile</string>
|
||||
<string name="error_updating_user_privacy">Error updating user privacy</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||
@@ -177,9 +184,13 @@
|
||||
<string name="reveal_verb">Reveal</string>
|
||||
<string name="hide_verb">Hide</string>
|
||||
<string name="allow_verb">Allow</string>
|
||||
<string name="moderate_verb">Moderate</string>
|
||||
<string name="delete_message__question">Delete message?</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_message_mark_deleted_warning">Message will be marked for deletion. The recipient(s) will be able to reveal this message.</string>
|
||||
<string name="delete_member_message__question">Delete member message?</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">The message will be deleted for all members.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">The message will be marked as moderated for all members.</string>
|
||||
<string name="for_me_only">Delete for me</string>
|
||||
<string name="for_everybody">For everyone</string>
|
||||
|
||||
@@ -217,6 +228,9 @@
|
||||
<string name="images_limit_desc">Only 10 images can be sent at the same time</string>
|
||||
<string name="image_decoding_exception_title">Decoding error</string>
|
||||
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
|
||||
<string name="you_are_observer">you are observer</string>
|
||||
<string name="observer_cant_send_message_title">You can\'t send messages!</string>
|
||||
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
|
||||
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Image</string>
|
||||
@@ -446,6 +460,7 @@
|
||||
<string name="smp_servers_check_address">Check server address and try again.</string>
|
||||
<string name="smp_servers_delete_server">Delete server</string>
|
||||
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
|
||||
<string name="smp_save_servers_question">Save servers?</string>
|
||||
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
|
||||
<string name="star_on_github">Star on GitHub</string>
|
||||
<string name="contribute">Contribute</string>
|
||||
@@ -522,6 +537,16 @@
|
||||
<string name="save_and_notify_group_members">Save and notify group members</string>
|
||||
<string name="exit_without_saving">Exit without saving</string>
|
||||
|
||||
|
||||
<!-- HiddenProfileView.kt -->
|
||||
<string name="hide_profile">Hide profile</string>
|
||||
<string name="password_to_show">Password to show</string>
|
||||
<string name="save_profile_password">Save profile password</string>
|
||||
<string name="hidden_profile_password">Hidden profile password</string>
|
||||
<string name="confirm_password">Confirm password</string>
|
||||
<string name="to_reveal_profile_enter_password">To reveal your hidden profile, enter a full password into a search field in "Your chat profiles" page.</string>
|
||||
<string name="error_saving_user_password">Error saving user password</string>
|
||||
|
||||
<!-- Welcome Prompts - WelcomeView.kt -->
|
||||
<string name="you_control_your_chat">You control your chat!</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">The messaging and application platform protecting your privacy and security.</string>
|
||||
@@ -618,13 +643,15 @@
|
||||
<!-- Call settings -->
|
||||
<string name="settings_audio_video_calls">Audio & video calls</string>
|
||||
<string name="your_calls">Your calls</string>
|
||||
<string name="connect_calls_via_relay">Connect via relay</string>
|
||||
<string name="always_use_relay">Always use relay</string>
|
||||
<string name="call_on_lock_screen">Calls on lock screen:</string>
|
||||
<string name="accept_call_on_lock_screen">Accept</string>
|
||||
<string name="show_call_on_lock_screen">Show</string>
|
||||
<string name="no_call_on_lock_screen">Disable</string>
|
||||
<string name="your_ice_servers">Your ICE servers</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE servers</string>
|
||||
<string name="relay_server_protects_ip">Relay server protects your IP address, but it can observe the duration of the call.</string>
|
||||
<string name="relay_server_if_necessary">Relay server is only used if necessary. Another party can observe your IP address.</string>
|
||||
|
||||
<!-- Call Lock Screen -->
|
||||
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> to accept call</string>
|
||||
@@ -669,7 +696,6 @@
|
||||
<string name="your_privacy">Your privacy</string>
|
||||
<string name="protect_app_screen">Protect app screen</string>
|
||||
<string name="auto_accept_images">Auto-accept images</string>
|
||||
<string name="transfer_images_faster">Transfer images faster</string>
|
||||
<string name="send_link_previews">Send link previews</string>
|
||||
<string name="full_backup">App data backup</string>
|
||||
|
||||
@@ -684,6 +710,7 @@
|
||||
<string name="settings_developer_tools">Developer tools</string>
|
||||
<string name="settings_experimental_features">Experimental features</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_language" translatable="false">LANGUAGE</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_messages">MESSAGES</string>
|
||||
@@ -864,6 +891,7 @@
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_observer">observer</string>
|
||||
<string name="group_member_role_member">member</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
<string name="group_member_role_owner">owner</string>
|
||||
@@ -886,6 +914,7 @@
|
||||
<!-- AddGroupMembersView.kt -->
|
||||
<string name="no_contacts_to_add">No contacts to add</string>
|
||||
<string name="new_member_role">New member role</string>
|
||||
<string name="initial_member_role">Initial role</string>
|
||||
<string name="icon_descr_expand_role">Expand role selection</string>
|
||||
<string name="invite_to_group_button">Invite to group</string>
|
||||
<string name="skip_inviting_button">Skip inviting members</string>
|
||||
@@ -907,6 +936,8 @@
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Group will be deleted for you - this cannot be undone!</string>
|
||||
<string name="button_leave_group">Leave group</string>
|
||||
<string name="button_edit_group_profile">Edit group profile</string>
|
||||
<string name="button_add_welcome_message">Add welcome message</string>
|
||||
<string name="button_welcome_message">Welcome message</string>
|
||||
<string name="group_link">Group link</string>
|
||||
<string name="create_group_link">Create group link</string>
|
||||
<string name="button_create_group_link">Create link</string>
|
||||
@@ -915,6 +946,7 @@
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">You can share a link or a QR code - anybody will be able to join the group. You won\'t lose members of the group if you later delete it.</string>
|
||||
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
|
||||
<string name="error_creating_link_for_group">Error creating group link</string>
|
||||
<string name="error_updating_link_for_group">Error updating group link</string>
|
||||
<string name="error_deleting_link_for_group">Error deleting group link</string>
|
||||
<string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string>
|
||||
|
||||
@@ -943,6 +975,11 @@
|
||||
<string name="conn_level_desc_direct">direct</string>
|
||||
<string name="conn_level_desc_indirect">indirect (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
|
||||
<!-- GroupWelcomeView.kt -->
|
||||
<string name="group_welcome_title">Welcome message</string>
|
||||
<string name="save_welcome_message_question">Save welcome message?</string>
|
||||
<string name="save_and_update_group_profile">Save and update group profile</string>
|
||||
|
||||
<!-- ConnectionStats -->
|
||||
<string name="conn_stats_section_title_servers">SERVERS</string>
|
||||
<string name="receiving_via">Receiving via</string>
|
||||
@@ -979,13 +1016,26 @@
|
||||
<string name="update_network_settings_confirmation">Update</string>
|
||||
|
||||
<!-- UserProfilesView.kt -->
|
||||
<string name="your_chat_profiles_stored_locally">Your chat profiles are stored locally, only on your device</string>
|
||||
<string name="users_add">Add profile</string>
|
||||
<string name="users_delete_question">Delete chat profile?</string>
|
||||
<string name="users_delete_all_chats_deleted">All chats and messages will be deleted - this cannot be undone!</string>
|
||||
<string name="users_delete_profile_for">Delete chat profile for</string>
|
||||
<string name="users_delete_with_connections">Profile and server connections</string>
|
||||
<string name="users_delete_data_only">Local profile data only</string>
|
||||
<string name="user_hide">Hide</string>
|
||||
<string name="user_unhide">Unhide</string>
|
||||
<string name="user_mute">Mute</string>
|
||||
<string name="user_unmute">Unmute</string>
|
||||
<string name="enter_password_to_show">Enter password in search</string>
|
||||
<string name="tap_to_activate_profile">Tap to activate profile.</string>
|
||||
<string name="cant_delete_user_profile">Can\'t delete user profile!</string>
|
||||
<string name="should_be_at_least_one_visible_profile">There should be at least one visible user profile.</string>
|
||||
<string name="should_be_at_least_one_profile">There should be at least one user profile.</string>
|
||||
<string name="make_profile_private">Make profile private!</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">You can hide or mute a user profile - hold it for the menu.</string>
|
||||
<string name="dont_show_again">Don\'t show again</string>
|
||||
<string name="muted_when_inactive">Muted when inactive!</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">You will still receive calls and notifications from muted profiles when they are active.</string>
|
||||
|
||||
<!-- Incognito mode -->
|
||||
<string name="incognito">Incognito</string>
|
||||
@@ -1003,6 +1053,9 @@
|
||||
<string name="theme_light">Light</string>
|
||||
<string name="theme_dark">Dark</string>
|
||||
|
||||
<!-- Languages -->
|
||||
<string name="language_system">System</string>
|
||||
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Theme</string>
|
||||
<string name="save_color">Save color</string>
|
||||
@@ -1115,4 +1168,30 @@
|
||||
<string name="v4_4_live_messages_desc">Recipients see updates as you type them.</string>
|
||||
<string name="v4_4_verify_connection_security">Verify connection security</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Compare security codes with your contacts.</string>
|
||||
<string name="v4_4_french_interface">French interface</string>
|
||||
<string name="v4_4_french_interface_descr">Thanks to the users – contribute via Weblate!</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Multiple chat profiles</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Different names, avatars and transport isolation.</string>
|
||||
<string name="v4_5_message_draft">Message draft</string>
|
||||
<string name="v4_5_message_draft_descr">Preserve the last message draft, with attachments.</string>
|
||||
<string name="v4_5_transport_isolation">Transport isolation</string>
|
||||
<string name="v4_5_transport_isolation_descr">By chat profile (default) or by connection (BETA).</string>
|
||||
<string name="v4_5_private_filenames">Private filenames</string>
|
||||
<string name="v4_5_private_filenames_descr">To protect timezone, image/voice files use UTC.</string>
|
||||
<string name="v4_5_reduced_battery_usage">Reduced battery usage</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">More improvements are coming soon!</string>
|
||||
<string name="v4_5_italian_interface">Italian interface</string>
|
||||
<string name="v4_5_italian_interface_descr">Thanks to the users – contribute via Weblate!</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Hidden chat profiles</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Protect your chat profiles with a password!</string>
|
||||
<string name="v4_6_audio_video_calls">Audio and video calls</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Support bluetooth and other improvements.</string>
|
||||
<string name="v4_6_group_moderation">Group moderation</string>
|
||||
<string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members (\"observer\" role)</string>
|
||||
<string name="v4_6_group_welcome_message">Group welcome message</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Set the message shown to new members!</string>
|
||||
<string name="v4_6_reduced_battery_usage">Further reduced battery usage</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">More improvements are coming soon!</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Chinese and Spanish interface</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Thanks to the users – contribute via Weblate!</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<paths>
|
||||
<files-path name="my_files" path="/"/>
|
||||
<files-path name="app_temp" path="../app_temp"/>
|
||||
</paths>
|
||||
|
||||