mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05c4a6c682 | |||
| b2aec6d6a7 | |||
| 09c4609b6c | |||
| f6d2aa7aae | |||
| 92facf58f7 | |||
| 15c36c5a84 | |||
| cea0543e98 | |||
| c0bbe77788 | |||
| 9f8cbe140d | |||
| 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 |
@@ -75,3 +75,4 @@ website/package-lock.json
|
||||
# Ignore test files
|
||||
website/.cache
|
||||
website/test/stubs-layout-cache/_includes/*.js
|
||||
apps/android/app/release
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
| Updated 07.02.2023 | Languages: EN, [FR](/docs/lang/fr/README.md) |
|
||||
|
||||
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
||||
|
||||
# SimpleX - the first messaging platform that has no user identifiers of any kind - 100% private by design!
|
||||
|
||||
[](https://github.com/simplex-chat/simplex-chat/actions/workflows/build.yml)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://www.reddit.com/r/SimpleXChat)
|
||||
[](https://mastodon.social/@simplex)
|
||||
|
||||
| 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)
|
||||
@@ -26,7 +40,85 @@
|
||||
- 🚀 [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](#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
|
||||
|
||||
@@ -38,16 +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)
|
||||
- [Translate the apps](#translate-the-apps)
|
||||
- [Contribute](#contribute)
|
||||
- [Help us with donations](#help-us-with-donations)
|
||||
- [Disclaimers, Security contact, License](#disclaimers)
|
||||
|
||||
## Why privacy matters
|
||||
@@ -100,14 +187,6 @@ Recent updates:
|
||||
|
||||
[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
|
||||
@@ -225,70 +304,6 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- High capacity multi-node SMP relays.
|
||||
|
||||
## Join a user group
|
||||
|
||||
You can join an English-speaking group if you want to ask any questions: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D)
|
||||
|
||||
There are also several groups in languages other than English, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users. We do not always answer questions there, so please ask them in one of the English-speaking groups.
|
||||
|
||||
- [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking).
|
||||
- [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking).
|
||||
- [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking).
|
||||
- [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
|
||||
|
||||
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
|
||||
|
||||
Join via the app to share what's going on and ask any questions!
|
||||
|
||||
## Translate the apps
|
||||
|
||||
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps are translated to many other languages. Join our translators to help SimpleX grow faster!
|
||||
|
||||
Current interface languages:
|
||||
|
||||
- English (development language)
|
||||
- German: [@mlanp](https://github.com/mlanp)
|
||||
- French: [@ishi_sama](https://github.com/ishi-sama)
|
||||
- Italian: [@unbranched](https://github.com/unbranched)
|
||||
- Russian: project team
|
||||
|
||||
Languages in progress: Chinese, Hindi, Czech, Japanese, Dutch and [many others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love to have you join the development! You can contribute to SimpleX Chat with:
|
||||
|
||||
- translate website homepage - there is a lot of content we would like to share, it would help to bring the new users.
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
|
||||
## Help us with donations
|
||||
|
||||
Huge thank you to everybody who donated to SimpleX Chat!
|
||||
|
||||
We are prioritizing users privacy and security - it would be impossible without your support.
|
||||
|
||||
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
|
||||
|
||||
Your donations help us raise more funds – any amount, even the price of the cup of coffee, would make a big difference for us.
|
||||
|
||||
It is possible to donate via:
|
||||
|
||||
- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us.
|
||||
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
|
||||
- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2
|
||||
- Solana address: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L
|
||||
- please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list.
|
||||
|
||||
Thank you,
|
||||
|
||||
Evgeny
|
||||
|
||||
SimpleX Chat founder
|
||||
|
||||
## Disclaimers
|
||||
|
||||
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
|
||||
|
||||
@@ -9,15 +9,12 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
minSdk 26
|
||||
targetSdk 32
|
||||
versionCode 102
|
||||
versionName "4.5.3"
|
||||
versionCode 107
|
||||
versionName "4.6"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
abiFilters 'arm64-v8a'
|
||||
}
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
@@ -77,9 +74,26 @@ android {
|
||||
jniLibs.useLegacyPackaging = compression_level != "0"
|
||||
}
|
||||
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
|
||||
if (isRelease) {
|
||||
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
|
||||
// if (isRelease) {
|
||||
// Comma separated list of languages that will be included in the apk
|
||||
android.defaultConfig.resConfigs("en", "ru", "de", "fr", "it", "nl", "cs")
|
||||
android.defaultConfig.resConfigs("en", "cs", "de", "es", "fr", "it", "nl", "ru", "zh-rCN")
|
||||
// }
|
||||
if (isBundle) {
|
||||
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'
|
||||
} else {
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
if (isRelease) {
|
||||
include 'arm64-v8a', 'armeabi-v7a'
|
||||
} else {
|
||||
include 'arm64-v8a', 'armeabi-v7a'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +210,8 @@ tasks.register("compressApk") {
|
||||
|
||||
if (project.properties['android.injected.signing.key.alias'] != null && buildType == 'release') {
|
||||
new File(outputDir, "app-release.apk").renameTo(new File(outputDir, "simplex.apk"))
|
||||
new File(outputDir, "app-armeabi-v7a-release.apk").renameTo(new File(outputDir, "simplex-armv7a.apk"))
|
||||
new File(outputDir, "app-arm64-v8a-release.apk").renameTo(new File(outputDir, "simplex.apk"))
|
||||
}
|
||||
|
||||
// View all gradle properties set
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
android:extractNativeLibs="${extract_native_libs}"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.SimpleX">
|
||||
<!-- android:localeConfig="@xml/locales_config"-->
|
||||
|
||||
<!-- Main activity -->
|
||||
<activity
|
||||
|
||||
@@ -53,10 +53,6 @@ add_library( support SHARED IMPORTED )
|
||||
set_target_properties( support PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libsupport.so)
|
||||
|
||||
add_library( crypto SHARED IMPORTED )
|
||||
set_target_properties( crypto PROPERTIES IMPORTED_LOCATION
|
||||
${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libcrypto.so)
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link multiple libraries, such as libraries you define in this
|
||||
# build script, prebuilt third-party libraries, or system libraries.
|
||||
@@ -64,7 +60,7 @@ set_target_properties( crypto PROPERTIES IMPORTED_LOCATION
|
||||
target_link_libraries( # Specifies the target library.
|
||||
app-lib
|
||||
|
||||
simplex support crypto
|
||||
simplex support
|
||||
|
||||
# Links the target library to the log library
|
||||
# included in the NDK.
|
||||
|
||||
@@ -7,6 +7,17 @@ void hs_init(int * argc, char **argv[]);
|
||||
void setLineBuffering(void);
|
||||
int pipe_std_to_socket(const char * name);
|
||||
|
||||
extern void __svfscanf(void){};
|
||||
extern void __vfwscanf(void){};
|
||||
extern void __memset_chk_fail(void){};
|
||||
extern void __strcpy_chk_generic(void){};
|
||||
extern void __strcat_chk_generic(void){};
|
||||
extern void __libc_globals(void){};
|
||||
extern void __rel_iplt_start(void){};
|
||||
|
||||
// Android 9 only, not 13
|
||||
extern void reallocarray(void){};
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
|
||||
const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE);
|
||||
@@ -30,6 +41,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 +97,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)
|
||||
|
||||
@@ -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 }
|
||||
@@ -422,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,
|
||||
@@ -436,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,
|
||||
@@ -486,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
|
||||
@@ -963,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)
|
||||
@@ -1218,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() =
|
||||
|
||||
@@ -80,7 +80,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
|
||||
fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {
|
||||
notifyMessageReceived(
|
||||
displayNotification(
|
||||
user = user,
|
||||
chatId = cInfo.id,
|
||||
displayName = cInfo.displayName,
|
||||
@@ -91,7 +91,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
|
||||
fun notifyContactConnected(user: User, contact: Contact) {
|
||||
notifyMessageReceived(
|
||||
displayNotification(
|
||||
user = user,
|
||||
chatId = contact.id,
|
||||
displayName = contact.displayName,
|
||||
@@ -101,11 +101,11 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
|
||||
fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (!cInfo.ntfsEnabled) return
|
||||
|
||||
notifyMessageReceived(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
}
|
||||
|
||||
fun notifyMessageReceived(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
|
||||
fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
|
||||
if (!user.showNotifications) return
|
||||
Log.d(TAG, "notifyMessageReceived $chatId")
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
|
||||
|
||||
@@ -133,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)
|
||||
@@ -214,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"
|
||||
@@ -231,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"
|
||||
@@ -259,6 +265,16 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
chatModel.incognito.value = appPrefs.incognito.get()
|
||||
}
|
||||
|
||||
private fun currentUserId(funcName: String): Long {
|
||||
val userId = chatModel.currentUser.value?.userId
|
||||
if (userId == null) {
|
||||
val error = "$funcName: no current user"
|
||||
Log.e(TAG, error)
|
||||
throw Exception(error)
|
||||
}
|
||||
return userId
|
||||
}
|
||||
|
||||
suspend fun startChat(user: User) {
|
||||
Log.d(TAG, "user: $user")
|
||||
try {
|
||||
@@ -290,21 +306,26 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun changeActiveUser(toUserId: Long) {
|
||||
suspend fun changeActiveUser(toUserId: Long, viewPwd: String?) {
|
||||
try {
|
||||
changeActiveUser_(toUserId)
|
||||
changeActiveUser_(toUserId, viewPwd)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Unable to set active user: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.failed_to_active_user_title), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun changeActiveUser_(toUserId: Long) {
|
||||
chatModel.currentUser.value = apiSetActiveUser(toUserId)
|
||||
suspend fun changeActiveUser_(toUserId: Long, viewPwd: String?) {
|
||||
val currentUser = apiSetActiveUser(toUserId, viewPwd)
|
||||
chatModel.currentUser.value = currentUser
|
||||
val users = listUsers()
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
getUserChatData()
|
||||
val invitation = chatModel.callInvitations.values.firstOrNull { inv -> inv.user.userId == toUserId }
|
||||
if (invitation != null) {
|
||||
chatModel.callManager.reportNewIncomingCall(invitation.copy(user = currentUser))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUserChatData() {
|
||||
@@ -401,15 +422,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}")
|
||||
@@ -470,10 +509,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun apiGetChats(): List<Chat> {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "apiGetChats: no current user")
|
||||
return emptyList()
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("apiGetChats") }.getOrElse { return emptyList() }
|
||||
val r = sendCmd(CC.ApiGetChats(userId))
|
||||
if (r is CR.ApiChats) return r.chats
|
||||
Log.e(TAG, "failed getting the list of chats: ${r.responseType} ${r.details}")
|
||||
@@ -517,11 +553,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiDeleteMemberChatItem(groupId: Long, groupMemberId: Long, itemId: Long): Pair<ChatItem, ChatItem?>? {
|
||||
val r = sendCmd(CC.ApiDeleteMemberChatItem(groupId, groupMemberId, itemId))
|
||||
if (r is CR.ChatItemDeleted) return r.deletedChatItem.chatItem to r.toChatItem?.chatItem
|
||||
Log.e(TAG, "apiDeleteMemberChatItem bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getUserSMPServers(): Pair<List<ServerCfg>, List<String>>? {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "getUserSMPServers: no current user")
|
||||
return null
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("getUserSMPServers") }.getOrElse { return null }
|
||||
val r = sendCmd(CC.APIGetUserSMPServers(userId))
|
||||
if (r is CR.UserSMPServers) return r.smpServers to r.presetSMPServers
|
||||
Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}")
|
||||
@@ -529,10 +569,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun setUserSMPServers(smpServers: List<ServerCfg>): Boolean {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "setUserSMPServers: no current user")
|
||||
return false
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("setUserSMPServers") }.getOrElse { return false }
|
||||
val r = sendCmd(CC.APISetUserSMPServers(userId, smpServers))
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
@@ -548,8 +585,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun testSMPServer(smpServer: String): SMPTestFailure? {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("testSMPServer: no current user") }
|
||||
val r = sendCmd(CC.TestSMPServer(userId, smpServer))
|
||||
val userId = currentUserId("testSMPServer")
|
||||
val r = sendCmd(CC.APITestSMPServer(userId, smpServer))
|
||||
return when (r) {
|
||||
is CR.SmpTestResult -> r.smpTestFailure
|
||||
else -> {
|
||||
@@ -560,14 +597,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun getChatItemTTL(): ChatItemTTL {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("getChatItemTTL: no current user") }
|
||||
val userId = currentUserId("getChatItemTTL")
|
||||
val r = sendCmd(CC.APIGetChatItemTTL(userId))
|
||||
if (r is CR.ChatItemTTL) return ChatItemTTL.fromSeconds(r.chatItemTTL)
|
||||
throw Exception("failed to get chat item TTL: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun setChatItemTTL(chatItemTTL: ChatItemTTL) {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("setChatItemTTL: no current user") }
|
||||
val userId = currentUserId("setChatItemTTL")
|
||||
val r = sendCmd(CC.APISetChatItemTTL(userId, chatItemTTL.seconds))
|
||||
if (r is CR.CmdOk) return
|
||||
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
|
||||
@@ -751,10 +788,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun apiListContacts(): List<Contact>? {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "apiListContacts: no current user")
|
||||
return null
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("apiListContacts") }.getOrElse { return null }
|
||||
val r = sendCmd(CC.ApiListContacts(userId))
|
||||
if (r is CR.ContactsList) return r.contacts
|
||||
Log.e(TAG, "apiListContacts bad response: ${r.responseType} ${r.details}")
|
||||
@@ -762,10 +796,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun apiUpdateProfile(profile: Profile): Profile? {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "apiUpdateProfile: no current user")
|
||||
return null
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("apiUpdateProfile") }.getOrElse { return null }
|
||||
val r = sendCmd(CC.ApiUpdateProfile(userId, profile))
|
||||
if (r is CR.UserProfileNoChange) return profile
|
||||
if (r is CR.UserProfileUpdated) return r.toProfile
|
||||
@@ -795,10 +826,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun apiCreateUserAddress(): String? {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "apiCreateUserAddress: no current user")
|
||||
return null
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
|
||||
val r = sendCmd(CC.ApiCreateMyAddress(userId))
|
||||
return when (r) {
|
||||
is CR.UserContactLinkCreated -> r.connReqContact
|
||||
@@ -812,10 +840,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun apiDeleteUserAddress(): Boolean {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "apiDeleteUserAddress: no current user")
|
||||
return false
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("apiDeleteUserAddress") }.getOrElse { return false }
|
||||
val r = sendCmd(CC.ApiDeleteMyAddress(userId))
|
||||
if (r is CR.UserContactLinkDeleted) return true
|
||||
Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}")
|
||||
@@ -823,10 +848,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
private suspend fun apiGetUserAddress(): UserContactLinkRec? {
|
||||
val userId = chatModel.currentUser.value?.userId ?: run {
|
||||
Log.e(TAG, "apiGetUserAddress: no current user")
|
||||
return null
|
||||
}
|
||||
val userId = kotlin.runCatching { currentUserId("apiGetUserAddress") }.getOrElse { return null }
|
||||
val r = sendCmd(CC.ApiShowMyAddress(userId))
|
||||
if (r is CR.UserContactLink) return r.contactLink
|
||||
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
|
||||
@@ -838,10 +860,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
|
||||
@@ -961,10 +980,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}")
|
||||
@@ -1060,9 +1076,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiCreateGroupLink(groupId: Long): String? {
|
||||
return when (val r = sendCmd(CC.APICreateGroupLink(groupId))) {
|
||||
is CR.GroupLinkCreated -> r.connReqContact
|
||||
suspend fun apiCreateGroupLink(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? {
|
||||
return when (val r = sendCmd(CC.APICreateGroupLink(groupId, memberRole))) {
|
||||
is CR.GroupLinkCreated -> r.connReqContact to r.memberRole
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r)
|
||||
@@ -1072,6 +1088,18 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiGroupLinkMemberRole(groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? {
|
||||
return when (val r = sendCmd(CC.APIGroupLinkMemberRole(groupId, memberRole))) {
|
||||
is CR.GroupLink -> r.connReqContact to r.memberRole
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiGroupLinkMemberRole", generalGetString(R.string.error_updating_link_for_group), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiDeleteGroupLink(groupId: Long): Boolean {
|
||||
return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) {
|
||||
is CR.GroupLinkDeleted -> true
|
||||
@@ -1084,9 +1112,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiGetGroupLink(groupId: Long): String? {
|
||||
suspend fun apiGetGroupLink(groupId: Long): Pair<String, GroupMemberRole>? {
|
||||
return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) {
|
||||
is CR.GroupLink -> r.connReqContact
|
||||
is CR.GroupLink -> r.connReqContact to r.memberRole
|
||||
else -> {
|
||||
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
@@ -1185,7 +1213,11 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
val contactRequest = r.contactRequest
|
||||
val cInfo = ChatInfo.ContactRequest(contactRequest)
|
||||
if (active(r.user)) {
|
||||
chatModel.addChat(Chat(chatInfo = cInfo, chatItems = listOf()))
|
||||
if (chatModel.hasChat(contactRequest.id)) {
|
||||
chatModel.updateChatInfo(cInfo)
|
||||
} else {
|
||||
chatModel.addChat(Chat(chatInfo = cInfo, chatItems = listOf()))
|
||||
}
|
||||
}
|
||||
ntfManager.notifyContactRequestReceived(r.user, cInfo)
|
||||
}
|
||||
@@ -1271,7 +1303,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,
|
||||
@@ -1343,6 +1375,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 ->
|
||||
@@ -1362,8 +1398,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
|
||||
@@ -1729,8 +1766,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()
|
||||
@@ -1744,6 +1785,7 @@ sealed class CC {
|
||||
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean): CC()
|
||||
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
|
||||
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
|
||||
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
|
||||
class ApiNewGroup(val userId: Long, val groupProfile: GroupProfile): CC()
|
||||
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class ApiJoinGroup(val groupId: Long): CC()
|
||||
@@ -1752,12 +1794,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()
|
||||
@@ -1804,8 +1847,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"
|
||||
@@ -1819,6 +1866,7 @@ sealed class CC {
|
||||
is ApiSendMessage -> "/_send ${chatRef(type, id)} live=${onOff(live)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
|
||||
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
|
||||
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
|
||||
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
|
||||
is ApiNewGroup -> "/_group $userId ${json.encodeToString(groupProfile)}"
|
||||
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
|
||||
is ApiJoinGroup -> "/_join #$groupId"
|
||||
@@ -1827,12 +1875,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)}"
|
||||
@@ -1881,6 +1930,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"
|
||||
@@ -1895,6 +1948,7 @@ sealed class CC {
|
||||
is ApiSendMessage -> "apiSendMessage"
|
||||
is ApiUpdateChatItem -> "apiUpdateChatItem"
|
||||
is ApiDeleteChatItem -> "apiDeleteChatItem"
|
||||
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
|
||||
is ApiNewGroup -> "apiNewGroup"
|
||||
is ApiAddMember -> "apiAddMember"
|
||||
is ApiJoinGroup -> "apiJoinGroup"
|
||||
@@ -1904,11 +1958,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"
|
||||
@@ -1961,13 +2016,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}"
|
||||
|
||||
@@ -2820,6 +2890,13 @@ class APIResponse(val resp: CR, val corr: String? = null) {
|
||||
resp = CR.ApiChat(user, chat),
|
||||
corr = data["corr"]?.toString()
|
||||
)
|
||||
} else if (type == "chatCmdError") {
|
||||
val userObject = resp["user_"]?.jsonObject
|
||||
val user = runCatching<User?> { json.decodeFromJsonElement(userObject!!) }.getOrNull()
|
||||
return APIResponse(
|
||||
resp = CR.ChatCmdError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))),
|
||||
corr = data["corr"]?.toString()
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error while parsing chat(s): " + e.stackTraceToString())
|
||||
@@ -2876,6 +2953,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()
|
||||
@@ -2925,8 +3003,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()
|
||||
@@ -2949,8 +3027,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()
|
||||
|
||||
@@ -2979,6 +3057,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"
|
||||
@@ -3080,6 +3159,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)}")
|
||||
@@ -3129,8 +3209,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))
|
||||
@@ -3150,8 +3230,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
|
||||
}
|
||||
@@ -3223,11 +3303,13 @@ sealed class ChatError {
|
||||
is ChatErrorAgent -> "agent ${agentError.string}"
|
||||
is ChatErrorStore -> "store ${storeError.string}"
|
||||
is ChatErrorDatabase -> "database ${databaseError.string}"
|
||||
is ChatErrorInvalidJSON -> "invalid json ${json}"
|
||||
}
|
||||
@Serializable @SerialName("error") class ChatErrorChat(val errorType: ChatErrorType): ChatError()
|
||||
@Serializable @SerialName("errorAgent") class ChatErrorAgent(val agentError: AgentErrorType): ChatError()
|
||||
@Serializable @SerialName("errorStore") class ChatErrorStore(val storeError: StoreError): ChatError()
|
||||
@Serializable @SerialName("errorDatabase") class ChatErrorDatabase(val databaseError: DatabaseError): ChatError()
|
||||
@Serializable @SerialName("invalidJSON") class ChatErrorInvalidJSON(val json: String): ChatError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -17,6 +17,7 @@ enum class DefaultTheme {
|
||||
val DEFAULT_PADDING = 16.dp
|
||||
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
||||
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||
val DEFAULT_BOTTOM_PADDING = 48.dp
|
||||
|
||||
val DarkColorPalette = darkColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
|
||||
@@ -83,6 +83,8 @@ fun TerminalLayout(
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = sendCommand,
|
||||
sendLiveMessage = null,
|
||||
|
||||
@@ -13,12 +13,14 @@ class CallManager(val chatModel: ChatModel) {
|
||||
Log.d(TAG, "CallManager.reportNewIncomingCall")
|
||||
with (chatModel) {
|
||||
callInvitations[invitation.contact.id] = invitation
|
||||
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
|
||||
activeCallInvitation.value = invitation
|
||||
controller.ntfManager.notifyCallInvitation(invitation)
|
||||
} else {
|
||||
val contact = invitation.contact
|
||||
controller.ntfManager.notifyMessageReceived(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
||||
if (invitation.user.showNotifications) {
|
||||
if (Clock.System.now() - invitation.callTs <= 3.minutes) {
|
||||
activeCallInvitation.value = invitation
|
||||
controller.ntfManager.notifyCallInvitation(invitation)
|
||||
} else {
|
||||
val contact = invitation.contact
|
||||
controller.ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ package chat.simplex.app.views.call
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.*
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.media.*
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.*
|
||||
@@ -19,6 +20,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -37,7 +39,7 @@ import androidx.webkit.WebViewClientCompat
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
@@ -53,6 +55,7 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withApi { chatModel.callManager.endCall(call) }
|
||||
})
|
||||
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
|
||||
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name }
|
||||
LaunchedEffect(Unit) {
|
||||
// Start service when call happening since it's not already started.
|
||||
@@ -60,17 +63,48 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
if (!ntfModeService) SimplexService.start(SimplexApp.context)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
var btDeviceCount = 0
|
||||
val audioCallback = object: AudioDeviceCallback() {
|
||||
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
|
||||
super.onAudioDevicesAdded(addedDevices)
|
||||
val addedCount = addedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
btDeviceCount += addedCount
|
||||
audioViaBluetooth.value = btDeviceCount > 0
|
||||
if (addedCount > 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
|
||||
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
|
||||
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
|
||||
}
|
||||
}
|
||||
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
|
||||
super.onAudioDevicesRemoved(removedDevices)
|
||||
val removedCount = removedDevices.count { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
btDeviceCount -= removedCount
|
||||
audioViaBluetooth.value = btDeviceCount > 0
|
||||
if (btDeviceCount == 0 && chatModel.activeCall.value?.callState == CallState.Connected) {
|
||||
// Setting params in Connected state makes sure that Bluetooth will NOT be broken on Android < 12
|
||||
setCallSound(chatModel.activeCall.value?.soundSpeaker ?: return, audioViaBluetooth)
|
||||
}
|
||||
}
|
||||
}
|
||||
am.registerAudioDeviceCallback(audioCallback, null)
|
||||
val pm = (SimplexApp.context.getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
||||
pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, SimplexApp.context.packageName + ":proximityLock")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
proximityLock?.acquire()
|
||||
onDispose {
|
||||
// Stop it when call ended
|
||||
if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context)
|
||||
// Clear selected communication device to default value after we changed it in call
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.clearCommunicationDevice()
|
||||
}
|
||||
dropAudioManagerOverrides()
|
||||
am.unregisterAudioDeviceCallback(audioCallback)
|
||||
proximityLock?.release()
|
||||
}
|
||||
}
|
||||
val cxt = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
WebRTCView(chatModel.callCommand) { apiMsg ->
|
||||
@@ -100,6 +134,7 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
|
||||
if (callStatus == WebRTCCallStatus.Connected) {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected)
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
}
|
||||
withApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
|
||||
} catch (e: Error) {
|
||||
@@ -108,8 +143,7 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
is WCallResponse.Connected -> {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
|
||||
scope.launch {
|
||||
delay(2000L)
|
||||
setCallSound(cxt, call)
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
}
|
||||
}
|
||||
is WCallResponse.Ended -> {
|
||||
@@ -143,15 +177,18 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) ActiveCallOverlay(call, chatModel)
|
||||
if (call != null) ActiveCallOverlay(call, chatModel, audioViaBluetooth)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
val prevVolumeControlStream = activity.volumeControlStream
|
||||
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
|
||||
// Lock orientation to portrait in order to have good experience with calls
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
onDispose {
|
||||
activity.volumeControlStream = prevVolumeControlStream
|
||||
// Unlock orientation
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
@@ -159,10 +196,10 @@ fun ActiveCallView(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) {
|
||||
var cxt = LocalContext.current
|
||||
private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetooth: MutableState<Boolean>) {
|
||||
ActiveCallOverlayLayout(
|
||||
call = call,
|
||||
speakerCanBeEnabled = !audioViaBluetooth.value,
|
||||
dismiss = { withApi { chatModel.callManager.endCall(call) } },
|
||||
toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) },
|
||||
toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) },
|
||||
@@ -171,38 +208,55 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel) {
|
||||
if (call != null) {
|
||||
call = call.copy(soundSpeaker = !call.soundSpeaker)
|
||||
chatModel.activeCall.value = call
|
||||
setCallSound(cxt, call)
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
}
|
||||
},
|
||||
flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun setCallSound(cxt: Context, call: Call) {
|
||||
Log.d(TAG, "setCallSound: set audio mode")
|
||||
val am = cxt.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
if (call.soundSpeaker) {
|
||||
am.mode = AudioManager.MODE_NORMAL
|
||||
am.isSpeakerphoneOn = true
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
am.availableCommunicationDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }?.let {
|
||||
private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) {
|
||||
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker")
|
||||
am.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val btDevice = am.availableCommunicationDevices.lastOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
|
||||
val preferredSecondaryDevice = if (speaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
if (btDevice != null) {
|
||||
am.setCommunicationDevice(btDevice)
|
||||
} else if (am.communicationDevice?.type != preferredSecondaryDevice) {
|
||||
am.availableCommunicationDevices.firstOrNull { it.type == preferredSecondaryDevice }?.let {
|
||||
am.setCommunicationDevice(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
am.mode = AudioManager.MODE_IN_CALL
|
||||
am.isSpeakerphoneOn = false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
am.availableCommunicationDevices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }?.let {
|
||||
am.setCommunicationDevice(it)
|
||||
}
|
||||
if (audioViaBluetooth.value) {
|
||||
am.isSpeakerphoneOn = false
|
||||
am.startBluetoothSco()
|
||||
} else {
|
||||
am.stopBluetoothSco()
|
||||
am.isSpeakerphoneOn = speaker
|
||||
}
|
||||
am.isBluetoothScoOn = am.isBluetoothScoAvailableOffCall && audioViaBluetooth.value
|
||||
}
|
||||
}
|
||||
|
||||
private fun dropAudioManagerOverrides() {
|
||||
val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
am.mode = AudioManager.MODE_NORMAL
|
||||
// Clear selected communication device to default value after we changed it in call
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
am.clearCommunicationDevice()
|
||||
} else {
|
||||
am.isSpeakerphoneOn = false
|
||||
am.stopBluetoothSco()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActiveCallOverlayLayout(
|
||||
call: Call,
|
||||
speakerCanBeEnabled: Boolean,
|
||||
dismiss: () -> Unit,
|
||||
toggleAudio: () -> Unit,
|
||||
toggleVideo: () -> Unit,
|
||||
@@ -240,7 +294,7 @@ private fun ActiveCallOverlayLayout(
|
||||
CallInfoView(call, alignment = Alignment.CenterHorizontally)
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = 48.dp), contentAlignment = Alignment.CenterStart) {
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
IconButton(onClick = dismiss) {
|
||||
Icon(Icons.Filled.CallEnd, stringResource(R.string.icon_descr_hang_up), tint = Color.Red, modifier = Modifier.size(64.dp))
|
||||
@@ -251,7 +305,7 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||
Box(Modifier.padding(end = 32.dp)) {
|
||||
ToggleSoundButton(call, toggleSound)
|
||||
ToggleSoundButton(call, speakerCanBeEnabled, toggleSound)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,10 +315,10 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: Int, action: () -> Unit) {
|
||||
private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: Int, action: () -> Unit, enabled: Boolean = true) {
|
||||
if (call.hasMedia) {
|
||||
IconButton(onClick = action) {
|
||||
Icon(icon, stringResource(iconText), tint = Color(0xFFFFFFD8), modifier = Modifier.size(40.dp))
|
||||
IconButton(onClick = action, enabled = enabled) {
|
||||
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else HighOrLowlight, modifier = Modifier.size(40.dp))
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.size(40.dp))
|
||||
@@ -281,11 +335,11 @@ private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleSoundButton(call: Call, toggleSound: () -> Unit) {
|
||||
private fun ToggleSoundButton(call: Call, enabled: Boolean, toggleSound: () -> Unit) {
|
||||
if (call.soundSpeaker) {
|
||||
ControlButton(call, Icons.Outlined.VolumeUp, R.string.icon_descr_speaker_off, toggleSound)
|
||||
ControlButton(call, Icons.Outlined.VolumeUp, R.string.icon_descr_speaker_off, toggleSound, enabled)
|
||||
} else {
|
||||
ControlButton(call, Icons.Outlined.VolumeDown, R.string.icon_descr_speaker_on, toggleSound)
|
||||
ControlButton(call, Icons.Outlined.VolumeDown, R.string.icon_descr_speaker_on, toggleSound, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,6 +539,7 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
|
||||
)
|
||||
),
|
||||
speakerCanBeEnabled = true,
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
toggleVideo = {},
|
||||
@@ -509,6 +564,7 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
|
||||
)
|
||||
),
|
||||
speakerCanBeEnabled = true,
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
toggleVideo = {},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,16 +7,16 @@ import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.ImageDecoder.DecodeException
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.AnimatedImageDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.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,8 +32,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toFile
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -184,10 +186,11 @@ fun ComposeView(
|
||||
val textStyle = remember { mutableStateOf(smallFont) }
|
||||
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
|
||||
val bitmap: Bitmap? = getBitmapFromUri(uri)
|
||||
if (bitmap != null) {
|
||||
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
|
||||
}
|
||||
}
|
||||
}
|
||||
val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
|
||||
@@ -201,19 +204,12 @@ fun ComposeView(
|
||||
val content = ArrayList<UploadContent>()
|
||||
val imagesPreview = ArrayList<String>()
|
||||
uris.forEach { uri ->
|
||||
val source = ImageDecoder.createSource(context.contentResolver, uri)
|
||||
val drawable = try {
|
||||
ImageDecoder.decodeDrawable(source)
|
||||
} catch (e: DecodeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.image_decoding_exception_title),
|
||||
text = generalGetString(R.string.image_decoding_exception_desc)
|
||||
)
|
||||
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}")
|
||||
null
|
||||
}
|
||||
var bitmap: Bitmap? = if (drawable != null) ImageDecoder.decodeBitmap(source) else null
|
||||
if (drawable is AnimatedImageDrawable) {
|
||||
val drawable = getDrawableFromUri(uri)
|
||||
var bitmap: Bitmap? = if (drawable != null) getBitmapFromUri(uri) else null
|
||||
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
|
||||
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
|
||||
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
|
||||
if (isAnimNewApi || isAnimOldApi) {
|
||||
// It's a gif or webp
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
|
||||
@@ -645,6 +641,9 @@ fun ComposeView(
|
||||
chatModel.sharedContent.value = null
|
||||
}
|
||||
|
||||
val userCanSend = rememberUpdatedState(chat.userCanSend)
|
||||
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
|
||||
|
||||
Column {
|
||||
contextItemView()
|
||||
when {
|
||||
@@ -656,11 +655,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 +697,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 +739,8 @@ fun ComposeView(
|
||||
needToAllowVoiceToContact,
|
||||
allowedVoiceByPrefs,
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
userIsObserver = userIsObserver.value,
|
||||
userCanSend = userCanSend.value,
|
||||
sendMessage = {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
|
||||
@@ -8,10 +8,12 @@ import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.*
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
@@ -50,6 +52,7 @@ import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import kotlinx.coroutines.*
|
||||
import java.lang.reflect.Field
|
||||
|
||||
@Composable
|
||||
fun SendMsgView(
|
||||
@@ -60,6 +63,8 @@ fun SendMsgView(
|
||||
liveMessageAlertShown: SharedPreference<Boolean>,
|
||||
needToAllowVoiceToContact: Boolean,
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
userIsObserver: Boolean,
|
||||
userCanSend: Boolean,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
sendMessage: () -> Unit,
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
@@ -74,10 +79,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 +112,11 @@ fun SendMsgView(
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val stopRecOnNextClick = remember { mutableStateOf(false) }
|
||||
when {
|
||||
needToAllowVoiceToContact || !allowedVoiceByPrefs -> {
|
||||
DisallowedVoiceButton {
|
||||
needToAllowVoiceToContact || !allowedVoiceByPrefs || !userCanSend -> {
|
||||
DisallowedVoiceButton(userCanSend) {
|
||||
if (needToAllowVoiceToContact) {
|
||||
showNeedToAllowVoiceAlert(allowVoiceToContact)
|
||||
} else {
|
||||
} else if (!allowedVoiceByPrefs) {
|
||||
showDisabledVoiceAlert(isDirectChat)
|
||||
}
|
||||
}
|
||||
@@ -118,7 +131,7 @@ fun SendMsgView(
|
||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||
&& cs.contextItem is ComposeContextItem.NoContextItem) {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
StartLiveMessageButton {
|
||||
StartLiveMessageButton(userCanSend) {
|
||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
}
|
||||
@@ -173,6 +186,7 @@ private fun NativeKeyboard(
|
||||
composeState: MutableState<ComposeState>,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
onMessageChange: (String) -> Unit
|
||||
) {
|
||||
val cs = composeState.value
|
||||
@@ -229,7 +243,17 @@ private fun NativeKeyboard(
|
||||
editText.background = drawable
|
||||
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
|
||||
editText.setText(cs.message)
|
||||
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) }
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) }
|
||||
} else {
|
||||
try {
|
||||
val f: Field = TextView::class.java.getDeclaredField("mCursorDrawableRes")
|
||||
f.isAccessible = true
|
||||
f.set(editText, R.drawable.edit_text_cursor)
|
||||
} catch (e: Exception) {
|
||||
Log.e(chat.simplex.app.TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
|
||||
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
|
||||
editText
|
||||
@@ -253,15 +277,22 @@ private fun NativeKeyboard(
|
||||
showDeleteTextButton.value = it.lineCount >= 4
|
||||
}
|
||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
Text(
|
||||
generalGetString(R.string.voice_message_send_text),
|
||||
Modifier.padding(padding),
|
||||
color = HighOrLowlight,
|
||||
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
|
||||
)
|
||||
ComposeOverlay(R.string.voice_message_send_text, textStyle, padding)
|
||||
} else if (userIsObserver) {
|
||||
ComposeOverlay(R.string.you_are_observer, textStyle, padding)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComposeOverlay(textId: Int, textStyle: MutableState<TextStyle>, padding: PaddingValues) {
|
||||
Text(
|
||||
generalGetString(textId),
|
||||
Modifier.padding(padding),
|
||||
color = HighOrLowlight,
|
||||
style = textStyle.value.copy(fontStyle = FontStyle.Italic)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) {
|
||||
IconButton(
|
||||
@@ -322,8 +353,8 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisallowedVoiceButton(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp), enabled = enabled) {
|
||||
Icon(
|
||||
Icons.Outlined.KeyboardVoice,
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
@@ -454,13 +485,13 @@ private fun SendMsgButton(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartLiveMessageButton(onClick: () -> Unit) {
|
||||
private fun StartLiveMessageButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
enabled = true,
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
||||
@@ -470,7 +501,7 @@ private fun StartLiveMessageButton(onClick: () -> Unit) {
|
||||
Icon(
|
||||
Icons.Filled.Bolt,
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
tint = if (enabled) MaterialTheme.colors.primary else HighOrLowlight,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
@@ -571,6 +602,8 @@ fun PreviewSendMsgView() {
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
@@ -599,6 +632,8 @@ fun PreviewSendMsgViewEditing() {
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
@@ -627,6 +662,8 @@ fun PreviewSendMsgViewInProgress() {
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
userCanSend = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
|
||||
+32
-5
@@ -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(
|
||||
|
||||
+94
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -25,6 +27,7 @@ import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.ComposeContextItem
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
// TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code)
|
||||
@@ -91,6 +94,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(
|
||||
@@ -123,9 +134,16 @@ fun ChatItemView(
|
||||
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
|
||||
val filePath = getLoadedFilePath(context, cItem.file)
|
||||
if (filePath != null) {
|
||||
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
ItemAction(stringResource(R.string.save_verb), Icons.Outlined.SaveAlt, onClick = {
|
||||
when (cItem.content.msgContent) {
|
||||
is MsgContent.MCImage -> saveImage(context, cItem.file)
|
||||
is MsgContent.MCImage -> {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
|
||||
saveImage(context, cItem.file)
|
||||
} else {
|
||||
writePermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
is MsgContent.MCVoice -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
else -> {}
|
||||
@@ -153,6 +171,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 +185,16 @@ fun ChatItemView(
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(R.string.reveal_verb),
|
||||
Icons.Outlined.Visibility,
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
if (!cItem.isDeletedContent) {
|
||||
ItemAction(
|
||||
stringResource(R.string.reveal_verb),
|
||||
Icons.Outlined.Visibility,
|
||||
onClick = {
|
||||
revealed.value = true
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
@@ -238,8 +262,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 -> DeletedItem()
|
||||
is CIContent.RcvModerated -> DeletedItem()
|
||||
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
@@ -264,6 +288,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) {
|
||||
@@ -308,6 +350,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),
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Flag
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -20,6 +21,7 @@ import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.*
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.util.fastMap
|
||||
@@ -75,10 +77,7 @@ fun FramedItemView(
|
||||
Modifier
|
||||
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp)
|
||||
.padding(end = 12.dp)
|
||||
.padding(top = 6.dp)
|
||||
.padding(bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
|
||||
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
if (icon != null) {
|
||||
@@ -96,6 +95,8 @@ fun FramedItemView(
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -166,7 +167,11 @@ fun FramedItemView(
|
||||
Column(Modifier.width(IntrinsicSize.Max)) {
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
if (ci.meta.itemDeleted != null) {
|
||||
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
|
||||
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
|
||||
FramedItemHeader(String.format(stringResource(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName), true, Icons.Outlined.Flag)
|
||||
} else {
|
||||
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
|
||||
}
|
||||
} else if (ci.meta.isLive) {
|
||||
FramedItemHeader(stringResource(R.string.live), false)
|
||||
}
|
||||
|
||||
+23
-10
@@ -1,8 +1,7 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -10,6 +9,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -30,19 +30,32 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
||||
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
|
||||
MarkedDeletedText(String.format(generalGetString(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
|
||||
} else {
|
||||
MarkedDeletedText(generalGetString(R.string.marked_deleted_description))
|
||||
}
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarkedDeletedText(text: String) {
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
// appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(text) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.annotatedStringResource
|
||||
import chat.simplex.app.views.helpers.openUriCatching
|
||||
import chat.simplex.app.views.usersettings.MarkdownHelpView
|
||||
import chat.simplex.app.views.usersettings.simplexTeamUri
|
||||
|
||||
@@ -36,7 +37,7 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||
Text(
|
||||
annotatedStringResource(R.string.you_can_connect_to_simplex_chat_founder),
|
||||
modifier = Modifier.clickable(onClick = {
|
||||
uriHandler.openUri(simplexTeamUri)
|
||||
uriHandler.openUriCatching(simplexTeamUri)
|
||||
}),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
|
||||
@@ -132,7 +132,7 @@ private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
|
||||
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
ConnectButton(generalGetString(R.string.chat_with_developers)) {
|
||||
uriHandler.openUri(simplexTeamUri)
|
||||
uriHandler.openUriCatching(simplexTeamUri)
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
ConnectButton(generalGetString(R.string.tap_to_start_new_chat), openNewChatSheet)
|
||||
@@ -210,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) {
|
||||
|
||||
@@ -9,11 +9,12 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -36,7 +37,13 @@ import kotlin.math.roundToInt
|
||||
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 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 {
|
||||
@@ -104,13 +111,12 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
chatModel.chats.clear()
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
switchingUsers.value = true
|
||||
}
|
||||
chatModel.controller.changeActiveUser(u.user.userId)
|
||||
chatModel.controller.changeActiveUser(u.user.userId, null)
|
||||
job.cancel()
|
||||
switchingUsers.value = false
|
||||
}
|
||||
@@ -144,33 +150,19 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProfileImage(
|
||||
image = u.image,
|
||||
size = 54.dp
|
||||
)
|
||||
Text(
|
||||
u.displayName,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp),
|
||||
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
UserProfileRow(u)
|
||||
if (u.activeUser) {
|
||||
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
} else if (u.hidden) {
|
||||
Icon(Icons.Outlined.Lock, null, Modifier.size(20.dp), tint = HighOrLowlight)
|
||||
} else if (unreadCount > 0) {
|
||||
Row {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
color = Color.White,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.background(if (u.showNtfs) MaterialTheme.colors.primary else HighOrLowlight, shape = CircleShape)
|
||||
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp),
|
||||
@@ -179,12 +171,35 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
}
|
||||
} else {
|
||||
} else if (!u.showNtfs) {
|
||||
Icon(Icons.Outlined.NotificationsOff, null, Modifier.size(20.dp), tint = HighOrLowlight)
|
||||
} else {
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfileRow(u: User) {
|
||||
Row(
|
||||
Modifier
|
||||
.widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f)
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ProfileImage(
|
||||
image = u.image,
|
||||
size = 54.dp
|
||||
)
|
||||
Text(
|
||||
u.displayName,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp),
|
||||
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
|
||||
|
||||
+26
-8
@@ -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,
|
||||
|
||||
@@ -8,7 +8,6 @@ import SectionView
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.FileUtils
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
@@ -40,6 +39,7 @@ import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.*
|
||||
import org.apache.commons.io.IOUtils
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -154,7 +154,7 @@ fun DatabaseLayout(
|
||||
val operationsDisabled = !stopped || progressIndicator
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 48.dp),
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = DEFAULT_BOTTOM_PADDING),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_chat_database))
|
||||
@@ -620,7 +620,7 @@ private fun saveArchiveFromUri(context: Context, importedArchiveUri: Uri): Strin
|
||||
if (inputStream != null && archiveName != null) {
|
||||
val archivePath = "${context.cacheDir}/$archiveName"
|
||||
val destFile = File(archivePath)
|
||||
FileUtils.copy(inputStream, FileOutputStream(destFile))
|
||||
IOUtils.copy(inputStream, FileOutputStream(destFile))
|
||||
archivePath
|
||||
} else {
|
||||
Log.e(TAG, "saveArchiveFromUri null inputStream")
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -22,7 +23,11 @@ fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit
|
||||
Modifier
|
||||
.padding(top = 4.dp), // Like in DefaultAppBar
|
||||
content = {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NavigationButtonBack(close)
|
||||
Row {
|
||||
endButtons()
|
||||
|
||||
@@ -34,7 +34,7 @@ fun DefaultTopAppBar(
|
||||
if (!showSearch) {
|
||||
title?.invoke()
|
||||
} else {
|
||||
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), onSearchValueChanged)
|
||||
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), alwaysVisible = false, onSearchValueChanged)
|
||||
}
|
||||
},
|
||||
backgroundColor = if (isInDarkTheme()) ToolbarDark else ToolbarLight,
|
||||
|
||||
@@ -6,7 +6,6 @@ import android.content.*
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.*
|
||||
import android.graphics.ImageDecoder.DecodeException
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.util.Base64
|
||||
@@ -114,7 +113,7 @@ fun base64ToBitmap(base64ImageString: String): Bitmap {
|
||||
class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() {
|
||||
@CallSuper
|
||||
override fun createIntent(context: Context, input: Void?): Intent {
|
||||
tmpFile = File.createTempFile("image", ".bmp", context.filesDir)
|
||||
tmpFile = File.createTempFile("image", ".bmp", File(getAppFilesDirectory(SimplexApp.context)))
|
||||
// Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system
|
||||
tmpFile?.deleteOnExit()
|
||||
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!)
|
||||
@@ -205,17 +204,10 @@ fun GetImageBottomSheet(
|
||||
val context = LocalContext.current
|
||||
val processPickedImage = { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val source = ImageDecoder.createSource(context.contentResolver, uri)
|
||||
try {
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
val bitmap = getBitmapFromUri(uri)
|
||||
if (bitmap != null) {
|
||||
imageBitmap.value = uri
|
||||
onImageChange(bitmap)
|
||||
} catch (e: DecodeException) {
|
||||
Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.image_decoding_exception_title),
|
||||
text = generalGetString(R.string.image_decoding_exception_desc)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
@@ -29,15 +30,18 @@ import kotlinx.coroutines.delay
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (String) -> Unit) {
|
||||
fun SearchTextField(modifier: Modifier, placeholder: String, alwaysVisible: Boolean, onValueChange: (String) -> Unit) {
|
||||
var searchText by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
delay(200)
|
||||
keyboard?.show()
|
||||
if (!alwaysVisible) {
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
delay(200)
|
||||
keyboard?.show()
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
@@ -87,7 +91,14 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
|
||||
Text(placeholder)
|
||||
},
|
||||
trailingIcon = if (searchText.text.isNotEmpty()) {{
|
||||
IconButton({ searchText = TextFieldValue(""); onValueChange("") }) {
|
||||
IconButton({
|
||||
if (alwaysVisible) {
|
||||
keyboard?.hide()
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
searchText = TextFieldValue("");
|
||||
onValueChange("")
|
||||
}) {
|
||||
Icon(Icons.Default.Close, stringResource(R.string.icon_descr_close_button), tint = MaterialTheme.colors.primary,)
|
||||
}
|
||||
}} else null,
|
||||
|
||||
@@ -85,13 +85,14 @@ fun SectionItemView(
|
||||
click: (() -> Unit)? = null,
|
||||
minHeight: Dp = 46.dp,
|
||||
disabled: Boolean = false,
|
||||
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
|
||||
content: (@Composable RowScope.() -> Unit)
|
||||
) {
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = minHeight)
|
||||
Row(
|
||||
if (click == null || disabled) modifier.padding(horizontal = DEFAULT_PADDING) else modifier.clickable(onClick = click).padding(horizontal = DEFAULT_PADDING),
|
||||
if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
content()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.Manifest
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
@@ -81,6 +82,7 @@ fun imageMimeType(fileName: String): String {
|
||||
}
|
||||
}
|
||||
|
||||
/** Before calling, make sure the user allows to write to external storage [Manifest.permission.WRITE_EXTERNAL_STORAGE] */
|
||||
fun saveImage(cxt: Context, ciFile: CIFile?) {
|
||||
val filePath = getLoadedFilePath(cxt, ciFile)
|
||||
val fileName = ciFile?.fileName
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
//import android.app.LocaleManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.*
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.FileUtils
|
||||
import android.os.*
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
@@ -29,11 +34,12 @@ import androidx.compose.ui.unit.*
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.text.HtmlCompat
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.CIFile
|
||||
import chat.simplex.app.model.json
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.apache.commons.io.IOUtils
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -319,6 +325,14 @@ fun getFileName(context: Context, uri: Uri): String? {
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppFilePath(context: Context, uri: Uri): String? {
|
||||
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
getAppFilePath(context, cursor.getString(nameIndex))
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileSize(context: Context, uri: Uri): Long? {
|
||||
return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
|
||||
@@ -327,9 +341,48 @@ fun getFileSize(context: Context, uri: Uri): Long? {
|
||||
}
|
||||
}
|
||||
|
||||
fun getBitmapFromUri(uri: Uri, withAlertOnException: Boolean = true): Bitmap? {
|
||||
return if (Build.VERSION.SDK_INT >= 28) {
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
try {
|
||||
ImageDecoder.decodeBitmap(source)
|
||||
} catch (e: android.graphics.ImageDecoder.DecodeException) {
|
||||
Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}")
|
||||
if (withAlertOnException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.image_decoding_exception_title),
|
||||
text = generalGetString(R.string.image_decoding_exception_desc)
|
||||
)
|
||||
}
|
||||
null
|
||||
}
|
||||
} else {
|
||||
BitmapFactory.decodeFile(getAppFilePath(SimplexApp.context, uri))
|
||||
}
|
||||
}
|
||||
|
||||
fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable? {
|
||||
return if (Build.VERSION.SDK_INT >= 28) {
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
try {
|
||||
ImageDecoder.decodeDrawable(source)
|
||||
} catch (e: android.graphics.ImageDecoder.DecodeException) {
|
||||
if (withAlertOnException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.image_decoding_exception_title),
|
||||
text = generalGetString(R.string.image_decoding_exception_desc)
|
||||
)
|
||||
}
|
||||
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Drawable.createFromPath(getAppFilePath(SimplexApp.context, uri))
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(context: Context, uri: Uri): String? {
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
val bitmap = getBitmapFromUri(uri) ?: return null
|
||||
return saveImage(context, bitmap)
|
||||
}
|
||||
|
||||
@@ -400,7 +453,7 @@ fun saveFileFromUri(context: Context, uri: Uri): String? {
|
||||
if (inputStream != null && fileToSave != null) {
|
||||
val destFileName = uniqueCombine(context, fileToSave)
|
||||
val destFile = File(getAppFilePath(context, destFileName))
|
||||
FileUtils.copy(inputStream, FileOutputStream(destFile))
|
||||
IOUtils.copy(inputStream, FileOutputStream(destFile))
|
||||
destFileName
|
||||
} else {
|
||||
Log.e(chat.simplex.app.TAG, "Util.kt saveFileFromUri null inputStream")
|
||||
@@ -501,3 +554,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -35,7 +35,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
Icon(
|
||||
Icons.Outlined.OpenInNew, stringResource(titleId), tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable { uriHandler.openUri(link) }
|
||||
.clickable { uriHandler.openUriCatching(link) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -270,8 +270,45 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.6",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Lock,
|
||||
titleId = R.string.v4_6_hidden_chat_profiles,
|
||||
descrId = R.string.v4_6_hidden_chat_profiles_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Flag,
|
||||
titleId = R.string.v4_6_group_moderation,
|
||||
descrId = R.string.v4_6_group_moderation_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.MapsUgc,
|
||||
titleId = R.string.v4_6_group_welcome_message,
|
||||
descrId = R.string.v4_6_group_welcome_message_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Call,
|
||||
titleId = R.string.v4_6_audio_video_calls,
|
||||
descrId = R.string.v4_6_audio_video_calls_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Battery3Bar,
|
||||
titleId = R.string.v4_6_reduced_battery_usage,
|
||||
descrId = R.string.v4_6_reduced_battery_usage_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v4_6_chinese_spanish_interface,
|
||||
descrId = R.string.v4_6_chinese_spanish_interface_descr,
|
||||
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
|
||||
fun setLastVersionDefault(m: ChatModel) {
|
||||
|
||||
@@ -2,13 +2,20 @@ package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionCustomFooter
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionItemWithValue
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
||||
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -17,6 +24,7 @@ import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
@@ -30,9 +38,13 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.SharedPreference
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.godaddy.android.colorpicker.*
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.*
|
||||
|
||||
enum class AppIcon(val resId: Int) {
|
||||
DEFAULT(R.mipmap.icon),
|
||||
@@ -40,7 +52,7 @@ enum class AppIcon(val resId: Int) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppearanceView() {
|
||||
fun AppearanceView(m: ChatModel) {
|
||||
val appIcon = remember { mutableStateOf(findEnabledIcon()) }
|
||||
|
||||
fun setAppIcon(newIcon: AppIcon) {
|
||||
@@ -62,6 +74,7 @@ fun AppearanceView() {
|
||||
|
||||
AppearanceLayout(
|
||||
appIcon,
|
||||
m.controller.appPrefs.appLanguage,
|
||||
changeIcon = ::setAppIcon,
|
||||
editPrimaryColor = { primary ->
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
@@ -73,6 +86,7 @@ fun AppearanceView() {
|
||||
|
||||
@Composable fun AppearanceLayout(
|
||||
icon: MutableState<AppIcon>,
|
||||
languagePref: SharedPreference<String?>,
|
||||
changeIcon: (AppIcon) -> Unit,
|
||||
editPrimaryColor: (Color) -> Unit,
|
||||
) {
|
||||
@@ -81,6 +95,37 @@ fun AppearanceView() {
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.appearance_settings))
|
||||
SectionView(stringResource(R.string.settings_section_title_language), padding = PaddingValues()) {
|
||||
val context = LocalContext.current
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// SectionItemWithValue(
|
||||
// generalGetString(R.string.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
|
||||
// remember { mutableStateOf("system") },
|
||||
// listOf(ValueTitleDesc("system", generalGetString(R.string.change_verb), "")),
|
||||
// onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) }
|
||||
// )
|
||||
// } else {
|
||||
val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") }
|
||||
SectionItemView {
|
||||
LangSelector(state) {
|
||||
state.value = it
|
||||
withApi {
|
||||
delay(200)
|
||||
val activity = context as? Activity
|
||||
if (activity != null) {
|
||||
if (it == "system") {
|
||||
saveAppLocale(languagePref, activity)
|
||||
} else {
|
||||
saveAppLocale(languagePref, activity, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
|
||||
LazyRow {
|
||||
items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index ->
|
||||
@@ -179,6 +224,32 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LangSelector(state: State<String>, onSelected: (String) -> Unit) {
|
||||
// Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs`
|
||||
val supportedLanguages = mapOf(
|
||||
"system" to generalGetString(R.string.language_system),
|
||||
"en" to "English",
|
||||
"cs" to "Čeština",
|
||||
"de" to "Deutsch",
|
||||
"es" to "Español",
|
||||
"fr" to "Français",
|
||||
"it" to "Italiano",
|
||||
"nl" to "Nederlands",
|
||||
"ru" to "Русский",
|
||||
"zh-CN" to "简体中文"
|
||||
)
|
||||
val values by remember { mutableStateOf(supportedLanguages.map { it.key to it.value }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() },
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme) -> Unit) {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
@@ -193,6 +264,9 @@ private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme)
|
||||
)
|
||||
}
|
||||
|
||||
//private fun openSystemLangPicker(activity: Activity) {
|
||||
// activity.startActivity(Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:" + SimplexApp.context.packageName)))
|
||||
//}
|
||||
|
||||
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
|
||||
SimplexApp.context.packageManager.getComponentEnabledSetting(
|
||||
@@ -206,6 +280,7 @@ fun PreviewAppearanceSettings() {
|
||||
SimpleXTheme {
|
||||
AppearanceLayout(
|
||||
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
|
||||
languagePref = SharedPreference({ null }, {}),
|
||||
changeIcon = {},
|
||||
editPrimaryColor = {},
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -43,16 +44,23 @@ fun CallSettingsLayout(
|
||||
AppBarTitle(stringResource(R.string.your_calls))
|
||||
val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) }
|
||||
SectionView(stringResource(R.string.settings_section_title_settings)) {
|
||||
SectionItemView() {
|
||||
SharedPreferenceToggle(stringResource(R.string.connect_calls_via_relay), webrtcPolicyRelay)
|
||||
}
|
||||
SectionItemView(editIceServers) { Text(stringResource(R.string.webrtc_ice_servers)) }
|
||||
SectionDivider()
|
||||
|
||||
val enabled = remember { mutableStateOf(true) }
|
||||
SectionItemView { LockscreenOpts(lockCallState, enabled, onSelected = { callOnLockScreen.set(it); lockCallState.value = it }) }
|
||||
SectionDivider()
|
||||
SectionItemView(editIceServers) { Text(stringResource(R.string.webrtc_ice_servers)) }
|
||||
SectionItemView() {
|
||||
SharedPreferenceToggle(stringResource(R.string.always_use_relay), webrtcPolicyRelay)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(
|
||||
if (remember { webrtcPolicyRelay.state }.value) {
|
||||
generalGetString(R.string.relay_server_protects_ip)
|
||||
} else {
|
||||
generalGetString(R.string.relay_server_if_necessary)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+89
@@ -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))
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -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) },
|
||||
|
||||
@@ -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(
|
||||
|
||||
+91
-74
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
+29
-10
@@ -6,6 +6,7 @@ import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
@@ -13,6 +14,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -20,7 +22,9 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.*
|
||||
@@ -58,6 +62,18 @@ 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 = rememberSaveable { mutableStateOf("") }
|
||||
ModalView(
|
||||
{ close() },
|
||||
if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight,
|
||||
endButtons = {
|
||||
SearchTextField(Modifier.fillMaxWidth(), stringResource(android.R.string.search_go), alwaysVisible = true) { search.value = it }
|
||||
},
|
||||
content = { modalView(chatModel, search) })
|
||||
}
|
||||
},
|
||||
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showVersion = {
|
||||
withApi {
|
||||
@@ -115,6 +131,7 @@ 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),
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (block: () -> Unit) -> Unit
|
||||
@@ -141,7 +158,8 @@ fun SettingsLayout(
|
||||
ProfilePreview(profile, stopped = stopped)
|
||||
}
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.ManageAccounts, stringResource(R.string.your_chat_profiles), { withAuth { showSettingsModal { UserProfilesView(it) }() } }, disabled = stopped)
|
||||
val profileHidden = rememberSaveable { mutableStateOf(false) }
|
||||
SettingsActionItem(Icons.Outlined.ManageAccounts, stringResource(R.string.your_chat_profiles), { withAuth { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { showModal { IncognitoView() }() }
|
||||
SectionDivider()
|
||||
@@ -154,13 +172,13 @@ fun SettingsLayout(
|
||||
SectionView(stringResource(R.string.settings_section_title_settings)) {
|
||||
SettingsActionItem(Icons.Outlined.Bolt, stringResource(R.string.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal) }, disabled = stopped)
|
||||
SettingsActionItem(Icons.Outlined.WifiTethering, stringResource(R.string.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal, showCustomModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView() }, disabled = stopped)
|
||||
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
|
||||
}
|
||||
@@ -173,9 +191,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()
|
||||
|
||||
@@ -313,7 +331,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable private fun ContributeItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#contribute") }) {
|
||||
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) {
|
||||
Icon(
|
||||
Icons.Outlined.Keyboard,
|
||||
contentDescription = "GitHub",
|
||||
@@ -326,8 +344,8 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
|
||||
@Composable private fun RateAppItem(uriHandler: UriHandler) {
|
||||
SectionItemView({
|
||||
runCatching { uriHandler.openUri("market://details?id=chat.simplex.app") }
|
||||
.onFailure { uriHandler.openUri("https://play.google.com/store/apps/details?id=chat.simplex.app") }
|
||||
runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") }
|
||||
.onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
@@ -341,7 +359,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",
|
||||
@@ -365,7 +383,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",
|
||||
@@ -531,6 +549,7 @@ fun PreviewSettingsLayout() {
|
||||
setPerformLA = {},
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showSettingsModalWithSearch = { },
|
||||
showCustomModal = { {} },
|
||||
showVersion = {},
|
||||
withAuth = {},
|
||||
|
||||
+205
-41
@@ -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.copy(activeUser = false))
|
||||
}
|
||||
} 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)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<solid android:color="@color/highOrLowLight" />
|
||||
<size android:width="1dp" />
|
||||
</shape>
|
||||
@@ -1,2 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<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>
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="allow_voice_messages_only_if">Povolte hlasové zprávy, pouze pokud je váš kontakt povolí.</string>
|
||||
<string name="allow_to_send_disappearing">Povolit odesílání mizejících zpráv.</string>
|
||||
<string name="allow_to_send_voice">Povolit odesílání hlasových zpráv.</string>
|
||||
<string name="allow_voice_messages_only_if">Povolit hlasové zprávy, pokud je váš kontakt povolí.</string>
|
||||
<string name="allow_to_send_disappearing">Mizící zprávy povoleny.</string>
|
||||
<string name="allow_to_send_voice">Hlasové zprávy povoleny.</string>
|
||||
<string name="v4_2_group_links_desc">Správci mohou vytvářet odkazy pro připojení ke skupinám.</string>
|
||||
<string name="accept_contact_button">Přijmout</string>
|
||||
<string name="smp_servers_preset_add">Přidejte přednastavené servery</string>
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="smp_servers_add">Přidat server…</string>
|
||||
<string name="network_enable_socks_info">Přistupovat k serverům přes SOCKS proxy na portu 9050\? Před povolením této možnosti musí být spuštěna proxy.</string>
|
||||
<string name="accept_feature">Přijmout</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Umožněte svým kontaktům odesílat mizející zprávy.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Umožněte svým kontaktům odesílat mizící zprávy.</string>
|
||||
<string name="about_simplex_chat">O <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="smp_servers_add_to_another_device">Přidat do jiného zařízení</string>
|
||||
<string name="accept_requests">Přijímat žádosti</string>
|
||||
@@ -24,7 +24,7 @@
|
||||
<string name="group_member_role_admin">správce</string>
|
||||
<string name="users_add">Přidat profil</string>
|
||||
<string name="users_delete_all_chats_deleted">Všechny chaty a zprávy budou smazány – tuto akci nelze vrátit zpět!</string>
|
||||
<string name="allow_disappearing_messages_only_if">Povolte mizející zprávy, pouze pokud to váš kontakt povolí.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Povolte mizící zprávy, pouze pokud to váš kontakt povolí.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Přidejte servery skenováním QR kódů.</string>
|
||||
<string name="chat_item_ttl_month">1 měsíci</string>
|
||||
<string name="chat_item_ttl_week">1 týdnu</string>
|
||||
@@ -33,8 +33,8 @@
|
||||
<string name="accept_connection_request__question">Přijmout žádost o připojení\?</string>
|
||||
<string name="all_group_members_will_remain_connected">Všichni členové skupiny zůstanou připojeni.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí.</string>
|
||||
<string name="allow_direct_messages">Povolit odesílání přímých zpráv členům.</string>
|
||||
<string name="allow_to_delete_messages">Povolit nevratné smazání odeslaných zpráv.</string>
|
||||
<string name="allow_direct_messages">Přímé zprávy členům povoleny.</string>
|
||||
<string name="allow_to_delete_messages">Nevratné mazání odeslaných zpráv povoleno.</string>
|
||||
<string name="clear_chat_warning">Všechny zprávy budou smazány – tuto akci nelze vrátit zpět! Zprávy budou smazány POUZE pro vás.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Umožněte svým kontaktům nevratně odstranit odeslané zprávy.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Povolte svým kontaktům odesílání hlasových zpráv.</string>
|
||||
@@ -43,14 +43,14 @@
|
||||
<string name="button_send_direct_message">Odeslat přímou zprávu</string>
|
||||
<string name="member_info_section_title_member">ČLEN</string>
|
||||
<string name="change_member_role_question">Změnit roli ve skupině\?</string>
|
||||
<string name="info_row_connection">Připojení</string>
|
||||
<string name="info_row_connection">Připoj</string>
|
||||
<string name="conn_level_desc_indirect">nepřímé (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="conn_stats_section_title_servers">SERVERY</string>
|
||||
<string name="receiving_via">Příjímáno přez</string>
|
||||
<string name="create_secret_group_title">Vytvoření tajné skupiny</string>
|
||||
<string name="group_display_name_field">Zobrazení názvu skupiny:</string>
|
||||
<string name="group_display_name_field">Zobrazený název skupiny:</string>
|
||||
<string name="group_full_name_field">Úplný název skupiny:</string>
|
||||
<string name="group_main_profile_sent">Váš profil v chatu bude zaslán členům skupiny</string>
|
||||
<string name="group_main_profile_sent">Váš chat profil bude zaslán členům skupiny</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Profil skupiny je uložen v zařízeních členů, nikoli na serverech.</string>
|
||||
<string name="network_options_save">Uložit</string>
|
||||
<string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string>
|
||||
@@ -58,8 +58,8 @@
|
||||
<string name="incognito_random_profile">Váš náhodný profil</string>
|
||||
<string name="incognito_random_profile_description">Vašemu kontaktu bude zaslán náhodný profil</string>
|
||||
<string name="save_color">Uložit barvu</string>
|
||||
<string name="reset_color">Obnovení barev</string>
|
||||
<string name="color_primary">Přízvuk</string>
|
||||
<string name="reset_color">Obnovit barvu</string>
|
||||
<string name="color_primary">Zbarvení</string>
|
||||
<string name="chat_preferences_you_allow">Povolujete</string>
|
||||
<string name="chat_preferences_default">výchozí (%s)</string>
|
||||
<string name="chat_preferences_yes">ano</string>
|
||||
@@ -67,29 +67,29 @@
|
||||
<string name="chat_preferences_always">vždy</string>
|
||||
<string name="set_group_preferences">Nastavení skupinových předvoleb</string>
|
||||
<string name="your_preferences">Vaše preference</string>
|
||||
<string name="timed_messages">Mizející zprávy</string>
|
||||
<string name="timed_messages">Mizící zprávy</string>
|
||||
<string name="feature_enabled_for_contact">povoleno pro kontakt</string>
|
||||
<string name="feature_received_prohibited">přijaté, zakázané</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Vy i váš kontakt můžete posílat mizející zprávy.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Vy i váš kontakt můžete posílat mizící zprávy.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Zmizelé zprávy může odesílat pouze váš kontakt.</string>
|
||||
<string name="only_you_can_delete_messages">Nevratně mazat zprávy můžete pouze vy (váš kontakt je může označit ke smazání).</string>
|
||||
<string name="message_deletion_prohibited">Nevratné mazání zpráv je v tomto chatu zakázáno.</string>
|
||||
<string name="prohibit_direct_messages">Zakázat odesílání přímých zpráv členům.</string>
|
||||
<string name="prohibit_direct_messages">Přímé zprávy členům zakázány.</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_hour">%d hodina</string>
|
||||
<string name="ttl_hour">%d hodinu</string>
|
||||
<string name="feature_offered_item_with_param">offered %s: %2s</string>
|
||||
<string name="v4_2_group_links">Odkazy na skupiny</string>
|
||||
<string name="v4_3_voice_messages">Hlasové zprávy</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Vaše kontakty mohou povolit úplné vymazání zpráv.</string>
|
||||
<string name="v4_4_disappearing_messages">Zmizení zpráv</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Vaše kontakty mohou povolit úplné mazání zpráv.</string>
|
||||
<string name="v4_4_disappearing_messages">Mizící zprávy</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Porovnejte bezpečnostní kódy se svými kontakty.</string>
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="connect_via_contact_link">Připojit se přes kontaktní odkaz\?</string>
|
||||
<string name="connect_via_invitation_link">Připojit se přes odkaz na pozvánku\?</string>
|
||||
<string name="connect_via_group_link">Připojit se přes odkaz skupiny\?</string>
|
||||
<string name="connect_via_contact_link">Připojit se odkazem\?</string>
|
||||
<string name="connect_via_invitation_link">Připojit se pozvánkou\?</string>
|
||||
<string name="connect_via_group_link">Připojit se odkazem skupiny\?</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Váš profil bude odeslán kontaktu, od kterého jste obdrželi tento odkaz.</string>
|
||||
<string name="server_connected">připojeno</string>
|
||||
<string name="server_error">chyba</string>
|
||||
@@ -97,19 +97,19 @@
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.</string>
|
||||
<string name="deleted_description">smazáno</string>
|
||||
<string name="invalid_chat">neplatný chat</string>
|
||||
<string name="invalid_data">neplatné údaje</string>
|
||||
<string name="invalid_data">neplatná data</string>
|
||||
<string name="connection_local_display_name">spojení <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">spojení navázáno</string>
|
||||
<string name="display_name_invited_to_connect">pozvánka k připojení</string>
|
||||
<string name="display_name_connecting">připojení…</string>
|
||||
<string name="description_you_shared_one_time_link">jste sdíleli jednorázové spojení</string>
|
||||
<string name="description_you_shared_one_time_link">sdíleli jste jednorázový odkaz</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">sdíleli jste jednorázový odkaz inkognito</string>
|
||||
<string name="description_via_group_link">prostřednictvím skupinového odkazu</string>
|
||||
<string name="description_via_contact_address_link">prostřednictvím odkazu na kontaktní adresu</string>
|
||||
<string name="description_via_contact_address_link_incognito">inkognito přes odkaz na kontaktní adresu</string>
|
||||
<string name="description_via_one_time_link">prostřednictvím jednorázového odkazu</string>
|
||||
<string name="description_via_one_time_link_incognito">inkognito přes jednorázový odkaz</string>
|
||||
<string name="simplex_link_contact">SimpleX kontaktní adresa</string>
|
||||
<string name="description_via_contact_address_link">prostřednictvím odkazu</string>
|
||||
<string name="description_via_contact_address_link_incognito">inkognito přes odkaz</string>
|
||||
<string name="description_via_one_time_link">jednorázovým odkazem</string>
|
||||
<string name="description_via_one_time_link_incognito">inkognito jednorázovým odkazem</string>
|
||||
<string name="simplex_link_contact">SimpleX adresa</string>
|
||||
<string name="simplex_link_invitation">Jednorázová pozvánka SimpleX</string>
|
||||
<string name="simplex_link_group">Skupinový odkaz SimpleX</string>
|
||||
<string name="simplex_link_connection">prostřednictvím <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
@@ -118,31 +118,31 @@
|
||||
<string name="simplex_link_mode_full">Úplný odkaz</string>
|
||||
<string name="simplex_link_mode_browser">Prostřednictvím prohlížeče</string>
|
||||
<string name="simplex_link_mode_browser_warning">Otevření odkazu v prohlížeči může snížit soukromí a bezpečnost připojení. Nedůvěryhodné odkazy SimpleX budou červené.</string>
|
||||
<string name="error_saving_smp_servers">Chyba při ukládání serverů SMP</string>
|
||||
<string name="error_setting_network_config">Chyba při aktualizaci konfigurace sítě</string>
|
||||
<string name="error_saving_smp_servers">Chyba ukládání serverů SMP</string>
|
||||
<string name="error_setting_network_config">Chyba změny konfigurace sítě</string>
|
||||
<string name="failed_to_parse_chat_title">Nepodařilo se načíst chat</string>
|
||||
<string name="failed_to_parse_chats_title">Nepodařilo se načíst chaty</string>
|
||||
<string name="contact_developers">Aktualizujte aplikaci a kontaktujte vývojáře.</string>
|
||||
<string name="connection_timeout">Časový limit připojení</string>
|
||||
<string name="connection_error">Chyba připojení</string>
|
||||
<string name="network_error_desc">Zkontrolujte prosím své síťové připojení pomocí <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> a zkuste to znovu.</string>
|
||||
<string name="error_sending_message">Chyba při odesílání zprávy</string>
|
||||
<string name="error_adding_members">Chyba při přidávání členů</string>
|
||||
<string name="error_sending_message">Chyba odesílání zprávy</string>
|
||||
<string name="error_adding_members">Chyba přidávání členů</string>
|
||||
<string name="contact_already_exists">Kontakt již existuje</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Jste již připojeni k <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
|
||||
<string name="invalid_connection_link">Neplatný odkaz na spojení</string>
|
||||
<string name="error_accepting_contact_request">Chyba příjmu požadavku od kontaktu</string>
|
||||
<string name="error_changing_address">Chyba změny adresy</string>
|
||||
<string name="settings_notifications_mode_title">Služba oznamování</string>
|
||||
<string name="settings_notifications_mode_title">Oznamovací služba</string>
|
||||
<string name="notifications_mode_service_desc">Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
|
||||
<string name="notification_preview_mode_message">Text zprávy</string>
|
||||
<string name="notification_preview_mode_contact">Jméno kontaktu</string>
|
||||
<string name="notification_preview_mode_hidden">Skryté</string>
|
||||
<string name="notification_preview_mode_message_desc">Zobrazit kontakt a zprávu</string>
|
||||
<string name="notification_contact_connected">Připojeno</string>
|
||||
<string name="notification_preview_mode_message_desc">Zobrazí kontakt a zprávu</string>
|
||||
<string name="notification_contact_connected">Připojen</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Zámek</string>
|
||||
<string name="auth_log_in_using_credential">Přihlaste se pomocí svého pověření</string>
|
||||
<string name="auth_enable_simplex_lock">Zapnutí zámku SimpleX</string>
|
||||
<string name="auth_log_in_using_credential">Přihlásit pomocí ověření</string>
|
||||
<string name="auth_enable_simplex_lock">Zapnout zámek SimpleX</string>
|
||||
<string name="reply_verb">Odpověď</string>
|
||||
<string name="share_verb">Sdílet</string>
|
||||
<string name="copy_verb">Kopírovat</string>
|
||||
@@ -159,11 +159,11 @@
|
||||
<string name="icon_descr_waiting_for_image">Čekání na obrázek</string>
|
||||
<string name="icon_descr_asked_to_receive">Požádáno o přijetí obrázku</string>
|
||||
<string name="icon_descr_image_snd_complete">Obrázek odeslán</string>
|
||||
<string name="waiting_for_image">Čekáme na obrázek</string>
|
||||
<string name="waiting_for_image">Čekám na obrázek</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">Obrázek bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="contact_sent_large_file">Váš kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="maximum_supported_file_size">V současné době je maximální podporovaná velikost souboru <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="error_saving_file">Chyba při ukládání souboru</string>
|
||||
<string name="error_saving_file">Chyba ukládání souboru</string>
|
||||
<string name="voice_message">Hlasová zpráva</string>
|
||||
<string name="voice_message_send_text">Hlasová zpráva…</string>
|
||||
<string name="icon_descr_server_status_connected">Připojeno</string>
|
||||
@@ -179,9 +179,9 @@
|
||||
<string name="reset_verb">Obnovit</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">bez podrobností</string>
|
||||
<string name="add_contact">Jednorázový zvací odkaz</string>
|
||||
<string name="copied">Zkopírováno do schránky</string>
|
||||
<string name="add_contact_or_create_group">Začít novou konverzaci</string>
|
||||
<string name="add_contact">Jednorázová pozvánka</string>
|
||||
<string name="copied">Zkopírováno</string>
|
||||
<string name="add_contact_or_create_group">Nová konverzace</string>
|
||||
<string name="create_group">Vytvořit tajnou skupinu</string>
|
||||
<string name="to_share_with_your_contact">(sdílet s kontaktem)</string>
|
||||
<string name="only_stored_on_members_devices">(uloženo pouze členy skupiny)</string>
|
||||
@@ -191,17 +191,17 @@
|
||||
<string name="choose_file">Vybrat soubor</string>
|
||||
<string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string>
|
||||
<string name="chat_help_tap_button">Klepněte na tlačítko</string>
|
||||
<string name="above_then_preposition_continuation">nad, potom:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Přidat nový kontakt</b>: pro vytvoření jednorázového QR kódu pro váš kontakt.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Skenovat QR kód</b>: připojení ke kontaktu, který vám ukáže QR kód.</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scan displayed QR code from the app, via <b>Scan QR code</b>.</string>
|
||||
<string name="above_then_preposition_continuation">potom:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Přidejte nový kontakt</b>: vytvořte jednorázý QR kód pro váš kontakt.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Naskenujte QR kód</b>: připojíte se ke kontaktu, který vám QR kód ukázal.</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 počítač: naskenujte QR kód z aplikace přez <b>Skenovat QR kód</b>.</string>
|
||||
<string name="clear_chat_question">Vyčistit chat\?</string>
|
||||
<string name="clear_verb">Vyčistit</string>
|
||||
<string name="mark_read">Označit jako přečteno</string>
|
||||
<string name="mark_unread">Označit jako nepřečteno</string>
|
||||
<string name="mute_chat">Ztlumit</string>
|
||||
<string name="unmute_chat">Zrušit ztlumení</string>
|
||||
<string name="you_invited_your_contact">Pozvali jste svůj kontakt</string>
|
||||
<string name="you_invited_your_contact">Pozvali jste kontakt</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Kontakt, se kterým jste tento odkaz sdíleli, se NEBUDE moci připojit!</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">Připojení, které jste přijali, bude zrušeno!</string>
|
||||
<string name="icon_descr_help">help</string>
|
||||
@@ -210,9 +210,9 @@
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Budete připojeni, jakmile bude vaše žádost o připojení přijata, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="connection_request_sent">Požadavek na připojení byl odeslán!</string>
|
||||
<string name="your_profile_will_be_sent">Váš profil v chatu bude odeslán vašemu kontaktu</string>
|
||||
<string name="create_one_time_link">Vytvořte jednorázový odkaz na pozvánku</string>
|
||||
<string name="one_time_link">Vytvořit jednorázový zvací odkaz</string>
|
||||
<string name="your_profile_will_be_sent">Váš chat profil bude odeslán vašemu kontaktu</string>
|
||||
<string name="create_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="one_time_link">Jednorázová pozvánka</string>
|
||||
<string name="security_code">Bezpečnostní kód</string>
|
||||
<string name="is_verified">%s je ověřeno</string>
|
||||
<string name="chat_console">Chat konzole</string>
|
||||
@@ -225,7 +225,7 @@
|
||||
<string name="smp_servers_invalid_address">Neplatná adresa serveru!</string>
|
||||
<string name="smp_servers_check_address">Zkontrolujte adresu serveru a zkuste to znovu.</string>
|
||||
<string name="smp_servers_delete_server">Smazat server</string>
|
||||
<string name="contribute">Přispějte</string>
|
||||
<string name="contribute">Přispět</string>
|
||||
<string name="how_to">Jak</string>
|
||||
<string name="how_to_use_your_servers">Jak používat servery</string>
|
||||
<string name="your_ICE_servers">Vaše servery ICE</string>
|
||||
@@ -246,18 +246,18 @@
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platforma pro zasílání zpráv a aplikace chránící vaše soukromí a bezpečnost.</string>
|
||||
<string name="create_profile">Vytvořit profil</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Profil je sdílen pouze s vašimi kontakty.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Zobrazované jméno nesmí obsahovat prázdné znaky.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Zobrazované jméno nesmí obsahovat mezery.</string>
|
||||
<string name="bold">tučně</string>
|
||||
<string name="callstatus_in_progress">probíhající hovor</string>
|
||||
<string name="decentralized">Decentralizovaná</string>
|
||||
<string name="how_it_works">Jak to funguje</string>
|
||||
<string name="how_simplex_works">Jak funguje <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odesílané pomocí <b>2 vrstvého end-to-end šifrování</b>.</string>
|
||||
<string name="onboarding_notifications_mode_title">Soukromá oznámení</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odesílané pomocí <b>2 vrstvého koncového šifrování</b>.</string>
|
||||
<string name="onboarding_notifications_mode_title">Oznámení</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Pravidelné</string>
|
||||
<string name="ignore">Ignorovat</string>
|
||||
<string name="call_already_ended">Hovor již skončil!</string>
|
||||
<string name="icon_descr_video_call">videohovor</string>
|
||||
<string name="icon_descr_video_call">video hovor</string>
|
||||
<string name="icon_descr_audio_call">audio hovor</string>
|
||||
<string name="settings_audio_video_calls">Audio a video hovory</string>
|
||||
<string name="call_on_lock_screen">Hovory na uzamčené obrazovce:</string>
|
||||
@@ -272,7 +272,7 @@
|
||||
<string name="settings_section_title_settings">NASTAVENÍ</string>
|
||||
<string name="settings_section_title_help">NÁPOVĚDA</string>
|
||||
<string name="settings_section_title_device">ZAŘÍZENÍ</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="settings_section_title_chats">CHATY</string>
|
||||
<string name="settings_experimental_features">Experimentální funkce</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_icon">IKONA APLIKACE</string>
|
||||
@@ -281,11 +281,11 @@
|
||||
<string name="settings_section_title_calls">VOLÁNÍ</string>
|
||||
<string name="export_database">Export databáze</string>
|
||||
<string name="import_database">Import databáze</string>
|
||||
<string name="delete_database">Smazání databáze</string>
|
||||
<string name="error_exporting_chat_database">Chyba při exportu chat databáze</string>
|
||||
<string name="delete_database">Smazat databázi</string>
|
||||
<string name="error_exporting_chat_database">Chyba exportu chat databáze</string>
|
||||
<string name="import_database_confirmation">Import</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Restartujte aplikaci, abyste mohli používat importovanou chat databázi.</string>
|
||||
<string name="delete_chat_profile_question">Smazat profil chatu\?</string>
|
||||
<string name="delete_chat_profile_question">Smazat chat profil\?</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Tuto akci nelze vzít zpět! Váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Restartujte aplikaci a vytvořte nový chat profil.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Nejnovější verzi chat databáze musíte používat POUZE v jednom zařízení, jinak se může stát, že přestanete přijímat zprávy od některých kontaktů.</string>
|
||||
@@ -293,24 +293,24 @@
|
||||
<string name="files_and_media_section">Soubory a média</string>
|
||||
<string name="delete_files_and_media_question">Smazat soubory a média\?</string>
|
||||
<string name="delete_messages">Odstranit zprávy</string>
|
||||
<string name="remove_passphrase_from_keychain">Odstranit přístupovou frázi z úložiště klíčů\?</string>
|
||||
<string name="remove_passphrase_from_keychain">Odstranit frázi z úložiště klíčů\?</string>
|
||||
<string name="notifications_will_be_hidden">Oznámení budou doručována pouze do doby, než se aplikace zastaví!</string>
|
||||
<string name="remove_passphrase">Odstranit</string>
|
||||
<string name="update_database">Aktualizovat</string>
|
||||
<string name="current_passphrase">Aktuální přístupová fráze…</string>
|
||||
<string name="update_database_passphrase">Aktualizovat přístupovou frázi databáze</string>
|
||||
<string name="current_passphrase">Aktuální fráze…</string>
|
||||
<string name="update_database_passphrase">Aktualizovat přístupovou frázi</string>
|
||||
<string name="enter_correct_current_passphrase">Zadejte prosím správnou aktuální přístupovou frázi.</string>
|
||||
<string name="database_is_not_encrypted">Váš chat databáze není šifrována - nastavte přístupovou frázi pro její ochranu.</string>
|
||||
<string name="keychain_is_storing_securely">K bezpečnému uložení heslové fráze slouží úložiště klíčů Android - umožňuje fungování služby oznámení.</string>
|
||||
<string name="database_is_not_encrypted">Chat databáze není šifrována - nastavte přístupovou frázi pro její ochranu.</string>
|
||||
<string name="keychain_is_storing_securely">K bezpečnému uložení přístupové fráze slouží úložiště klíčů Android - umožňuje fungování služby oznámení.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Upozornění</b>: pokud přístupovou frázi ztratíte, NEBUDE možné ji obnovit ani změnit.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">Databáze bude šifrována a přístupová fráze bude uložena v úložišti klíčů.</string>
|
||||
<string name="store_passphrase_securely">Heslo uložte bezpečně, v případě jeho ztráty jej NEBUDE možné změnit.</string>
|
||||
<string name="file_with_path">Soubor: %s</string>
|
||||
<string name="database_passphrase_is_required">Pro otevření chatu je vyžadována přístupová fráze databáze.</string>
|
||||
<string name="database_passphrase_is_required">Pro otevření chatu je vyžadována přístupová fráze.</string>
|
||||
<string name="unknown_error">Neznámá chyba</string>
|
||||
<string name="open_chat">Otevřete chat</string>
|
||||
<string name="restore_database">Obnovte zálohu databáze</string>
|
||||
<string name="restore_database_alert_desc">Po obnovení zálohy databáze zadejte předchozí heslo. Tuto akci nelze vrátit zpět.</string>
|
||||
<string name="restore_database_alert_desc">Po obnovení zálohy databáze zadejte předchozí frázi. Tuto akci nelze vrátit zpět.</string>
|
||||
<string name="chat_is_stopped_indication">Chat je zastaven</string>
|
||||
<string name="chat_archive_header">Chat se archivuje</string>
|
||||
<string name="delete_chat_archive_question">Smazat chat archiv\?</string>
|
||||
@@ -327,7 +327,7 @@
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">změnila se vaše adresa</string>
|
||||
<string name="icon_descr_expand_role">Rozšířit výběr rolí</string>
|
||||
<string name="invite_prohibited">Nelze pozvat kontakt!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Již máte profil chatu se stejným názvem. Zvolte prosím jiné jméno.</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Již máte chat profil se stejným názvem. Zvolte prosím jiné jméno.</string>
|
||||
<string name="smp_server_test_create_queue">Vytvořit frontu</string>
|
||||
<string name="smp_server_test_secure_queue">Zabezpečit frontu</string>
|
||||
<string name="service_notifications">Okamžitá oznámení!</string>
|
||||
@@ -360,7 +360,7 @@
|
||||
<string name="integrity_msg_bad_hash">špatný kontrolní součet zprávy</string>
|
||||
<string name="chat_database_imported">Chat databáze importována</string>
|
||||
<string name="new_passphrase">Nová přístupová fráze…</string>
|
||||
<string name="save_passphrase_and_open_chat">Uložte heslo a otevřete chat</string>
|
||||
<string name="save_passphrase_and_open_chat">Uložte frázi a otevřete chat</string>
|
||||
<string name="chat_archive_section">CHAT ARCHIV</string>
|
||||
<string name="no_contacts_selected">Nebyl vybrán žádný kontakt</string>
|
||||
<string name="invite_prohibited_description">Snažíte se pozvat kontakt se kterým jste sdíleli inkognito profil, do skupiny ve které používáte svůj hlavní profil</string>
|
||||
@@ -385,9 +385,9 @@
|
||||
<string name="clear_verification">Jasné ověření</string>
|
||||
<string name="to_verify_compare">Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</string>
|
||||
<string name="your_settings">Vaše nastavení</string>
|
||||
<string name="your_simplex_contact_address">Vaše kontaktní adresa <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="your_simplex_contact_address">Vaše adresa <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="database_passphrase_and_export">Heslo databáze a export</string>
|
||||
<string name="your_chat_profiles">Vaše chat profily</string>
|
||||
<string name="your_chat_profiles">Chat profily</string>
|
||||
<string name="chat_with_the_founder">Zaslat otázky a nápady</string>
|
||||
<string name="smp_servers_test_server">Test serveru</string>
|
||||
<string name="enter_one_ICE_server_per_line">Servery ICE (jeden na řádek)</string>
|
||||
@@ -406,14 +406,14 @@
|
||||
<string name="ntf_channel_messages">Zprávy SimpleX Chat</string>
|
||||
<string name="settings_notification_preview_mode_title">Zobrazení náhledu</string>
|
||||
<string name="settings_notification_preview_title">Náhled oznámení</string>
|
||||
<string name="notifications_mode_off">Spustí se při otevření aplikace</string>
|
||||
<string name="notifications_mode_off">Spuštěna při otevřené aplikaci</string>
|
||||
<string name="notifications_mode_periodic">Spouští se pravidelně</string>
|
||||
<string name="notifications_mode_service">Vždy zapnuto</string>
|
||||
<string name="notifications_mode_service">Vždy zapnuta</string>
|
||||
<string name="notifications_mode_periodic_desc">Každých 10 minut kontroluje nové zprávy, po dobu až 1 minuty</string>
|
||||
<string name="notification_preview_mode_contact_desc">Zobrazit pouze kontakt</string>
|
||||
<string name="notification_preview_mode_contact_desc">Zobrazí pouze kontakt</string>
|
||||
<string name="notification_preview_somebody">Skrytý kontakt:</string>
|
||||
<string name="la_notice_turn_on">Zapněte funkci</string>
|
||||
<string name="auth_simplex_lock_turned_on">Zapnutý zámek SimpleX Lock</string>
|
||||
<string name="auth_simplex_lock_turned_on">Zámek SimpleX zapnut</string>
|
||||
<string name="auth_unlock">Odemknout</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Ověřování zařízení není povoleno. Jakmile povolíte ověřování zařízení, můžete zámek SimpleX Lock zapnout prostřednictvím Nastavení.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Ověřování zařízení je zakázáno. Zámek SimpleX je vypnut.</string>
|
||||
@@ -439,7 +439,7 @@
|
||||
<string name="voice_messages_prohibited">Hlasové zprávy jsou zakázány!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv.</string>
|
||||
<string name="send_live_message_desc">Poslat živou zprávu - zpráva se bude aktualizovat pro příjemce během psaní.</string>
|
||||
<string name="share_one_time_link">Vytvořte jednorázový odkaz na pozvánku</string>
|
||||
<string name="share_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="scan_QR_code">Skenovat QR kód</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">( skenovat nebo vložit ze schránky)</string>
|
||||
<string name="edit_image">Upravit obrázek</string>
|
||||
@@ -453,7 +453,7 @@
|
||||
<string name="status_contact_has_e2e_encryption">kontakt má šifrování e2e</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">kontakt nemá šifrování e2e</string>
|
||||
<string name="call_connection_peer_to_peer">peer-to-peer</string>
|
||||
<string name="call_connection_via_relay">přes relay</string>
|
||||
<string name="call_connection_via_relay">přes relé</string>
|
||||
<string name="icon_descr_hang_up">Zavěsit</string>
|
||||
<string name="icon_descr_video_off">Video vypnuto</string>
|
||||
<string name="icon_descr_video_on">Video zapnuto</string>
|
||||
@@ -463,14 +463,14 @@
|
||||
<string name="alert_title_skipped_messages">Přeskočené zprávy</string>
|
||||
<string name="privacy_and_security">Ochrana osobních údajů a zabezpečení</string>
|
||||
<string name="your_privacy">Vaše soukromí</string>
|
||||
<string name="protect_app_screen">Ochrana obrazovky aplikace</string>
|
||||
<string name="send_link_previews">Odesílání náhledů odkazů</string>
|
||||
<string name="full_backup">Zálohování dat aplikace</string>
|
||||
<string name="confirm_new_passphrase">Potvrdit novou heslovou frázi…</string>
|
||||
<string name="protect_app_screen">Skrývat aplikaci</string>
|
||||
<string name="send_link_previews">Odesílat náhledy odkazů</string>
|
||||
<string name="full_backup">Zálohovat data aplikace</string>
|
||||
<string name="confirm_new_passphrase">Potvrdit frázi…</string>
|
||||
<string name="error_with_info">Chyba: %s</string>
|
||||
<string name="leave_group_question">Opustit skupinu\?</string>
|
||||
<string name="icon_descr_group_inactive">Skupina je neaktivní</string>
|
||||
<string name="rcv_group_event_member_left">vlevo</string>
|
||||
<string name="rcv_group_event_member_left">odešel</string>
|
||||
<string name="clear_contacts_selection_button">Vyčistit</string>
|
||||
<string name="switch_verb">Přepnout</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Role bude změněna na \"%s\". Všichni ve skupině budou informováni.</string>
|
||||
@@ -479,7 +479,7 @@
|
||||
<string name="network_option_seconds_label">vteřin</string>
|
||||
<string name="incognito_info_allows">Umožňuje mít v jednom chat profilu mnoho anonymních spojení bez sdílení údajů mezi nimi.</string>
|
||||
<string name="incognito_info_share">Pokud s někým sdílíte inkognito profil, bude použit pro skupiny, do kterých vás pozve.</string>
|
||||
<string name="theme_system">Systém</string>
|
||||
<string name="theme_system">Systémové</string>
|
||||
<string name="voice_messages">Hlasové zprávy</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Vy i váš kontakt můžete nevratně mazat odeslané zprávy.</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
@@ -518,10 +518,10 @@
|
||||
<string name="live">ŽIVĚ</string>
|
||||
<string name="description_via_group_link_incognito">inkognito přes skupinový odkaz</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů SMP jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
|
||||
<string name="failed_to_create_user_title">Chyba při vytváření profilu!</string>
|
||||
<string name="failed_to_create_user_title">Chyba vytváření profilu!</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Duplicitní zobrazované jméno!</string>
|
||||
<string name="failed_to_active_user_title">Chyba při přepínání profilu!</string>
|
||||
<string name="error_joining_group">Chyba při připojování ke skupině</string>
|
||||
<string name="failed_to_active_user_title">Chyba přepínání profilu!</string>
|
||||
<string name="error_joining_group">Chyba připojování ke skupině</string>
|
||||
<string name="cannot_receive_file">Nelze přijmout soubor</string>
|
||||
<string name="sender_cancelled_file_transfer">Odesílatel zrušil přenos souboru.</string>
|
||||
<string name="error_receiving_file">Chyba příjmu souboru</string>
|
||||
@@ -544,12 +544,12 @@
|
||||
<string name="periodic_notifications_disabled">Pravidelná oznámení jsou vypnuta!</string>
|
||||
<string name="database_initialization_error_desc">Databáze nefunguje správně. Klepnutím se dozvíte více</string>
|
||||
<string name="notifications_mode_off_desc">Aplikace může přijímat oznámení pouze při svém běhu, žádná služba na pozadí nebude spuštěna</string>
|
||||
<string name="notification_display_mode_hidden_desc">Skrýt kontakt a zprávu</string>
|
||||
<string name="notification_display_mode_hidden_desc">Skryje kontakt i zprávu</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">Chcete-li chránit své informace, zapněte zámek SimpleX Lock.
|
||||
\nPřed zapnutím této funkce budete vyzváni k dokončení ověření.</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Při spuštění nebo obnovení aplikace po 30 sekundách na pozadí budete vyzváni k ověření.</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Při spuštění, nebo obnovení aplikace po 30 sekundách, budete vyzváni k odemčení.</string>
|
||||
<string name="auth_disable_simplex_lock">Vypnout zámek SimpleX</string>
|
||||
<string name="auth_confirm_credential">Potvrďte své pověření</string>
|
||||
<string name="auth_confirm_credential">Potvrďte vzor</string>
|
||||
<string name="auth_unavailable">Ověřování není k dispozici</string>
|
||||
<string name="auth_stop_chat">Zastavit chat</string>
|
||||
<string name="auth_open_chat_console">Otevřete chat konzoli</string>
|
||||
@@ -565,7 +565,7 @@
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">neautorizované odeslání</string>
|
||||
<string name="group_preview_you_are_invited">jste pozváni do skupiny</string>
|
||||
<string name="group_preview_join_as">připojit jako %s</string>
|
||||
<string name="group_connection_pending">připojuje se…</string>
|
||||
<string name="group_connection_pending">připojuji…</string>
|
||||
<string name="tap_to_start_new_chat">Začněte nový chat</string>
|
||||
<string name="chat_with_developers">Chat s vývojáři</string>
|
||||
<string name="you_have_no_chats">Nemáte žádné konverzace</string>
|
||||
@@ -585,25 +585,25 @@
|
||||
<string name="live_message">Živá zpráva!</string>
|
||||
<string name="connect_via_link_or_qr">Připojit se prostřednictvím odkazu / QR kódu</string>
|
||||
<string name="thank_you_for_installing_simplex">Děkujeme za instalaci <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Můžete se <font color="#0088ff">připojit k <xliff:g id="appNameFull">SimpleX Chat</xliff:g> vývojářům a položit jim případné dotazy a získat aktualizace</font>.</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Můžete se <font color="#0088ff">připojit k <xliff:g id="appNameFull">SimpleX Chat</xliff:g> vývojářům, položit jim případné dotazy a získat aktualizace</font>.</string>
|
||||
<string name="to_connect_via_link_title">Připojení prostřednictvím odkazu</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Pokud jste dostali <xliff:g id="appName">SimpleX Chat</xliff:g> zvací odkaz, Můžete ho otevřít v prohlížeči:</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Pokud jste dostali <xliff:g id="appName">SimpleX Chat</xliff:g> pozvánku, můžete ji otevřít v prohlížeči:</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Pokud zvolíte odmítnutí, odesílatel NEBUDE upozorněn.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobilní telefon: tap <b>Otevřete v mobilní aplikaci</b>, potom klikněte <b>Připojit</b>.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 telefon: <b>Otevřete v mobilní aplikaci</b>, potom klikněte na <b>Připojit</b>.</string>
|
||||
<string name="reject_contact_button">Odmítnout</string>
|
||||
<string name="clear_chat_button">Smazat chat</string>
|
||||
<string name="clear_chat_menu_action">Vyčistit</string>
|
||||
<string name="delete_contact_menu_action">Smazat</string>
|
||||
<string name="set_contact_name">Nastavit jméno kontaktu</string>
|
||||
<string name="you_accepted_connection">Přijali jste spojení</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Aby se připojení dokončilo, musí být váš kontakt online.
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">K dokončení připojení, musí být váš kontakt online.
|
||||
\nToto připojení můžete zrušit a kontakt odebrat (a zkusit to později s novým odkazem).</string>
|
||||
<string name="contact_wants_to_connect_with_you">Chce se s vámi spojit!</string>
|
||||
<string name="icon_descr_profile_image_placeholder">Zástupný symbol profilového obrázku</string>
|
||||
<string name="image_descr_profile_image">profilový obrázek</string>
|
||||
<string name="icon_descr_close_button">Tlačítko Zavřít</string>
|
||||
<string name="alert_title_contact_connection_pending">Kontakt ještě není připojen!</string>
|
||||
<string name="image_descr_link_preview">náhledový obrázek odkazu</string>
|
||||
<string name="image_descr_link_preview">náhled odkazu</string>
|
||||
<string name="icon_descr_cancel_link_preview">Zrušit náhled odkazu</string>
|
||||
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> Logo</string>
|
||||
<string name="icon_descr_email">E-mail</string>
|
||||
@@ -634,7 +634,7 @@
|
||||
<string name="use_simplex_chat_servers__question">Použít <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servery\?</string>
|
||||
<string name="using_simplex_chat_servers">Použití <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serverů.</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Uložené servery WebRTC ICE budou odstraněny.</string>
|
||||
<string name="error_saving_ICE_servers">Chyba při ukládání serverů ICE</string>
|
||||
<string name="error_saving_ICE_servers">Chyba ukládání serverů ICE</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
|
||||
<string name="save_servers_button">Uložit</string>
|
||||
<string name="network_and_servers">Síť a servery</string>
|
||||
@@ -655,7 +655,7 @@
|
||||
<string name="app_version_title">Verze aplikace</string>
|
||||
<string name="app_version_name">Verze aplikace: v%s</string>
|
||||
<string name="core_version">Verze jádra: v%s</string>
|
||||
<string name="core_build_timestamp">Jádro sestaveno na: %s</string>
|
||||
<string name="core_build_timestamp">Jádro sestaveno: %s</string>
|
||||
<string name="delete_address__question">Smazat adresu\?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string>
|
||||
<string name="contact_requests">Žádosti o kontakt</string>
|
||||
@@ -709,18 +709,20 @@
|
||||
<string name="audio_call_no_encryption">zvukový hovor (nešifrováno e2e)</string>
|
||||
<string name="reject">Odmítnout</string>
|
||||
<string name="your_calls">Vaše hovory</string>
|
||||
<string name="connect_calls_via_relay">Spojení přes relay</string>
|
||||
<string name="always_use_relay">Spojení přes relé</string>
|
||||
<string name="show_call_on_lock_screen">Zobrazit</string>
|
||||
<string name="no_call_on_lock_screen">Zakázat</string>
|
||||
<string name="your_ice_servers">Vaše servery ICE</string>
|
||||
<string name="webrtc_ice_servers">WebRTC servery ICE</string>
|
||||
<string name="relay_server_protects_ip">Přenosový server chrání vaši IP adresu, ale může sledovat dobu trvání hovoru.</string>
|
||||
<string name="relay_server_if_necessary">Přenosový server se používá pouze v případě potřeby. Jiná strana může sledovat vaši IP adresu.</string>
|
||||
<string name="status_no_e2e_encryption">bez šifrování e2e</string>
|
||||
<string name="icon_descr_flip_camera">Otočit foťák</string>
|
||||
<string name="icon_descr_flip_camera">Druhý foťák</string>
|
||||
<string name="icon_descr_call_pending_sent">Čekající hovor</string>
|
||||
<string name="icon_descr_call_missed">Zmeškaný hovor</string>
|
||||
<string name="icon_descr_call_rejected">Odmítnutý hovor</string>
|
||||
<string name="icon_descr_call_connecting">Spojování hovoru</string>
|
||||
<string name="icon_descr_call_ended">Ukončený hovor</string>
|
||||
<string name="icon_descr_call_ended">Skončený hovor</string>
|
||||
<string name="answer_call">Přijmout hovor</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> přeskočená zpráva (zprávy)</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Může se to stát, když:
|
||||
@@ -731,31 +733,31 @@
|
||||
\nBudeme přidávat redundantní servery, abychom zabránili ztrátě zpráv.</string>
|
||||
<string name="settings_section_title_you">VY</string>
|
||||
<string name="settings_section_title_support">PODPOŘIT SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_develop">ROZVÍJET</string>
|
||||
<string name="settings_section_title_develop">VÝVOJ</string>
|
||||
<string name="settings_developer_tools">Nástroje pro vývojáře</string>
|
||||
<string name="settings_section_title_incognito">Inkognito mód</string>
|
||||
<string name="your_chat_database">Vaše chat databáze</string>
|
||||
<string name="settings_section_title_incognito">inkognito mód</string>
|
||||
<string name="your_chat_database">Chat databáze</string>
|
||||
<string name="run_chat_section">SPUSTIT CHAT</string>
|
||||
<string name="chat_is_running">Chat je spuštěn</string>
|
||||
<string name="chat_is_stopped">Chat je zastaven</string>
|
||||
<string name="chat_database_section">CHAT DATABÁZE</string>
|
||||
<string name="database_passphrase">Heslo databáze</string>
|
||||
<string name="database_passphrase">přístupová fráze databáze</string>
|
||||
<string name="new_database_archive">Archiv nové databáze</string>
|
||||
<string name="old_database_archive">Archiv staré databáze</string>
|
||||
<string name="error_starting_chat">Chyba při spuštění chatu</string>
|
||||
<string name="error_starting_chat">Chyba spouštění chatu</string>
|
||||
<string name="stop_chat_question">Zastavit chat\?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Zastavte chat pro export, import nebo smazání chat databáze. Během zastavení, nebudete moci přijímat ani odesílat zprávy.</string>
|
||||
<string name="stop_chat_confirmation">Zastavit</string>
|
||||
<string name="set_password_to_export">Nastavení přístupové fráze pro export</string>
|
||||
<string name="set_password_to_export">Nastavte přístupovou frázi pro export</string>
|
||||
<string name="set_password_to_export_desc">Databáze je šifrována pomocí náhodné přístupové fráze. Před exportem ji změňte.</string>
|
||||
<string name="error_stopping_chat">Chyba při zastavení chatu</string>
|
||||
<string name="error_stopping_chat">Chyba zastavování chatu</string>
|
||||
<string name="import_database_question">Importovat chat databázi\?</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Vaše aktuální chat databáze bude SMAZÁNA a NAHRAZENA importovanou databází.
|
||||
\nTuto akci nelze vzít zpět - váš profily, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
|
||||
\nTuto akci nelze vzít zpět - vaše profily, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
|
||||
<string name="error_deleting_database">Chyba mazání chat databáze</string>
|
||||
<string name="error_importing_database">Chyba importu chat databáze</string>
|
||||
<string name="chat_database_deleted">Chat databáze odstraněna</string>
|
||||
<string name="delete_files_and_media_for_all_users">Odstranění souborů pro všechny chat profily</string>
|
||||
<string name="delete_files_and_media_for_all_users">Odstranit soubory všech chat profilů</string>
|
||||
<string name="delete_files_and_media_all">Odstranit všechny soubory</string>
|
||||
<string name="delete_files_and_media_desc">Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány.</string>
|
||||
<string name="no_received_app_files">Žádné přijaté ani odeslané soubory</string>
|
||||
@@ -768,11 +770,11 @@
|
||||
<string name="enable_automatic_deletion_question">Povolit automatické mazání zpráv\?</string>
|
||||
<string name="enable_automatic_deletion_message">Tuto akci nelze vzít zpět - zprávy odeslané a přijaté dříve, než bylo zvoleno, budou smazány. Může to trvat několik minut.</string>
|
||||
<string name="error_changing_message_deletion">Chyba změny nastavení</string>
|
||||
<string name="save_passphrase_in_keychain">Uložit přístupovou frázi do úložiště klíčů</string>
|
||||
<string name="save_passphrase_in_keychain">Uložit frázi do úložiště klíčů</string>
|
||||
<string name="database_encrypted">Databáze šifrována!</string>
|
||||
<string name="error_encrypting_database">Chyba šifrování databáze</string>
|
||||
<string name="encrypt_database">Šifrovat</string>
|
||||
<string name="encrypted_with_random_passphrase">Databáze je šifrována pomocí náhodné přístupové fráze, můžete ji změnit.</string>
|
||||
<string name="encrypted_with_random_passphrase">Databáze je šifrována pomocí náhodné přístupové fráze, musíte ji změnit.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">K bezpečnému uložení přístupové fráze se použije úložiště klíčů Android, po restartování aplikace nebo změně přístupové fráze - umožní přijímání oznámení.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení.</string>
|
||||
<string name="encrypt_database_question">Šifrovat databázi\?</string>
|
||||
@@ -786,7 +788,7 @@
|
||||
<string name="database_error">Chyba databáze</string>
|
||||
<string name="keychain_error">Chyba klíčenky</string>
|
||||
<string name="passphrase_is_different">Přístupová fráze databáze se liší od té uložené v klíčence.</string>
|
||||
<string name="cannot_access_keychain">Nelze získat přístup k úložišti klíčů pro uložení přístupová fráze k databázi.</string>
|
||||
<string name="cannot_access_keychain">Nelze získat přístup ke klíčence pro uložení přístupová fráze k databázi.</string>
|
||||
<string name="unknown_database_error_with_info">Neznámá chyba databáze: %s</string>
|
||||
<string name="wrong_passphrase_title">Špatná přístupová fráze!</string>
|
||||
<string name="enter_correct_passphrase">Zadejte správnou přístupovou frázi.</string>
|
||||
@@ -795,13 +797,13 @@
|
||||
<string name="restore_database_alert_title">Obnovit zálohu databáze\?</string>
|
||||
<string name="restore_database_alert_confirm">Obnovit</string>
|
||||
<string name="database_restore_error">Chyba obnovení databáze</string>
|
||||
<string name="restore_passphrase_not_found_desc">Heslo nebylo nalezeno v úložišti klíčů, zadejte jej prosím ručně. K této situaci mohlo dojít, pokud jste obnovili data aplikace pomocí zálohovacího nástroje. Pokud tomu tak není, obraťte se na vývojáře.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Chat můžete spustit přes Nastavení aplikace / Databáze nebo restartováním aplikace.</string>
|
||||
<string name="restore_passphrase_not_found_desc">Heslo nebylo v klíčence nalezeno, zadejte jej prosím ručně. K této situaci mohlo dojít, pokud jste obnovili data aplikace pomocí zálohovacího nástroje. Pokud tomu tak není, obraťte se na vývojáře.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Chat můžete spustit v Nastavení / Databáze nebo restartováním aplikace.</string>
|
||||
<string name="save_archive">Uložit archiv</string>
|
||||
<string name="delete_archive">Smazat archiv</string>
|
||||
<string name="group_invitation_item_description">pozvánka do skupiny <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="archive_created_on_ts">Vytvořeno dne <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Jste zváni do skupiny. Připojte se a spojte se s členy skupiny.</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Jste zváni do skupiny. Připojte se k členům skupiny.</string>
|
||||
<string name="join_group_incognito_button">Připojit se inkognito</string>
|
||||
<string name="joining_group">Připojit ke skupině</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Připojili jste se k této skupině. Připojení k pozvání člena skupiny.</string>
|
||||
@@ -835,15 +837,15 @@
|
||||
<string name="group_member_role_member">člen</string>
|
||||
<string name="group_member_role_owner">vlastník</string>
|
||||
<string name="group_member_status_removed">odstraněn</string>
|
||||
<string name="group_member_status_left">vlevo</string>
|
||||
<string name="group_member_status_left">odešel</string>
|
||||
<string name="group_member_status_group_deleted">skupina smazána</string>
|
||||
<string name="group_member_status_invited">pozvánka</string>
|
||||
<string name="group_member_status_introduced">připojující (zavedený)</string>
|
||||
<string name="group_member_status_invited">pozván</string>
|
||||
<string name="group_member_status_introduced">představen (zaveden)</string>
|
||||
<string name="group_member_status_intro_invitation">připojení (pozvánka na představení)</string>
|
||||
<string name="group_member_status_accepted">připojení (přijato)</string>
|
||||
<string name="group_member_status_announced">připojení (oznámeno)</string>
|
||||
<string name="group_member_status_accepted">připojen (přijat)</string>
|
||||
<string name="group_member_status_announced">připojen (oznámen)</string>
|
||||
<string name="group_member_status_connected">připojen</string>
|
||||
<string name="group_member_status_complete">kompletní</string>
|
||||
<string name="group_member_status_complete">komplet</string>
|
||||
<string name="group_member_status_creator">tvůrce</string>
|
||||
<string name="group_member_status_connecting">připojování</string>
|
||||
<string name="no_contacts_to_add">Žádné kontakty k přidání</string>
|
||||
@@ -859,11 +861,11 @@
|
||||
<string name="button_delete_group">Smazat skupinu</string>
|
||||
<string name="delete_group_question">Smazat skupinu\?</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Skupina bude smazána pro všechny členy - nelze to vzít zpět!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Skupina bude smazána pro vás - toto nelze vzít zpět!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Skupina bude smazána pouze pro vás - toto nelze vzít zpět!</string>
|
||||
<string name="button_leave_group">Opustit skupinu</string>
|
||||
<string name="button_edit_group_profile">Upravit profil skupiny</string>
|
||||
<string name="group_link">Odkaz na skupinu</string>
|
||||
<string name="create_group_link">Vytvořit odkaz na skupinu</string>
|
||||
<string name="group_link">Odkaz skupiny</string>
|
||||
<string name="create_group_link">Vytvořit odkaz skupiny</string>
|
||||
<string name="delete_link">Smazat odkaz</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Můžete sdílet odkaz nebo QR kód - ke skupině se bude moci připojit kdokoli. O členy skupiny nepřijdete, pokud ji později odstraníte.</string>
|
||||
<string name="error_creating_link_for_group">Chyba vytváření odkazu skupiny</string>
|
||||
@@ -899,10 +901,10 @@
|
||||
<string name="users_delete_with_connections">Profil a připojení k serveru</string>
|
||||
<string name="users_delete_data_only">Pouze místní data profilu</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Náhodný profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz.</string>
|
||||
<string name="incognito_info_protects">Režim inkognito chrání soukromí vašeho hlavního profilového jména a obrázku - pro každý nový kontakt je vytvořen nový náhodný profil.</string>
|
||||
<string name="incognito_info_find">Chcete-li najít profil použitý pro inkognito připojení, klepněte na název kontaktu nebo skupiny v horní části chatu.</string>
|
||||
<string name="theme_light">Světlý</string>
|
||||
<string name="theme_dark">Tmavý</string>
|
||||
<string name="incognito_info_protects">Režim inkognito chrání soukromí vašeho hlavního profilu, jména a obrázku - pro každý nový kontakt je vytvořen nový náhodný profil.</string>
|
||||
<string name="incognito_info_find">Chcete-li najít profil použitý pro inkognito připojení, klepněte na název kontaktu nebo skupiny v horní části.</string>
|
||||
<string name="theme_light">Světlé</string>
|
||||
<string name="theme_dark">Tmavé</string>
|
||||
<string name="theme">Téma</string>
|
||||
<string name="chat_preferences_contact_allows">Kontakt povolil</string>
|
||||
<string name="chat_preferences_on">zapnuto</string>
|
||||
@@ -911,32 +913,32 @@
|
||||
<string name="contact_preferences">Předvolby kontaktu</string>
|
||||
<string name="group_preferences">Předvolby skupiny</string>
|
||||
<string name="direct_messages">Přímé zprávy</string>
|
||||
<string name="full_deletion">Smazat pro všechny</string>
|
||||
<string name="feature_enabled">povoleno</string>
|
||||
<string name="feature_enabled_for_you">povoleno pro vás</string>
|
||||
<string name="feature_off">vypnuto</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Zakázat zasílání mizejících zpráv.</string>
|
||||
<string name="full_deletion">Mazání všem</string>
|
||||
<string name="feature_enabled">zapnuty</string>
|
||||
<string name="feature_enabled_for_you">povoleno vám</string>
|
||||
<string name="feature_off">vypnuty</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Mizící zprávy zakázány.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Kontakty mohou označit zprávy ke smazání; vy je budete moci zobrazit.</string>
|
||||
<string name="prohibit_sending_voice_messages">Zakázat odesílání hlasových zpráv.</string>
|
||||
<string name="only_you_can_send_disappearing">Mizející zprávy můžete odesílat pouze vy.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Mizející zprávy jsou v tomto chatu zakázány.</string>
|
||||
<string name="prohibit_sending_voice_messages">Hlasové zprávy zakázány.</string>
|
||||
<string name="only_you_can_send_disappearing">Pouze vy můžete odesílat mizící zprávy.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Mizící zprávy jsou v tomto chatu zakázány.</string>
|
||||
<string name="only_your_contact_can_delete">Nevratně mazat zprávy může pouze váš kontakt (vy je můžete označit ke smazání).</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Hlasové zprávy můžete posílat vy i váš kontakt.</string>
|
||||
<string name="only_you_can_send_voice">Hlasové zprávy můžete posílat pouze vy.</string>
|
||||
<string name="only_your_contact_can_send_voice">Hlasové zprávy může odesílat pouze váš kontakt.</string>
|
||||
<string name="voice_prohibited_in_this_chat">Hlasové zprávy jsou v tomto chatu zakázány.</string>
|
||||
<string name="prohibit_sending_disappearing">Zakázat posílání mizejících zpráv.</string>
|
||||
<string name="prohibit_message_deletion">Zakázat nevratné mazání zpráv.</string>
|
||||
<string name="prohibit_sending_voice">Zakázat odesílání hlasových zpráv.</string>
|
||||
<string name="group_members_can_send_disappearing">Členové skupiny mohou posílat mizející zprávy.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Mizející zprávy jsou v této skupině zakázány.</string>
|
||||
<string name="prohibit_sending_disappearing">Posílání mizících zpráv zakázáno.</string>
|
||||
<string name="prohibit_message_deletion">Nevratné mazání odeslaných zpráv zakázáno.</string>
|
||||
<string name="prohibit_sending_voice">Hlasové zprávy zakázány.</string>
|
||||
<string name="group_members_can_send_disappearing">Členové skupiny mohou posílat mizící zprávy.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Mizící zprávy jsou v této skupině zakázány.</string>
|
||||
<string name="group_members_can_send_dms">Členové skupiny mohou posílat přímé zprávy.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Přímé zprávy mezi členy jsou v této skupině zakázány.</string>
|
||||
<string name="group_members_can_delete">Členové skupiny mohou nevratně mazat odeslané zprávy.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">Nevratné mazání zpráv je v této skupině zakázáno.</string>
|
||||
<string name="group_members_can_send_voice">Členové skupiny mohou posílat hlasové zprávy.</string>
|
||||
<string name="voice_messages_are_prohibited">Hlasové zprávy jsou v této skupině zakázány.</string>
|
||||
<string name="delete_after">Smazat po</string>
|
||||
<string name="delete_after">Smazat za</string>
|
||||
<string name="ttl_month">%d měsíc</string>
|
||||
<string name="ttl_months">%d měsíců</string>
|
||||
<string name="ttl_day">%d den</string>
|
||||
@@ -947,9 +949,9 @@
|
||||
<string name="feature_offered_item">nabízeno %s</string>
|
||||
<string name="feature_cancelled_item">zrušeno %s</string>
|
||||
<string name="whats_new">Co je nového</string>
|
||||
<string name="new_in_version">Novinky v %s</string>
|
||||
<string name="new_in_version">Novinky %s</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Automatické přijímání žádostí o kontakt</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">S volitelnou uvítací zprávou.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Volitelná uvítací zpráva.</string>
|
||||
<string name="v4_3_voice_messages_desc">Max. 40 sekund, přijímá se okamžitě.</string>
|
||||
<string name="v4_5_private_filenames">Soukromé názvy souborů</string>
|
||||
<string name="v4_5_private_filenames_descr">Pro ochranu časového pásma, obrazové/hlasové soubory používají UTC.</string>
|
||||
@@ -957,14 +959,63 @@
|
||||
<string name="v4_5_reduced_battery_usage_descr">Další vylepšení se chystají již brzy!</string>
|
||||
<string name="v4_5_italian_interface">Italské rozhraní</string>
|
||||
<string name="v4_5_italian_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Budete připojeni, až bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="your_contact_address">Vaše kontaktní adresa</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Budete připojeni, jakmile bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="your_contact_address">Vaše adresa</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Váš chat profil bude odeslán
|
||||
\nvašemu kontaktu</string>
|
||||
<string name="your_chat_profiles_stored_locally">Vaše chat profily jsou uloženy lokálně, pouze ve vašem zařízení.</string>
|
||||
<string name="your_chats">Vaše konverzace</string>
|
||||
<string name="paste_connection_link_below_to_connect">Do níže uvedeného pole vložte odkaz, který jste obdrželi pro spojení s kontaktem.</string>
|
||||
<string name="share_invitation_link">Sdílet zvací odkaz</string>
|
||||
<string name="status_e2e_encrypted">end-to-end šifrované</string>
|
||||
<string name="share_invitation_link">Sdílet pozvánku</string>
|
||||
<string name="status_e2e_encrypted">koncově šifrované</string>
|
||||
<string name="moderated_description">moderované</string>
|
||||
<string name="moderated_item_description">moderovaný %s</string>
|
||||
<string name="delete_member_message__question">Smazat zprávu člena\?</string>
|
||||
<string name="moderate_verb">moderovaný</string>
|
||||
<string name="observer_cant_send_message_desc">Kontaktujte prosím správce skupiny.</string>
|
||||
<string name="you_are_observer">jste pozorovatel</string>
|
||||
<string name="group_member_role_observer">pozorovatel</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">Zpráva bude smazána pro všechny členy.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Zpráva bude pro všechny členy označena jako moderovaná.</string>
|
||||
<string name="observer_cant_send_message_title">Nemůžete posílat zprávy!</string>
|
||||
<string name="error_updating_link_for_group">Chyba aktualizace odkazu skupiny</string>
|
||||
<string name="initial_member_role">Počáteční role</string>
|
||||
<string name="language_system">Systém</string>
|
||||
<string name="smp_save_servers_question">Uložit servery\?</string>
|
||||
<string name="dont_show_again">Znovu neukazuj</string>
|
||||
<string name="cant_delete_user_profile">Nemohu smazat uživatelský profil!</string>
|
||||
<string name="button_add_welcome_message">Přidat uvítací zprávu</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Čínské a Španělské rozhranní</string>
|
||||
<string name="v4_6_audio_video_calls">Hlasové a video hovory</string>
|
||||
<string name="confirm_password">Potvrdit heslo</string>
|
||||
<string name="enter_password_to_show">Pro zobrazení zadejte heslo výše!</string>
|
||||
<string name="v4_6_reduced_battery_usage">Další snížení spotřeby baterie</string>
|
||||
<string name="error_saving_user_password">Chyba ukládání hesla uživatele</string>
|
||||
<string name="error_updating_user_privacy">Chyba aktualizace soukromí uživatele</string>
|
||||
<string name="v4_6_group_moderation">Správa skupin</string>
|
||||
<string name="v4_6_group_welcome_message">Uvítací zpráva skupin</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Skryté chat profily</string>
|
||||
<string name="hidden_profile_password">Hesla skrytých profilů</string>
|
||||
<string name="user_hide">Skrýt</string>
|
||||
<string name="hide_profile">Skrýt profil</string>
|
||||
<string name="make_profile_private">Změnit profil na soukromý!</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Další vylepšení již brzy!</string>
|
||||
<string name="v4_6_group_moderation_descr">Nyní mohou správci:
|
||||
\n- mazat zprávy členů.
|
||||
\n- zakázat členy (role \"pozorovatel\")</string>
|
||||
<string name="save_profile_password">Uložit heslo profilu</string>
|
||||
<string name="user_mute">Ztlumit</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Chraňte své chat profily heslem!</string>
|
||||
<string name="save_and_update_group_profile">Uložit a aktualizovat profil skupiny</string>
|
||||
<string name="muted_when_inactive">Ztlumit při neaktivitě!</string>
|
||||
<string name="password_to_show">Heslo k zobrazení</string>
|
||||
<string name="save_welcome_message_question">Uložit uvítací zprávu\?</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Nastavte zprávu zobrazenou novým členům!</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Podpora bluetooth a další vylepšení.</string>
|
||||
<string name="tap_to_activate_profile">Klepnutím aktivujete profil.</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
|
||||
<string name="should_be_at_least_one_profile">Měl by tam být alespoň jeden uživatelský profil.</string>
|
||||
<string name="button_welcome_message">Uvítací zpráva</string>
|
||||
<string name="group_welcome_title">Uvítací zpráva</string>
|
||||
<string name="user_unmute">Zrušit ztlumení</string>
|
||||
</resources>
|
||||
@@ -513,13 +513,15 @@
|
||||
<!-- Call settings -->
|
||||
<string name="settings_audio_video_calls">Audio- & Videoanrufe</string>
|
||||
<string name="your_calls">Ihre Anrufe</string>
|
||||
<string name="connect_calls_via_relay">Über ein Relais verbinden</string>
|
||||
<string name="always_use_relay">Über ein Relais verbinden</string>
|
||||
<string name="call_on_lock_screen">Anrufe auf Sperrbildschirm:</string>
|
||||
<string name="accept_call_on_lock_screen">Akzeptieren</string>
|
||||
<string name="show_call_on_lock_screen">Anzeigen</string>
|
||||
<string name="no_call_on_lock_screen">Deaktivieren</string>
|
||||
<string name="your_ice_servers">Ihre ICE-Server</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE-Server</string>
|
||||
<string name="relay_server_protects_ip">Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen.</string>
|
||||
<string name="relay_server_if_necessary">Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden.</string>
|
||||
<!-- Call Lock Screen -->
|
||||
<string name="open_simplex_chat_to_accept_call">Öffnen Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g>, um den Anruf anzunehmen.</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Aktivieren Sie Anrufe vom Sperrbildschirm über die Einstellungen.</string>
|
||||
@@ -1041,4 +1043,58 @@
|
||||
<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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -967,4 +969,58 @@
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -967,4 +969,58 @@
|
||||
<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.</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>
|
||||
@@ -459,7 +459,7 @@
|
||||
<string name="core_version">コアのバージョン: v%s</string>
|
||||
<string name="edit_image">画像を編集</string>
|
||||
<string name="callstatus_missed">不在着信</string>
|
||||
<string name="connect_calls_via_relay">リレー経由で繋がる。</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>
|
||||
|
||||
@@ -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>
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="icon_descr_cancel_image_preview">Annuleer afbeeldingsvoorbeeld</string>
|
||||
<string name="feature_cancelled_item">geannuleerd %s</string>
|
||||
<string name="icon_descr_cancel_live_message">Live bericht annuleren</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">veranderend adres…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">adres wijzigen…</string>
|
||||
<string name="notifications_mode_service">Altijd aan</string>
|
||||
<string name="icon_descr_asked_to_receive">Gevraagd om de afbeelding te ontvangen</string>
|
||||
<string name="change_verb">Wijziging</string>
|
||||
@@ -21,7 +21,7 @@
|
||||
<string name="allow_direct_messages">Sta het verzenden van directe berichten naar leden toe.</string>
|
||||
<string name="allow_to_delete_messages">Sta toe om verzonden berichten onomkeerbaar te verwijderen.</string>
|
||||
<string name="allow_to_send_voice">Sta toe om spraak berichten te verzenden.</string>
|
||||
<string name="chat_is_running">Chat is aktief</string>
|
||||
<string name="chat_is_running">Chat is actief</string>
|
||||
<string name="clear_chat_menu_action">Wissen</string>
|
||||
<string name="chat_database_section">CHAT DATABASE</string>
|
||||
<string name="chat_archive_section">CHAT ARCHIEF</string>
|
||||
@@ -57,7 +57,7 @@
|
||||
<string name="auto_accept_images">Afbeeldingen automatisch accepteren</string>
|
||||
<string name="auth_unavailable">Verificatie niet beschikbaar</string>
|
||||
<string name="back">Terug</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Contactverzoeken automatisch accepteren</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Contact verzoeken automatisch accepteren</string>
|
||||
<string name="bold">vetgedrukt</string>
|
||||
<string name="incognito_random_profile_description">Er wordt een willekeurig profiel naar uw contactpersoon verzonden</string>
|
||||
<string name="attach">Bijvoegen</string>
|
||||
@@ -67,17 +67,17 @@
|
||||
<string name="all_your_contacts_will_remain_connected">Al uw contacten blijven verbonden.</string>
|
||||
<string name="allow_voice_messages_question">Spraak berichten toestaan\?</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Goed voor batterij</b>. Achtergrondservice controleert elke 10 minuten op nieuwe berichten. U kunt oproepen en dringende berichten missen.</string>
|
||||
<string name="integrity_msg_bad_hash">Onjuiste bericht-hash</string>
|
||||
<string name="integrity_msg_bad_hash">Onjuiste bericht hash</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scan QR-code</b>: om verbinding te maken met uw contactpersoon die u de QR-code laat zien.</string>
|
||||
<string name="integrity_msg_bad_id">Onjuiste bericht-ID</string>
|
||||
<string name="integrity_msg_bad_id">Onjuiste bericht ID</string>
|
||||
<string name="call_already_ended">Oproep al beëindigd!</string>
|
||||
<string name="chat_item_ttl_month">1 maand</string>
|
||||
<string name="about_simplex">Over SimpleX</string>
|
||||
<string name="about_simplex_chat">Over <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="above_then_preposition_continuation">hierboven, dan:</string>
|
||||
<string name="above_then_preposition_continuation">hier boven, dan:</string>
|
||||
<string name="accept_requests">Verzoeken accepteren</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle gesprekken en berichten worden verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="clear_chat_warning">Alle berichten worden verwijderd - dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle gesprekken en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="clear_chat_warning">Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Sta verdwijnende berichten alleen toe als uw contactpersoon dit toestaat.</string>
|
||||
<string name="allow_voice_messages_only_if">Sta spraak berichten alleen toe als uw contactpersoon ze toestaat.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Laat uw contacten verzonden berichten onomkeerbaar verwijderen.</string>
|
||||
@@ -86,28 +86,28 @@
|
||||
<string name="icon_descr_audio_off">Geluid uit</string>
|
||||
<string name="full_backup">Back-up van app gegevens</string>
|
||||
<string name="answer_call">Beantwoord oproep</string>
|
||||
<string name="keychain_is_storing_securely">Android Keychain wordt gebruikt om het wachtwoord veilig op te slaan - hierdoor kan de meldings service werken.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keychain wordt gebruikt om het wachtwoord veilig op te slaan nadat u de app opnieuw hebt opgestart of het wachtwoord heeft gewijzigd - hiermee kunt u meldingen ontvangen.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keychain wordt gebruikt om het wachtwoord veilig op te slaan, hierdoor kan de meldings service werken.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keychain wordt gebruikt om het wachtwoord veilig op te slaan nadat u de app opnieuw hebt opgestart of het wachtwoord heeft gewijzigd, hiermee kunt u meldingen ontvangen.</string>
|
||||
<string name="app_version_code">App build: %s</string>
|
||||
<string name="notifications_mode_off_desc">App kan alleen meldingen ontvangen wanneer deze actief is, er wordt geen achtergrondservice gestart</string>
|
||||
<string name="appearance_settings">Uiterlijk</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="app_version_title">App versie</string>
|
||||
<string name="app_version_name">App-versie: v%s</string>
|
||||
<string name="app_version_name">App versie: v%s</string>
|
||||
<string name="network_session_mode_user_description">Er wordt een aparte TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk chat profiel dat je in de app hebt </b>.</string>
|
||||
<string name="audio_call_no_encryption">audio oproep (niet e2e versleuteld)</string>
|
||||
<string name="accept_automatically">Automatisch</string>
|
||||
<string name="notifications_mode_service_desc">Achtergrondservice is altijd actief - meldingen worden weergegeven zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="notifications_mode_service_desc">Achtergrondservice is altijd actief, meldingen worden weergegeven zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Nieuw contact toevoegen</b>: om uw eenmalige QR-code voor uw contact te maken.</string>
|
||||
<string name="icon_descr_call_ended">Oproep beëindigd</string>
|
||||
<string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief, waardoor achtergrondservice en periodieke verzoeken om nieuwe berichten worden uitgeschakeld. Je kunt ze weer inschakelen via instellingen.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Beste voor de batterij</b>. U ontvangt alleen meldingen als de app draait, de achtergronddienst wordt NIET gebruikt.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Het kan worden uitgeschakeld via instellingen</b> - meldingen worden nog steeds weergegeven terwijl de app actief is.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Het kan worden uitgeschakeld via instellingen</b>, meldingen worden nog steeds weergegeven terwijl de app actief is.</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Zowel jij als je contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contactpersoon kunnen verdwijnende berichten sturen.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Zowel jij als je contactpersoon kunnen spraak berichten verzenden.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Let op</b>: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijt raakt.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Gebruikt meer batterij</b>! Achtergrondservice is altijd actief - meldingen worden weergegeven zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Gebruikt meer batterij</b>! Achtergrondservice is altijd actief, meldingen worden weergegeven zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="icon_descr_cancel_link_preview">link voorbeeld annuleren</string>
|
||||
<string name="callstatus_ended">oproep beëindigd <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="database_initialization_error_title">Kan de database niet initialiseren</string>
|
||||
@@ -116,7 +116,7 @@
|
||||
<string name="cannot_access_keychain">Geen toegang tot Keystore om database wachtwoord op te slaan</string>
|
||||
<string name="cannot_receive_file">Kan bestand niet ontvangen</string>
|
||||
<string name="change_role">Rol wijzigen</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">veranderend adres…</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">adres wijzigen…</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">adres voor u gewijzigd</string>
|
||||
<string name="rcv_group_event_changed_member_role">veranderde rol van %s naar %s</string>
|
||||
<string name="change_member_role_question">Groep rol wijzigen\?</string>
|
||||
@@ -130,8 +130,8 @@
|
||||
<string name="network_session_mode_user">Chat profiel</string>
|
||||
<string name="settings_section_title_chats">GESPREKKEN</string>
|
||||
<string name="chat_with_developers">Praat met de ontwikkelaars</string>
|
||||
<string name="smp_servers_check_address">Controleer het serveradres en probeer het opnieuw.</string>
|
||||
<string name="choose_file">Kies bestand</string>
|
||||
<string name="smp_servers_check_address">Controleer het server adres en probeer het opnieuw.</string>
|
||||
<string name="choose_file">Bestand</string>
|
||||
<string name="clear_verb">Wissen</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Vergelijk beveiligingscodes met je contacten.</string>
|
||||
<string name="icon_descr_contact_checked">Contact gecontroleerd</string>
|
||||
@@ -143,7 +143,7 @@
|
||||
<string name="group_member_status_intro_invitation">verbinden (introductie uitnodiging)</string>
|
||||
<string name="display_name_connection_established">verbinding gemaakt</string>
|
||||
<string name="connection_request_sent">Verbindingsverzoek verzonden!</string>
|
||||
<string name="connection_timeout">Time-out verbinding</string>
|
||||
<string name="connection_timeout">Timeout verbinding</string>
|
||||
<string name="share_one_time_link">Maak een eenmalige uitnodiging link</string>
|
||||
<string name="create_address">Adres aanmaken</string>
|
||||
<string name="create_group_link">Groep link maken</string>
|
||||
@@ -166,13 +166,13 @@
|
||||
<string name="notification_preview_somebody">Contact verborgen:</string>
|
||||
<string name="image_decoding_exception_title">Decodeerfout</string>
|
||||
<string name="maximum_supported_file_size">De momenteel maximaal ondersteunde bestandsgrootte is <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact en alle berichten worden verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Contact en alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="icon_descr_server_status_connected">Verbonden</string>
|
||||
<string name="confirm_verb">Bevestigen</string>
|
||||
<string name="connect_via_link_or_qr">Maak verbinding via link / QR-code</string>
|
||||
<string name="copied">Gekopieerd naar het klembord</string>
|
||||
<string name="contribute">Bijdragen</string>
|
||||
<string name="configure_ICE_servers">ICE-servers configureren</string>
|
||||
<string name="configure_ICE_servers">ICE servers configureren</string>
|
||||
<string name="network_session_mode_entity">Verbinding</string>
|
||||
<string name="core_build_timestamp">Core gebouwd op: %s</string>
|
||||
<string name="core_version">Core versie: v%s</string>
|
||||
@@ -215,9 +215,9 @@
|
||||
<string name="create_profile_button">Maak</string>
|
||||
<string name="create_profile">Maak een profiel aan</string>
|
||||
<string name="delete_address">Adres verwijderen</string>
|
||||
<string name="connect_calls_via_relay">Verbinden via relais</string>
|
||||
<string name="always_use_relay">Verbinden via relais</string>
|
||||
<string name="status_contact_has_e2e_encryption">contact heeft e2e-codering</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">contact heeft geen e2e-encryptie</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">contact heeft geen e2e versleuteling</string>
|
||||
<string name="set_password_to_export_desc">De database is versleuteld met een willekeurige wachtwoord. Wijzig dit voordat u exporteert.</string>
|
||||
<string name="database_passphrase">Database wachtwoord</string>
|
||||
<string name="confirm_new_passphrase">Bevestig nieuw wachtwoord…</string>
|
||||
@@ -267,16 +267,16 @@
|
||||
<string name="delete_pending_connection__question">Wachtende verbinding verwijderen\?</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scan weergegeven QR-code vanuit de app, via <b>Scan QR-code</b>.</string>
|
||||
<string name="settings_developer_tools">Ontwikkel gereedschap</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Apparaatverificatie is uitgeschakeld. SimpleX Lock uitschakelen.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Apparaatverificatie is uitgeschakeld. SimpleX Vergrendelen uitschakelen.</string>
|
||||
<string name="display_name">Weergavenaam</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Lock inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld.</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Apparaatverificatie is niet ingeschakeld. Je kunt SimpleX Vergrendelen inschakelen via Instellingen zodra je apparaatverificatie hebt ingeschakeld.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Directe berichten tussen leden zijn verboden in deze groep.</string>
|
||||
<string name="total_files_count_and_size">%d bestand(en) met een totale grootte van %s</string>
|
||||
<string name="ttl_hour">%d uur</string>
|
||||
<string name="no_call_on_lock_screen">Uitzetten</string>
|
||||
<string name="v4_4_disappearing_messages">Verdwijnende berichten</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Verdwijnende berichten zijn verboden in dit gesprek.</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX Lock uitschakelen</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX Vergrendelen uitschakelen</string>
|
||||
<string name="timed_messages">Verdwijnende berichten</string>
|
||||
<string name="smp_server_test_disconnect">Verbinding verbreken</string>
|
||||
<string name="icon_descr_server_status_disconnected">Verbinding verbroken</string>
|
||||
@@ -314,14 +314,14 @@
|
||||
<string name="error_changing_address">Fout bij wijzigen van adres</string>
|
||||
<string name="error_deleting_pending_contact_connection">Fout bij het verwijderen van in behandeling zijnde contact verbinding</string>
|
||||
<string name="error_deleting_user">Fout bij het verwijderen van gebruikers profiel</string>
|
||||
<string name="auth_enable_simplex_lock">SimpleX Lock inschakelen</string>
|
||||
<string name="auth_enable_simplex_lock">SimpleX Vergrendelen inschakelen</string>
|
||||
<string name="hide_verb">Verbergen</string>
|
||||
<string name="icon_descr_edited">bewerkt</string>
|
||||
<string name="for_everybody">Voor iedereen</string>
|
||||
<string name="icon_descr_server_status_error">Fout</string>
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="edit_image">Bewerk afbeelding</string>
|
||||
<string name="exit_without_saving">Afsluiten zonder op te slaan</string>
|
||||
<string name="exit_without_saving">Afsluiten zonder opslaan</string>
|
||||
<string name="full_name_optional__prompt">Volledige naam (optioneel)</string>
|
||||
<string name="encrypted_video_call">e2e versleuteld video gesprek</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Schakel oproepen vanaf het vergrendelscherm in via Instellingen.</string>
|
||||
@@ -343,7 +343,7 @@
|
||||
<string name="snd_group_event_group_profile_updated">groep profiel bijgewerkt</string>
|
||||
<string name="group_member_status_group_deleted">groep verwijderd</string>
|
||||
<string name="icon_descr_expand_role">Vouw de rolselectie uit</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Groep wordt verwijderd voor alle leden - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Groep wordt verwijderd voor alle leden, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="error_creating_link_for_group">Fout bij maken van groep link</string>
|
||||
<string name="error_deleting_link_for_group">Fout bij verwijderen groep link</string>
|
||||
<string name="group_link">Groep link</string>
|
||||
@@ -356,7 +356,7 @@
|
||||
<string name="feature_enabled_for_contact">ingeschakeld voor contact</string>
|
||||
<string name="feature_enabled_for_you">voor u ingeschakeld</string>
|
||||
<string name="group_members_can_delete">Groepsleden kunnen verzonden berichten onherroepelijk verwijderen.</string>
|
||||
<string name="group_members_can_send_dms">Groepsleden kunnen directe berichten sturen.</string>
|
||||
<string name="group_members_can_send_dms">Groepsleden kunnen directe berichten sturen</string>
|
||||
<string name="group_members_can_send_voice">Groepsleden kunnen spraak berichten verzenden.</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per chat profiel (standaard) of per verbinding (BETA).</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Verschillende namen, avatars en transportisolatie.</string>
|
||||
@@ -368,7 +368,7 @@
|
||||
<string name="enable_automatic_deletion_question">Automatisch verwijderen van berichten aanzetten\?</string>
|
||||
<string name="enter_correct_passphrase">Voer het juiste wachtwoord in.</string>
|
||||
<string name="button_edit_group_profile">Groep profiel bewerken</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Schakel TCP-keep-alive in</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Schakel TCP keep-alive in</string>
|
||||
<string name="encrypt_database">Versleutelen</string>
|
||||
<string name="error_adding_members">Fout bij het toevoegen van gebruiker(s)</string>
|
||||
<string name="smp_servers_enter_manually">Voer de server handmatig in</string>
|
||||
@@ -378,15 +378,15 @@
|
||||
<string name="section_title_for_console">VOOR CONSOLE</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Groep profiel wordt opgeslagen op de apparaten van de leden, niet op de servers.</string>
|
||||
<string name="notification_preview_mode_hidden">Verborgen</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">De groep wordt voor u verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">De groep wordt voor u verwijderd, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="hide_notification">Verbergen</string>
|
||||
<string name="server_error">fout</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!</string>
|
||||
<string name="error_saving_file">Fout bij opslaan van bestand</string>
|
||||
<string name="file_not_found">Bestand niet gevonden</string>
|
||||
<string name="file_saved">Bestand opgeslagen</string>
|
||||
<string name="from_gallery_button">Uit galerij</string>
|
||||
<string name="error_saving_ICE_servers">Fout bij opslaan van ICE-servers</string>
|
||||
<string name="from_gallery_button">Galerij</string>
|
||||
<string name="error_saving_ICE_servers">Fout bij opslaan van ICE servers</string>
|
||||
<string name="callstate_ended">geëindigd</string>
|
||||
<string name="group_members_can_send_disappearing">Groepsleden kunnen verdwijnende berichten sturen.</string>
|
||||
<string name="ttl_week">%d week</string>
|
||||
@@ -397,8 +397,8 @@
|
||||
<string name="error_with_info">Fout: %s</string>
|
||||
<string name="error_creating_address">Fout bij aanmaken van adres</string>
|
||||
<string name="icon_descr_help">help</string>
|
||||
<string name="icon_descr_flip_camera">Flip-camera</string>
|
||||
<string name="error_saving_smp_servers">Fout bij opslaan van SMP-servers</string>
|
||||
<string name="icon_descr_flip_camera">Draai camera</string>
|
||||
<string name="error_saving_smp_servers">Fout bij opslaan van SMP servers</string>
|
||||
<string name="error_setting_network_config">Fout bij updaten van netwerk configuratie</string>
|
||||
<string name="failed_to_parse_chat_title">Kan het gesprek niet laden</string>
|
||||
<string name="failed_to_parse_chats_title">Kan de gesprekken niet laden</string>
|
||||
@@ -420,7 +420,7 @@
|
||||
<string name="notification_new_contact_request">Nieuw contactverzoek</string>
|
||||
<string name="auth_log_in_using_credential">Log in met uw inloggegevens</string>
|
||||
<string name="message_delivery_error_desc">Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd.</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Bericht wordt verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Bericht wordt verwijderd, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="large_file">Groot bestand!</string>
|
||||
<string name="mark_read">Markeer gelezen</string>
|
||||
<string name="mark_unread">Markeer als ongelezen</string>
|
||||
@@ -440,11 +440,11 @@
|
||||
<string name="how_it_works">Hoe het werkt</string>
|
||||
<string name="callstatus_missed">gemiste oproep</string>
|
||||
<string name="how_simplex_works">Hoe <xliff:g id="appName">SimpleX</xliff:g> werkt</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Veel mensen vroegen: <i>als <xliff:g id="appName">SimpleX</xliff:g> geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?</i></string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Veel mensen vroegen: <i>als <xliff:g id="appName">SimpleX</xliff:g> geen gebruikers ID\'s heeft, hoe kan het dan berichten bezorgen\?</i></string>
|
||||
<string name="incoming_audio_call">Inkomende audio oproep</string>
|
||||
<string name="incoming_video_call">Inkomend video gesprek</string>
|
||||
<string name="ignore">Negeren</string>
|
||||
<string name="status_no_e2e_encryption">geen e2e-encryptie</string>
|
||||
<string name="status_no_e2e_encryption">geen e2e versleuteling</string>
|
||||
<string name="import_database_question">Chat database importeren\?</string>
|
||||
<string name="chat_item_ttl_none">nooit</string>
|
||||
<string name="no_received_app_files">Geen ontvangen of verzonden bestanden</string>
|
||||
@@ -454,17 +454,17 @@
|
||||
<string name="group_member_status_invited">uitgenodigd</string>
|
||||
<string name="button_leave_group">Groep verlaten</string>
|
||||
<string name="info_row_local_name">Lokale naam</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Incognito modus wordt hier niet ondersteund - uw hoofdprofiel wordt naar groepsleden verzonden</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Incognito modus wordt hier niet ondersteund, uw hoofdprofiel wordt naar groepsleden verzonden</string>
|
||||
<string name="users_delete_data_only">Alleen lokale profielgegevens</string>
|
||||
<string name="message_deletion_prohibited_in_chat">Het onomkeerbaar verwijderen van berichten is verboden in deze groep.</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">App-scherm verbergen in de recente apps.</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">App scherm verbergen in de recente apps.</string>
|
||||
<string name="settings_section_title_incognito">Incognito modus</string>
|
||||
<string name="messages_section_title">Berichten</string>
|
||||
<string name="new_passphrase">Nieuw wachtwoord…</string>
|
||||
<string name="keychain_error">Keychain fout</string>
|
||||
<string name="join_group_button">Word lid van</string>
|
||||
<string name="leave_group_question">Groep verlaten\?</string>
|
||||
<string name="new_member_role">Nieuwe ledenrol</string>
|
||||
<string name="new_member_role">Nieuwe leden rol</string>
|
||||
<string name="no_contacts_to_add">Geen contacten om toe te voegen</string>
|
||||
<string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel.</string>
|
||||
<string name="theme_light">Licht</string>
|
||||
@@ -491,12 +491,12 @@
|
||||
\nWe zullen serverredundantie toevoegen om verloren berichten te voorkomen.</string>
|
||||
<string name="joining_group">Deel nemen aan groep</string>
|
||||
<string name="leave_group_button">Verlaten</string>
|
||||
<string name="group_member_role_member">gebruiker</string>
|
||||
<string name="group_member_role_member">Gebruiker</string>
|
||||
<string name="image_descr_link_preview">link voorbeeld afbeelding</string>
|
||||
<string name="member_info_section_title_member">GEBRUIKER</string>
|
||||
<string name="settings_section_title_messages">BERICHTEN</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobiel: tik op <b>Openen in mobiele app</b> en tik vervolgens op <b>Verbinden</b> in de app.</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Gebruiker wordt uit de groep verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Gebruiker wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="message_delivery_error_title">Fout bij bezorging van bericht</string>
|
||||
<string name="delete_message_mark_deleted_warning">Bericht wordt gemarkeerd voor verwijdering. De ontvanger(s) kunnen dit bericht onthullen.</string>
|
||||
<string name="network_status">Netwerk status</string>
|
||||
@@ -506,7 +506,7 @@
|
||||
<string name="import_database">Database importeren</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Verbeterde privacy en veiligheid</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">De afbeelding wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later!</string>
|
||||
<string name="incognito_info_protects">De incognito modus beschermt de privacy van uw hoofdprofielnaam en -afbeelding - voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt.</string>
|
||||
<string name="incognito_info_protects">De incognito modus beschermt de privacy van uw hoofdprofielnaam en afbeelding, voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt.</string>
|
||||
<string name="new_database_archive">Nieuw database archief</string>
|
||||
<string name="no_details">geen details</string>
|
||||
<string name="conn_level_desc_indirect">indirect (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
@@ -516,14 +516,14 @@
|
||||
<string name="settings_notifications_mode_title">Meldingsservice</string>
|
||||
<string name="notifications">Meldingen</string>
|
||||
<string name="invalid_contact_link">Ongeldige link!</string>
|
||||
<string name="smp_servers_invalid_address">Ongeldig serveradres!</string>
|
||||
<string name="smp_servers_invalid_address">Ongeldig server adres!</string>
|
||||
<string name="install_simplex_chat_for_terminal">Installeer <xliff:g id="appNameFull">SimpleX Chat</xliff:g> voor terminal</string>
|
||||
<string name="how_to">Hoe</string>
|
||||
<string name="how_to_use_your_servers">Hoe u uw servers gebruikt</string>
|
||||
<string name="network_and_servers">Netwerk & servers</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE-servers (één per lijn)</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Zorg ervoor dat WebRTC ICE-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="network_disable_socks_info">Als u bevestigt, kunnen de berichtenservers uw IP-adres zien en uw provider - met welke servers u verbinding maakt.</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE servers (één per lijn)</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="network_disable_socks_info">Als u bevestigt, kunnen de berichten servers uw IP-adres zien en uw provider, met welke servers u verbinding maakt.</string>
|
||||
<string name="network_use_onion_hosts_no">Nee</string>
|
||||
<string name="immune_to_spam_and_abuse">Immuun voor spam en misbruik</string>
|
||||
<string name="make_private_connection">Maak een privéverbinding</string>
|
||||
@@ -535,7 +535,7 @@
|
||||
<string name="rcv_group_event_invited_via_your_group_link">uitgenodigd via je groep link</string>
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="icon_descr_call_missed">Gemiste oproep</string>
|
||||
<string name="description_via_contact_address_link_incognito">incognito via link naar contactadres</string>
|
||||
<string name="description_via_contact_address_link_incognito">incognito via contact adres link</string>
|
||||
<string name="description_via_group_link_incognito">incognito via groep link</string>
|
||||
<string name="description_via_one_time_link_incognito">incognito via eenmalige link</string>
|
||||
<string name="invalid_chat">ongeldige gesprek</string>
|
||||
@@ -543,21 +543,21 @@
|
||||
<string name="invalid_message_format">ongeldig berichtformaat</string>
|
||||
<string name="display_name_invited_to_connect">uitgenodigd om te verbinden</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Zorg ervoor dat SMP-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Zorg ervoor dat SMP server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="marked_deleted_description">gemarkeerd als verwijderd</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</string>
|
||||
<string name="image_descr_profile_image">profielfoto</string>
|
||||
<string name="privacy_redefined">Privacy opnieuw gedefinieerd</string>
|
||||
<string name="privacy_and_security">Privacy en beveiliging</string>
|
||||
<string name="network_error_desc">Controleer uw netwerkverbinding met <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> en probeer het opnieuw.</string>
|
||||
<string name="error_smp_test_certificate">Mogelijk is de certificaatvingerafdruk in het serveradres onjuist</string>
|
||||
<string name="error_smp_test_certificate">Mogelijk is de certificaat vingerafdruk in het server adres onjuist</string>
|
||||
<string name="periodic_notifications">Periodieke meldingen</string>
|
||||
<string name="auth_open_chat_console">Chat console openen</string>
|
||||
<string name="toast_permission_denied">Geen toestemming!</string>
|
||||
<string name="icon_descr_profile_image_placeholder">profielafbeelding tijdelijke aanduiding</string>
|
||||
<string name="one_time_link">Eenmalige uitnodiging link</string>
|
||||
<string name="paste_button">Plakken</string>
|
||||
<string name="smp_servers_preset_address">Vooraf ingesteld serveradres</string>
|
||||
<string name="smp_servers_preset_address">Vooraf ingesteld server adres</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts worden niet gebruikt.</string>
|
||||
<string name="onboarding_notifications_mode_title">Privé meldingen</string>
|
||||
<string name="paste_the_link_you_received">Plak de ontvangen link</string>
|
||||
@@ -573,12 +573,12 @@
|
||||
<string name="only_you_can_send_disappearing">Alleen jij kunt verdwijnende berichten verzenden.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Alleen uw contactpersoon kan verdwijnende berichten verzenden.</string>
|
||||
<string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</string>
|
||||
<string name="feature_offered_item_with_param">aangeboden %s: %2s</string>
|
||||
<string name="feature_offered_item_with_param">voorgesteld %s: %2s</string>
|
||||
<string name="old_database_archive">Oud database archief</string>
|
||||
<string name="enter_correct_current_passphrase">Voer het juiste huidige wachtwoord in.</string>
|
||||
<string name="group_member_role_owner">eigenaar</string>
|
||||
<string name="network_option_ping_count">PING-telling</string>
|
||||
<string name="network_option_ping_interval">PING-interval</string>
|
||||
<string name="group_member_role_owner">Eigenaar</string>
|
||||
<string name="network_option_ping_count">PING telling</string>
|
||||
<string name="network_option_ping_interval">PING interval</string>
|
||||
<string name="v4_5_message_draft_descr">Bewaar het laatste berichtconcept, met bijlagen.</string>
|
||||
<string name="v4_5_private_filenames">Privé bestandsnamen</string>
|
||||
<string name="images_limit_desc">Er kunnen slechts 10 afbeeldingen tegelijk worden verzonden</string>
|
||||
@@ -599,13 +599,13 @@
|
||||
<string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts worden gebruikt indien beschikbaar.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code – iedereen kan de servers draaien.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code. Iedereen kan de servers draaien.</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string>
|
||||
<string name="only_your_contact_can_delete">Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string>
|
||||
<string name="only_you_can_send_voice">Alleen jij kunt spraak berichten verzenden.</string>
|
||||
<string name="only_your_contact_can_send_voice">Alleen uw contactpersoon kan spraak berichten verzenden.</string>
|
||||
<string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string>
|
||||
<string name="feature_offered_item">aangeboden %s</string>
|
||||
<string name="feature_offered_item">voorgesteld %s</string>
|
||||
<string name="store_passphrase_securely_without_recover">Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken.</string>
|
||||
<string name="store_passphrase_securely">Bewaar het wachtwoord veilig, u kunt deze NIET wijzigen als u deze kwijtraakt.</string>
|
||||
<string name="open_chat">Gesprekken openen</string>
|
||||
@@ -613,9 +613,9 @@
|
||||
<string name="icon_descr_call_pending_sent">Oproep in behandeling</string>
|
||||
<string name="simplex_link_mode_browser_warning">Het openen van de link in de browser kan de privacy en beveiliging van de verbinding verminderen. Niet vertrouwde SimpleX links worden rood weergegeven.</string>
|
||||
<string name="contact_developers">Werk de app bij en neem contact op met de ontwikkelaars.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Alleen client-apparaten slaan gebruikersprofielen, contacten, groepen en berichten op die zijn verzonden met <b>2-laags end-to-end-codering</b>.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met <b>2-laags end-to-end codering</b>.</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">De afzender heeft mogelijk het verbindingsverzoek verwijderd.</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">Schakel SimpleX Lock in om uw informatie te beschermen.
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Schakel SimpleX Vergrendelen in om uw informatie te beschermen.
|
||||
\nU wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld.</string>
|
||||
<string name="la_notice_turn_on">Aanzetten</string>
|
||||
<string name="auth_unlock">Ontgrendelen</string>
|
||||
@@ -629,15 +629,15 @@
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Uw contactpersoon kan de QR-code vanuit de app scannen.</string>
|
||||
<string name="share_invitation_link">Uitnodiging link delen</string>
|
||||
<string name="scan_code">Code scannen</string>
|
||||
<string name="your_contact_address">Uw contactadres</string>
|
||||
<string name="your_contact_address">Uw contact adres</string>
|
||||
<string name="your_settings">Uw instellingen</string>
|
||||
<string name="share_link">Deel link</string>
|
||||
<string name="you_control_your_chat">Jij beheert je gesprek!</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen.</string>
|
||||
<string name="callstate_starting">beginnen…</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">U bepaalt via welke server(s) je de berichten <b>ontvangt</b>, uw contacten – de servers die u gebruikt om ze berichten te sturen.</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">U bepaalt via welke server(s) je de berichten <b>ontvangt</b>, uw contacten de servers die u gebruikt om ze berichten te sturen.</string>
|
||||
<string name="icon_descr_video_on">Video aan</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Deze actie kan niet ongedaan worden gemaakt - uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string>
|
||||
<string name="messages_section_description">Deze instelling is van toepassing op berichten in jou huidige chat profiel</string>
|
||||
<string name="save_archive">Bewaar archief</string>
|
||||
<string name="rcv_group_event_updated_group_profile">bijgewerkt groep profiel</string>
|
||||
@@ -654,7 +654,7 @@
|
||||
<string name="notification_preview_mode_message_desc">Toon contact en bericht</string>
|
||||
<string name="notification_preview_mode_contact_desc">Toon alleen contactpersoon</string>
|
||||
<string name="ntf_channel_messages">SimpleX Chat berichten</string>
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX Lock ingeschakeld</string>
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX Vergrendelen ingeschakeld</string>
|
||||
<string name="auth_stop_chat">Stop chat</string>
|
||||
<string name="reply_verb">Antwoord</string>
|
||||
<string name="save_verb">Opslaan</string>
|
||||
@@ -669,7 +669,7 @@
|
||||
<string name="welcome">Welkom!</string>
|
||||
<string name="group_preview_you_are_invited">je bent uitgenodigd voor de groep</string>
|
||||
<string name="you_have_no_chats">Je hebt geen gesprekken</string>
|
||||
<string name="your_chats">Je gesprekken</string>
|
||||
<string name="your_chats">Jouw gesprekken</string>
|
||||
<string name="share_file">Deel bestand…</string>
|
||||
<string name="share_image">Afbeelding delen…</string>
|
||||
<string name="icon_descr_waiting_for_image">Wachten op afbeelding</string>
|
||||
@@ -706,14 +706,14 @@
|
||||
<string name="security_code">Beveiligingscode</string>
|
||||
<string name="is_not_verified">%s is niet geverifieerd</string>
|
||||
<string name="is_verified">%s is geverifieerd</string>
|
||||
<string name="to_verify_compare">Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contactpersoon te verifiëren.</string>
|
||||
<string name="to_verify_compare">Vergelijk (of scan) de code op uw apparaten om end-to-end codering met uw contactpersoon te verifiëren.</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">U kunt ook verbinding maken door op de link te klikken. Als het in de browser wordt geopend, klikt u op de knop <b> Openen in mobiele app </b>.</string>
|
||||
<string name="your_profile_will_be_sent">Uw chat profiel wordt naar uw contactpersoon verzonden</string>
|
||||
<string name="your_chat_profiles">Je chat profielen</string>
|
||||
<string name="your_simplex_contact_address">Uw <xliff:g id="appName">SimpleX</xliff:g> contactadres</string>
|
||||
<string name="your_chat_profiles">Uw chat profielen</string>
|
||||
<string name="your_simplex_contact_address">Uw <xliff:g id="appName">SimpleX</xliff:g> contact adres</string>
|
||||
<string name="chat_with_the_founder">Stuur vragen en ideeën</string>
|
||||
<string name="send_us_an_email">Stuur ons een e-mail</string>
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_lock">SimpleX Vergrendelen</string>
|
||||
<string name="smp_servers">SMP servers</string>
|
||||
<string name="smp_servers_save">Bewaar servers</string>
|
||||
<string name="smp_servers_test_failed">Servertest mislukt!</string>
|
||||
@@ -722,17 +722,17 @@
|
||||
<string name="smp_servers_test_server">Server test</string>
|
||||
<string name="rate_the_app">Beoordeel de app</string>
|
||||
<string name="smp_servers_use_server">Gebruik server</string>
|
||||
<string name="using_simplex_chat_servers">Gebruik van <xliff:g id="appNameFull">SimpleX Chat</xliff:g>-servers.</string>
|
||||
<string name="smp_servers_your_server_address">Uw serveradres</string>
|
||||
<string name="using_simplex_chat_servers">Gebruik van <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers.</string>
|
||||
<string name="smp_servers_your_server_address">Uw server adres</string>
|
||||
<string name="smp_servers_your_server">Uw server</string>
|
||||
<string name="network_session_mode_transport_isolation">Transport isolation</string>
|
||||
<string name="network_socks_toggle">SOCKS-proxy gebruiken (poort 9050)</string>
|
||||
<string name="save_preferences_question">Voorkeuren opslaan\?</string>
|
||||
<string name="save_and_notify_contact">Opslaan en Contact melden</string>
|
||||
<string name="section_title_welcome_message">WELKOMS BERICHT</string>
|
||||
<string name="section_title_welcome_message">WELKOMST BERICHT</string>
|
||||
<string name="your_current_profile">Je huidige profiel</string>
|
||||
<string name="save_and_notify_contacts">Opslaan en Contacten melden</string>
|
||||
<string name="save_and_notify_group_members">Opslaan en Groepleden melden</string>
|
||||
<string name="save_and_notify_group_members">Opslaan en Groepsleden melden</string>
|
||||
<string name="strikethrough">staking</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Het berichten- en applicatieplatform dat uw privacy en veiligheid beschermt.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Het profiel wordt alleen gedeeld met uw contacten.</string>
|
||||
@@ -742,7 +742,7 @@
|
||||
<string name="secret">geheim</string>
|
||||
<string name="next_generation_of_private_messaging">De volgende generatie privéberichten</string>
|
||||
<string name="callstate_waiting_for_answer">wachten op antwoord…</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Om de privacy te beschermen, heeft <xliff:g id="appName">SimpleX</xliff:g> in plaats van gebruikers-ID\'s die door alle andere platforms worden gebruikt, ID\'s voor berichtenwachtrijen, afzonderlijk voor elk van uw contacten.</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Om de privacy te beschermen, heeft <xliff:g id="appName">SimpleX</xliff:g> in plaats van gebruikers ID\'s die door alle andere platforms worden gebruikt, ID\'s voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</string>
|
||||
<string name="use_chat">Gebruik chat</string>
|
||||
<string name="onboarding_notifications_mode_off">Wanneer de app actief is</string>
|
||||
<string name="video_call_no_encryption">video gesprek (niet e2e versleuteld)</string>
|
||||
@@ -751,8 +751,10 @@
|
||||
<string name="icon_descr_video_call">video oproep</string>
|
||||
<string name="your_calls">Uw oproepen</string>
|
||||
<string name="show_call_on_lock_screen">Toon</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE-servers</string>
|
||||
<string name="your_ice_servers">Uw ICE-servers</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE servers</string>
|
||||
<string name="relay_server_protects_ip">Relay server beschermt uw IP-adres, maar kan de duur van het gesprek observeren.</string>
|
||||
<string name="relay_server_if_necessary">Relay server wordt alleen gebruikt als dat nodig is. Een andere partij kan uw IP-adres zien.</string>
|
||||
<string name="your_ice_servers">Uw ICE servers</string>
|
||||
<string name="alert_title_skipped_messages">Overgeslagen berichten</string>
|
||||
<string name="call_connection_via_relay">via relais</string>
|
||||
<string name="icon_descr_video_off">Video uit</string>
|
||||
@@ -769,7 +771,7 @@
|
||||
<string name="you_must_use_the_most_recent_version_of_database">U mag de meest recente versie van uw chat database ALLEEN op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten.</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Start de app opnieuw om de geïmporteerde chat database te gebruiken.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Stop de chat om database acties mogelijk te maken.</string>
|
||||
<string name="delete_files_and_media_desc">Deze actie kan niet ongedaan worden gemaakt - alle ontvangen en verzonden bestanden en media worden verwijderd. Foto\'s met een lage resolutie blijven behouden.</string>
|
||||
<string name="delete_files_and_media_desc">Deze actie kan niet ongedaan worden gemaakt, alle ontvangen en verzonden bestanden en media worden verwijderd. Foto\'s met een lage resolutie blijven behouden.</string>
|
||||
<string name="remove_passphrase_from_keychain">Wachtwoord verwijderen uit Keychain\?</string>
|
||||
<string name="remove_passphrase">Verwijderen</string>
|
||||
<string name="update_database">Update</string>
|
||||
@@ -783,13 +785,13 @@
|
||||
<string name="rcv_group_event_user_deleted">heeft je verwijderd</string>
|
||||
<string name="group_info_member_you">jij: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> LEDEN</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">U kunt een link of een QR-code delen - iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert.</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">U kunt een link of een QR-code delen. Iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert.</string>
|
||||
<string name="switch_verb">Wijzig</string>
|
||||
<string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in \"%s\". Iedereen in de groep wordt op de hoogte gebracht.</string>
|
||||
<string name="receiving_via">Ontvang via</string>
|
||||
<string name="save_group_profile">Groep profiel opslaan</string>
|
||||
<string name="group_is_decentralized">De groep is volledig gedecentraliseerd – het is alleen zichtbaar voor de leden.</string>
|
||||
<string name="network_option_tcp_connection_timeout">Time-out van TCP-verbinding</string>
|
||||
<string name="network_option_tcp_connection_timeout">Timeout van TCP-verbinding</string>
|
||||
<string name="voice_messages">Spraak berichten</string>
|
||||
<string name="voice_prohibited_in_this_chat">Spraak berichten zijn verboden in dit gesprek.</string>
|
||||
<string name="prohibit_sending_disappearing">Verbied het verzenden van verdwijnende berichten.</string>
|
||||
@@ -800,7 +802,7 @@
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Stop de chat om de chat database te exporteren, importeren of verwijderen. U kunt geen berichten ontvangen en verzenden terwijl de chat is gestopt.</string>
|
||||
<string name="chat_item_ttl_seconds">%s seconde(n)</string>
|
||||
<string name="update_database_passphrase">Database wachtwoord bijwerken</string>
|
||||
<string name="database_is_not_encrypted">Uw chat database is niet versleuteld - stel een wachtwoord in om deze te beschermen.</string>
|
||||
<string name="database_is_not_encrypted">Uw chat database is niet versleuteld, stel een wachtwoord in om deze te beschermen.</string>
|
||||
<string name="database_restore_error">Databasefout herstellen</string>
|
||||
<string name="unknown_error">Onbekende fout</string>
|
||||
<string name="wrong_passphrase_title">Verkeerd wachtwoord!</string>
|
||||
@@ -823,7 +825,7 @@
|
||||
<string name="conn_stats_section_title_servers">SERVERS</string>
|
||||
<string name="network_options_reset_to_defaults">Resetten naar standaardwaarden</string>
|
||||
<string name="switch_receiving_address">Ontvangst adres wijzigen</string>
|
||||
<string name="network_option_protocol_timeout">Protocol time-out</string>
|
||||
<string name="network_option_protocol_timeout">Protocol timeout</string>
|
||||
<string name="network_options_revert">Terugdraaien</string>
|
||||
<string name="network_options_save">Opslaan</string>
|
||||
<string name="network_option_seconds_label">sec</string>
|
||||
@@ -857,16 +859,16 @@
|
||||
<string name="voice_messages_prohibited">Spraak berichten verboden!</string>
|
||||
<string name="reset_verb">Resetten</string>
|
||||
<string name="send_verb">Verstuur</string>
|
||||
<string name="send_live_message_desc">Stuur een live bericht - het wordt bijgewerkt voor de ontvanger(s) terwijl u het typt</string>
|
||||
<string name="send_live_message_desc">Stuur een live bericht, het wordt bijgewerkt voor de ontvanger(s) terwijl u het typt</string>
|
||||
<string name="to_share_with_your_contact">(om te delen met uw contact)</string>
|
||||
<string name="thank_you_for_installing_simplex">Bedankt voor het installeren van <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="use_camera_button">Gebruik camera</string>
|
||||
<string name="use_camera_button">Camera</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Gebruik voor nieuwe verbindingen</string>
|
||||
<string name="star_on_github">Star on GitHub</string>
|
||||
<string name="smp_servers_per_user">De servers voor nieuwe verbindingen van je huidige chat profiel</string>
|
||||
<string name="your_SMP_servers">Uw SMP-servers</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Opgeslagen WebRTC ICE-servers worden verwijderd.</string>
|
||||
<string name="your_ICE_servers">Uw ICE-servers</string>
|
||||
<string name="your_SMP_servers">Uw SMP servers</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Opgeslagen WebRTC ICE servers worden verwijderd.</string>
|
||||
<string name="your_ICE_servers">Uw ICE servers</string>
|
||||
<string name="save_servers_button">Opslaan</string>
|
||||
<string name="network_enable_socks">SOCKS-proxy gebruiken\?</string>
|
||||
<string name="network_use_onion_hosts">Gebruik .onion-hosts</string>
|
||||
@@ -877,19 +879,19 @@
|
||||
<string name="update_network_session_mode_question">Transportisolatiemodus updaten\?</string>
|
||||
<string name="callstate_received_confirmation">bevestiging ontvangen…</string>
|
||||
<string name="callstate_waiting_for_confirmation">Wachten op bevestiging…</string>
|
||||
<string name="first_platform_without_user_ids">Het eerste platform zonder gebruikers-ID\'s - privé door ontwerp.</string>
|
||||
<string name="first_platform_without_user_ids">Het eerste platform zonder gebruikers ID\'s, privé door ontwerp.</string>
|
||||
<string name="prohibit_direct_messages">Verbied het sturen van directe berichten naar leden.</string>
|
||||
<string name="prohibit_sending_voice">Verbieden het verzenden van spraak berichten.</string>
|
||||
<string name="v4_2_security_assessment_desc">De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Met optioneel welkomstbericht.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Met optioneel welkomst bericht.</string>
|
||||
<string name="v4_3_voice_messages">Spraak berichten</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Uw contacten kunnen volledige verwijdering van berichten toestaan.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">U moet elke keer dat de app start het wachtwoord invoeren - deze wordt niet op het apparaat opgeslagen.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen.</string>
|
||||
<string name="wrong_passphrase">Verkeerd wachtwoord voor de database</string>
|
||||
<string name="save_passphrase_and_open_chat">Bewaar het wachtwoord en open je gesprekken</string>
|
||||
<string name="database_backup_can_be_restored">De poging om het wachtwoord van de database te wijzigen is niet voltooid.</string>
|
||||
<string name="restore_database">Databaseback-up terugzetten</string>
|
||||
<string name="restore_database_alert_title">Databaseback-up terugzetten\?</string>
|
||||
<string name="restore_database">Database back-up terugzetten</string>
|
||||
<string name="restore_database_alert_title">Database back-up terugzetten\?</string>
|
||||
<string name="restore_database_alert_confirm">Herstellen</string>
|
||||
<string name="snd_group_event_changed_member_role">je veranderde de rol van %s in %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">je veranderde de rol voor jezelf naar %s</string>
|
||||
@@ -906,38 +908,38 @@
|
||||
<string name="error_smp_test_server_auth">Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord</string>
|
||||
<string name="set_group_preferences">Groep voorkeuren instellen</string>
|
||||
<string name="share_message">Bericht delen…</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Vergrendelen</string>
|
||||
<string name="save_passphrase_in_keychain">Sla het wachtwoord op in Keychain</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="v4_5_italian_interface_descr">Dank aan de gebruikers – draag bij via Weblate!</string>
|
||||
<string name="periodic_notifications_desc">De app haalt regelmatig nieuwe berichten op - het gebruikt een paar procent van de batterij per dag. De app maakt geen gebruik van push meldingen - gegevens van uw apparaat worden niet naar de servers verzonden.</string>
|
||||
<string name="periodic_notifications_desc">De app haalt regelmatig nieuwe berichten op - het gebruikt een paar procent van de batterij per dag. De app maakt geen gebruik van push meldingen, gegevens van uw apparaat worden niet naar de servers verzonden.</string>
|
||||
<string name="image_decoding_exception_desc">De afbeelding kan niet worden gedecodeerd. Probeer een andere afbeelding of neem contact op met de ontwikkelaars.</string>
|
||||
<string name="settings_section_title_themes">THEMA\'S</string>
|
||||
<string name="smp_servers_scan_qr">Scan server QR-code</string>
|
||||
<string name="this_string_is_not_a_connection_link">Deze string is geen verbinding link!</string>
|
||||
<string name="enable_automatic_deletion_message">Deze actie kan niet ongedaan worden gemaakt - de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren.</string>
|
||||
<string name="enable_automatic_deletion_message">Deze actie kan niet ongedaan worden gemaakt, de berichten die eerder zijn verzonden en ontvangen dan geselecteerd, worden verwijderd. Het kan enkele minuten duren.</string>
|
||||
<string name="switch_receiving_address_desc">Deze functie is experimenteel! Het werkt alleen als op de andere client versie 4.2 is geïnstalleerd. U zou het bericht in het gesprek moeten zien zodra de adreswijziging is voltooid. Controleer of u nog steeds berichten van dit contact (of groepslid) kunt ontvangen.</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Om je privacy te behouden, heeft de app in plaats van push meldingen een <b><xliff:g id="appName">SimpleX</xliff:g> achtergrondservice</b> – deze gebruikt een paar procent van de batterij per dag.</string>
|
||||
<string name="chat_preferences_you_allow">Jij staat toe</string>
|
||||
<string name="you_are_invited_to_group">Je bent uitgenodigd voor de groep</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">U kunt <font color="#0088ff">verbinding maken met <xliff:g id="appNameFull">SimpleX Chat</xliff:g> ontwikkelaars om vragen te stellen en updates te ontvangen</font>.</string>
|
||||
<string name="connection_error_auth_desc">Tenzij uw contactpersoon de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn - meld het alstublieft.
|
||||
<string name="connection_error_auth_desc">Tenzij uw contactpersoon de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.
|
||||
\nOm verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string>
|
||||
<string name="update_onion_hosts_settings_question">.onion hosts-instelling updaten\?</string>
|
||||
<string name="use_simplex_chat_servers__question"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>-servers gebruiken\?</string>
|
||||
<string name="use_simplex_chat_servers__question"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers gebruiken\?</string>
|
||||
<string name="voice_messages_are_prohibited">Spraak berichten zijn verboden in deze groep.</string>
|
||||
<string name="personal_welcome">Welkom <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">U kunt uw adres delen als een link of als een QR-code - iedereen kan verbinding met u maken. U verliest uw contacten niet als u deze later verwijdert.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">U kunt uw adres delen als een link of als een QR-code. Iedereen kan verbinding met u maken. U verliest uw contacten niet als u deze later verwijdert.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten.</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">je hebt het adres gewijzigd voor %s</string>
|
||||
<string name="snd_group_event_member_deleted">je hebt <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> verwijderd</string>
|
||||
<string name="contact_sent_large_file">Je contactpersoon heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Uw huidige chat database wordt VERWIJDERD en VERVANGEN door de geïmporteerde.
|
||||
\nDeze actie kan niet ongedaan worden gemaakt - uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde.
|
||||
\nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string>
|
||||
<string name="invite_prohibited_description">Je probeert een contact met wie je een incognito profiel hebt gedeeld, uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten.
|
||||
\n
|
||||
\n<xliff:g id="appName">SimpleX</xliff:g>-servers kunnen uw profiel niet zien.</string>
|
||||
\n<xliff:g id="appName">SimpleX</xliff:g> servers kunnen uw profiel niet zien.</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat.</string>
|
||||
<string name="images_limit_title">Te veel afbeeldingen!</string>
|
||||
<string name="icon_descr_speaker_off">Luidspreker uit</string>
|
||||
@@ -946,7 +948,7 @@
|
||||
<string name="sender_cancelled_file_transfer">Afzender heeft bestandsoverdracht geannuleerd.</string>
|
||||
<string name="receiving_files_not_yet_supported">het ontvangen van bestanden wordt nog niet ondersteund</string>
|
||||
<string name="sending_files_not_yet_supported">het verzenden van bestanden wordt nog niet ondersteund</string>
|
||||
<string name="simplex_link_contact">SimpleX contactadres</string>
|
||||
<string name="simplex_link_contact">SimpleX contact adres</string>
|
||||
<string name="simplex_link_group">SimpleX groep link</string>
|
||||
<string name="simplex_link_mode">SimpleX links</string>
|
||||
<string name="simplex_link_invitation">Eenmalige SimpleX uitnodiging</string>
|
||||
@@ -954,7 +956,7 @@
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="unknown_message_format">onbekend berichtformaat</string>
|
||||
<string name="simplex_link_mode_browser">Via browser</string>
|
||||
<string name="description_via_contact_address_link">via contactadres link</string>
|
||||
<string name="description_via_contact_address_link">via contact adres link</string>
|
||||
<string name="description_via_group_link">via groep link</string>
|
||||
<string name="description_via_one_time_link">via een eenmalige link</string>
|
||||
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
@@ -962,8 +964,62 @@
|
||||
<string name="description_you_shared_one_time_link">je hebt een eenmalige link gedeeld</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">je hebt een eenmalige link incognito gedeeld</string>
|
||||
<string name="chat_help_tap_button">Tik op de knop</string>
|
||||
<string name="read_more_in_github_with_link">Lees meer in onze <font color="#0088ff">GitHub-repository</font>.</string>
|
||||
<string name="read_more_in_github">Lees meer in onze GitHub-repository.</string>
|
||||
<string name="read_more_in_github_with_link">Lees meer in onze <font color="#0088ff">GitHub repository</font>.</string>
|
||||
<string name="read_more_in_github">Lees meer in onze GitHub repository.</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> bericht(en) overgeslagen</string>
|
||||
<string name="moderated_description">gemodereerd</string>
|
||||
<string name="moderated_item_description">gemodereerd door %s</string>
|
||||
<string name="delete_member_message__question">Bericht van lid verwijderen\?</string>
|
||||
<string name="moderate_verb">Modereren</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">Het bericht wordt verwijderd voor alle leden.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Het bericht wordt gemarkeerd als gemodereerd voor alle leden.</string>
|
||||
<string name="observer_cant_send_message_desc">Neem contact op met de groep beheerder.</string>
|
||||
<string name="error_updating_link_for_group">Fout bij bijwerken van groep link</string>
|
||||
<string name="initial_member_role">Initiële rol</string>
|
||||
<string name="group_member_role_observer">Waarnemer</string>
|
||||
<string name="observer_cant_send_message_title">Je kunt geen berichten versturen!</string>
|
||||
<string name="you_are_observer">jij bent waarnemer</string>
|
||||
<string name="language_system">Systeem</string>
|
||||
<string name="v4_6_audio_video_calls">Audio en video oproepen</string>
|
||||
<string name="cant_delete_user_profile">Kan gebruikers profiel niet verwijderen!</string>
|
||||
<string name="confirm_password">Bevestig wachtwoord</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Chinese en Spaanse interface</string>
|
||||
<string name="enter_password_to_show">Voer wachtwoord in bij zoeken</string>
|
||||
<string name="error_saving_user_password">Fout bij opslaan gebruikers wachtwoord</string>
|
||||
<string name="button_add_welcome_message">Welkomst bericht toevoegen</string>
|
||||
<string name="dont_show_again">Niet meer weergeven</string>
|
||||
<string name="v4_6_group_moderation">Groep moderatie</string>
|
||||
<string name="error_updating_user_privacy">Fout bij updaten van gebruikers privacy</string>
|
||||
<string name="v4_6_reduced_battery_usage">Verder verminderd batterij verbruik</string>
|
||||
<string name="v4_6_group_welcome_message">Groep welkomst bericht</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Verborgen chat profielen</string>
|
||||
<string name="hide_profile">Profiel verbergen</string>
|
||||
<string name="user_hide">Verbergen</string>
|
||||
<string name="hidden_profile_password">Verborgen profiel wachtwoord</string>
|
||||
<string name="muted_when_inactive">Gedempt wanneer inactief!</string>
|
||||
<string name="user_mute">Dempen</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string>
|
||||
<string name="make_profile_private">Profiel privé maken!</string>
|
||||
<string name="v4_6_group_moderation_descr">Nu kunnen beheerders:
|
||||
\n- berichten van leden verwijderen.
|
||||
\n- schakel leden uit (\"waarnemer\" rol)</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Bescherm je chat profielen met een wachtwoord!</string>
|
||||
<string name="password_to_show">Wachtwoord om weer te geven</string>
|
||||
<string name="save_and_update_group_profile">Groep profiel opslaan en bijwerken</string>
|
||||
<string name="smp_save_servers_question">Servers opslaan\?</string>
|
||||
<string name="save_profile_password">Bewaar profiel wachtwoord</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Stel het getoonde bericht in voor nieuwe leden!</string>
|
||||
<string name="save_welcome_message_question">Welkomst bericht opslaan\?</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Ondersteuning voor bluetooth en andere verbeteringen.</string>
|
||||
<string name="tap_to_activate_profile">Tik om profiel te activeren.</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Dank aan de gebruikers – draag bij via Weblate!</string>
|
||||
<string name="should_be_at_least_one_profile">Er moet ten minste één gebruikers profiel zijn.</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">U kunt een gebruikers profiel verbergen of dempen - houd het vast voor het menu.</string>
|
||||
<string name="user_unhide">zichtbaar maken</string>
|
||||
<string name="user_unmute">Dempen opheffen</string>
|
||||
<string name="group_welcome_title">Welkomst bericht</string>
|
||||
<string name="should_be_at_least_one_visible_profile">"Er moet ten minste één zichtbaar gebruikers profiel zijn."</string>
|
||||
<string name="to_reveal_profile_enter_password">Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen.</string>
|
||||
<string name="button_welcome_message">Welkomst bericht</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn.</string>
|
||||
</resources>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -6,4 +6,433 @@
|
||||
<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>
|
||||
@@ -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>
|
||||
@@ -1039,4 +1041,58 @@
|
||||
<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>
|
||||
@@ -9,9 +9,9 @@
|
||||
<string name="accept_contact_button">接受</string>
|
||||
<string name="accept_call_on_lock_screen">接受</string>
|
||||
<string name="accept_feature">接受</string>
|
||||
<string name="chat_item_ttl_month">1月</string>
|
||||
<string name="chat_item_ttl_month">1个月</string>
|
||||
<string name="chat_item_ttl_week">1周</string>
|
||||
<string name="color_primary">强化</string>
|
||||
<string name="color_primary">色调</string>
|
||||
<string name="callstatus_accepted">已接受通话</string>
|
||||
<string name="accept">接受</string>
|
||||
<string name="network_enable_socks_info">通过 SOCKS 代理访问服务器在端口9050?允许该选项前必须开始代理。</string>
|
||||
@@ -38,7 +38,7 @@
|
||||
<string name="messages_section_title">消息</string>
|
||||
<string name="delete_messages_after">在此后删除消息</string>
|
||||
<string name="settings_section_title_messages">消息</string>
|
||||
<string name="users_add">添加资料</string>
|
||||
<string name="users_add">添加个人资料</string>
|
||||
<string name="users_delete_all_chats_deleted">所有聊天记录和消息将被删除——这一行为无法撤销!</string>
|
||||
<string name="clear_chat_warning">所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</string>
|
||||
<string name="allow_to_send_voice">允许发送语音消息。</string>
|
||||
@@ -67,7 +67,7 @@
|
||||
<string name="connect_via_contact_link">通过联系人链接连接?</string>
|
||||
<string name="connect_via_group_link">通过群组链接连接?</string>
|
||||
<string name="connect_via_link_or_qr">通过群组链接/二维码连接</string>
|
||||
<string name="connect_calls_via_relay">通过中继连接</string>
|
||||
<string name="always_use_relay">通过中继连接</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">允许您的联系人不可撤回地删除已发送消息。</string>
|
||||
<string name="chat_preferences_contact_allows">联系人允许</string>
|
||||
<string name="allow_voice_messages_only_if">仅有您的联系人许可后才允许语音消息。</string>
|
||||
@@ -99,7 +99,7 @@
|
||||
<string name="app_version_title">应用程序版本</string>
|
||||
<string name="full_backup">应用程序数据备份</string>
|
||||
<string name="settings_section_title_icon">应用程序图标</string>
|
||||
<string name="incognito_random_profile_from_contact_description">一个随机资料将被发送到收到您链接的联系人那里</string>
|
||||
<string name="incognito_random_profile_from_contact_description">随机配置文件将发送给您从中收到此链接的联系人</string>
|
||||
<string name="app_version_name">应用程序版本:v%s</string>
|
||||
<string name="notifications_mode_off_desc">仅在运行时应用程序可以接受通知,不会启动后台服务</string>
|
||||
<string name="incognito_random_profile_description">一个随机资料将发送给您的联系人</string>
|
||||
@@ -112,7 +112,7 @@
|
||||
<string name="integrity_msg_bad_hash">错误消息散列</string>
|
||||
<string name="integrity_msg_bad_id">错误消息 ID</string>
|
||||
<string name="settings_audio_video_calls">语音和视频通话</string>
|
||||
<string name="accept_automatically">自动地</string>
|
||||
<string name="accept_automatically">自动</string>
|
||||
<string name="turning_off_service_and_periodic">激活电池优化,关闭了后台服务和新消息的定期请求。您可以通过设置重新启用它们。</string>
|
||||
<string name="notifications_mode_service_desc">后台服务一直在运行——一旦有消息,就会显示通知。</string>
|
||||
<string name="icon_descr_audio_off">关闭音频</string>
|
||||
@@ -132,7 +132,7 @@
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b> 可以通过设置禁用它 </b> - 应用程序运行时仍会显示通知。</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b> 使用更多电池 </b>!后台服务一直在运行——一旦收到消息,就会显示通知。</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>请注意</b>:如果您丢失密码,您将无法恢复或者更改密码。</string>
|
||||
<string name="call_already_ended">通话已经结束!</string>
|
||||
<string name="call_already_ended">通话已结束!</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>扫描二维码</b> :与向您展示二维码的联系人联系。</string>
|
||||
<string name="alert_title_cant_invite_contacts">无法邀请联系人!</string>
|
||||
<string name="invite_prohibited">无法邀请联系人!</string>
|
||||
@@ -185,20 +185,20 @@
|
||||
<string name="incognito">隐身聊天</string>
|
||||
<string name="joining_group">加入群组</string>
|
||||
<string name="join_group_incognito_button">加入隐身聊天</string>
|
||||
<string name="settings_section_title_incognito">隐身聊天模式</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">这里不支持隐身聊天模式——您的主要资料将被发送给群组成员</string>
|
||||
<string name="settings_section_title_incognito">隐身模式</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">此处不支持隐身模式——您的主要个人资料将发送给群组成员</string>
|
||||
<string name="tap_to_start_new_chat">点击开始一个新聊天</string>
|
||||
<string name="incognito_random_profile">您的随机资料</string>
|
||||
<string name="description_via_contact_address_link_incognito">通过联系人地址链接隐身聊天</string>
|
||||
<string name="description_via_group_link_incognito">通过群组链接隐身聊天</string>
|
||||
<string name="description_via_contact_address_link_incognito">通过联系地址链接隐身</string>
|
||||
<string name="description_via_group_link_incognito">通过群组链接隐身</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">您分享了一次性链接隐身聊天</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">点击以加入隐身聊天</string>
|
||||
<string name="group_main_profile_sent">您的聊天资料将被发送给群组成员</string>
|
||||
<string name="invite_prohibited_description">您正在尝试邀请与您共享隐身聊天资料的联系人加入您使用主要资料的群组</string>
|
||||
<string name="incognito_info_protects">隐身聊天模式可以保护您的主要资料名和头像的隐私——为每个新联系人创建一个新的随机资料。</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">您正在为该群组使用隐身聊天资料——为防止共享您的主要资料,邀请联系人是不允许的</string>
|
||||
<string name="invite_prohibited_description">您正在尝试邀请与您共享隐身个人资料的联系人加入您使用主要个人资料的群组</string>
|
||||
<string name="incognito_info_protects">隐身模式可以保护你的主要个人资料名称和图像的隐私——对于每个新的联系人,都会创建一个新的随机个人资料。</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人</string>
|
||||
<string name="your_profile_will_be_sent">您的聊天资料将被发送给您的联系人</string>
|
||||
<string name="description_via_one_time_link_incognito">通过一次性链接隐身聊天</string>
|
||||
<string name="description_via_one_time_link_incognito">通过一次性链接隐身</string>
|
||||
<string name="only_group_owners_can_enable_voice">只有群主可以启用语音信息。</string>
|
||||
<string name="your_privacy">您的隐私设置</string>
|
||||
<string name="privacy_and_security">隐私和安全</string>
|
||||
@@ -218,7 +218,7 @@
|
||||
<string name="passphrase_is_different">数据库密码不同于保存在密钥库中的密码。</string>
|
||||
<string name="database_encryption_will_be_updated">数据库加密密码将被更新并存储在密钥库中。</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">数据库将被加密,密码存储在密钥库中。</string>
|
||||
<string name="restore_passphrase_not_found_desc">在密匙库中没有找到密码,请手动输入。如果你使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。</string>
|
||||
<string name="restore_passphrase_not_found_desc">在密匙库中没有找到密码,请手动输入。如果您使用备份工具恢复了应用程序的数据,可能会发生这种情况。如果不是这种情况,请联系开发者。</string>
|
||||
<string name="remove_passphrase_from_keychain">从密钥库中删除密码?</string>
|
||||
<string name="save_passphrase_in_keychain">在密钥库中保存密码</string>
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> 服务</string>
|
||||
@@ -234,7 +234,7 @@
|
||||
<string name="clear_chat_question">清除聊天记录?</string>
|
||||
<string name="chat_with_developers">与开发者聊天</string>
|
||||
<string name="clear_contacts_selection_button">清除</string>
|
||||
<string name="colored">有色</string>
|
||||
<string name="colored">彩色</string>
|
||||
<string name="callstate_connected">已连接</string>
|
||||
<string name="connect_button">连接</string>
|
||||
<string name="connect_via_link_verb">连接</string>
|
||||
@@ -266,7 +266,7 @@
|
||||
<string name="error_deleting_pending_contact_connection">删除待定的联系人连接错误</string>
|
||||
<string name="error_receiving_file">接收文件错误</string>
|
||||
<string name="failed_to_active_user_title">切换资料错误!</string>
|
||||
<string name="notification_preview_mode_hidden">已隐藏</string>
|
||||
<string name="notification_preview_mode_hidden">隐藏</string>
|
||||
<string name="edit_verb">编辑</string>
|
||||
<string name="notification_display_mode_hidden_desc">隐藏联系人和消息</string>
|
||||
<string name="icon_descr_edited">已编辑</string>
|
||||
@@ -335,7 +335,7 @@
|
||||
<string name="icon_descr_contact_checked">已检查联系人</string>
|
||||
<string name="notification_preview_somebody">联系人已隐藏:</string>
|
||||
<string name="alert_title_contact_connection_pending">联系人尚未连接!</string>
|
||||
<string name="icon_descr_context">背景图标</string>
|
||||
<string name="icon_descr_context">上下文图标</string>
|
||||
<string name="copied">复制到剪贴板</string>
|
||||
<string name="contact_connection_pending">连接中……</string>
|
||||
<string name="create_group_link">创建群组链接</string>
|
||||
@@ -373,7 +373,7 @@
|
||||
<string name="group_member_status_group_deleted">群组已删除</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">将为所有成员删除群组——此操作无法撤消!</string>
|
||||
<string name="conn_level_desc_direct">直接</string>
|
||||
<string name="direct_messages">私聊</string>
|
||||
<string name="direct_messages">直接信息</string>
|
||||
<string name="feature_enabled">已启用</string>
|
||||
<string name="group_members_can_send_voice">群组成员可以发送语音消息。</string>
|
||||
<string name="v4_2_group_links">群组链接</string>
|
||||
@@ -441,7 +441,7 @@
|
||||
<string name="enable_automatic_deletion_question">启用自动删除消息?</string>
|
||||
<string name="section_title_for_console">用于控制台</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">此群中禁止成员之间私聊。</string>
|
||||
<string name="disappearing_messages_are_prohibited">此群组中禁止显示限时消息。</string>
|
||||
<string name="disappearing_messages_are_prohibited">该组禁止限时消息。</string>
|
||||
<string name="group_members_can_delete">群组成员可以不可撤回地删除已发送的消息。</string>
|
||||
<string name="group_members_can_send_dms">群组成员可以私信。</string>
|
||||
<string name="v4_4_disappearing_messages">限时消息</string>
|
||||
@@ -463,7 +463,7 @@
|
||||
<string name="info_row_local_name">本地名称</string>
|
||||
<string name="invalid_message_format">无效的消息格式</string>
|
||||
<string name="invalid_data">无效数据</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="live">实时</string>
|
||||
<string name="display_name_invited_to_connect">已邀请连接</string>
|
||||
<string name="invalid_connection_link">无效的连接链接</string>
|
||||
<string name="italic">斜体</string>
|
||||
@@ -510,7 +510,7 @@
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">只有客户端设备存储用户配置文件、联系人、群组和使用 <b>双层端到端加密 </b> 发送的消息。</string>
|
||||
<string name="video_call_no_encryption">视频通话(非端到端加密)</string>
|
||||
<string name="onboarding_notifications_mode_periodic">定期</string>
|
||||
<string name="onboarding_notifications_mode_title">私人通知</string>
|
||||
<string name="onboarding_notifications_mode_title">私密通知</string>
|
||||
<string name="onboarding_notifications_mode_off">应用程序运行时</string>
|
||||
<string name="status_no_e2e_encryption">无端到端加密</string>
|
||||
<string name="show_call_on_lock_screen">显示</string>
|
||||
@@ -540,7 +540,7 @@
|
||||
<string name="notifications">通知</string>
|
||||
<string name="simplex_service_notification_text">正在接收消息……</string>
|
||||
<string name="enter_passphrase_notification_desc">要接收通知,请输入数据库密码</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>——它每天使用百分之几的电池。</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>——它每天使用百分之几的电池。</string>
|
||||
<string name="your_settings">您的设置</string>
|
||||
<string name="turn_off_battery_optimization">为了使用它,请 <b>禁用电池优化</b>为<xliff:g id="appName">SimpleX</xliff:g>在下一个对话框。否则通知将被禁用。</string>
|
||||
<string name="settings_notification_preview_title">通知预览</string>
|
||||
@@ -549,7 +549,7 @@
|
||||
<string name="notifications_mode_off">在应用程序打开时运行</string>
|
||||
<string name="settings_notification_preview_mode_title">显示预览</string>
|
||||
<string name="periodic_notifications_desc">该应用程序会定期获取新消息——它每天会消耗百分之几的电量。该应用程序不使用推送通知——您设备中的数据不会发送到服务器。</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">你的联系人可以允许完全删除消息。</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">您的联系人可以允许完全删除消息。</string>
|
||||
<string name="v4_4_disappearing_messages_desc">已发送的消息将在设定的时间后被删除。</string>
|
||||
<string name="your_chat_database">您的聊天数据库</string>
|
||||
<string name="wrong_passphrase_title">密码错误!</string>
|
||||
@@ -576,7 +576,7 @@
|
||||
<string name="update_network_settings_confirmation">更新</string>
|
||||
<string name="open_simplex_chat_to_accept_call">打开 <xliff:g id="appNameFull">SimpleX Chat</xliff:g> 来接听电话</string>
|
||||
<string name="icon_descr_video_call">视频通话</string>
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> 想通过该方式与您联系</string>
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> 想通过以下方式与您联系</string>
|
||||
<string name="icon_descr_call_rejected">拒接来电</string>
|
||||
<string name="call_connection_peer_to_peer">点对点</string>
|
||||
<string name="error_with_info">错误:%s</string>
|
||||
@@ -592,12 +592,12 @@
|
||||
<string name="your_chat_profiles">您的聊天资料</string>
|
||||
<string name="icon_descr_call_missed">未接来电</string>
|
||||
<string name="icon_descr_call_pending_sent">待定来电</string>
|
||||
<string name="your_chat_profiles_stored_locally">您的聊天资料存储在本地,只在您的设备上</string>
|
||||
<string name="your_chat_profiles_stored_locally">您的聊天资料存储在本地,仅存储在您的设备上</string>
|
||||
<string name="connection_error_auth_desc">除非您的联系人已删除此连接或此链接已被使用,否则它可能是一个错误——请报告。
|
||||
\n如果要连接,请让您的联系人创建另一个连接链接,并检查您的网络连接是否稳定。</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="your_chat_profile_will_be_sent_to_your_contact">您的聊天资料将被发送
|
||||
\n给你的联系人</string>
|
||||
\n给您的联系人</string>
|
||||
<string name="users_delete_with_connections">资料和服务器连接</string>
|
||||
<string name="update_network_settings_question">更新网络设置?</string>
|
||||
<string name="only_you_can_delete_messages">只有您可以不可撤回地删除消息(您的联系人可以将它们标记为删除)。</string>
|
||||
@@ -615,7 +615,7 @@
|
||||
<string name="rcv_group_event_updated_group_profile">已更新的群组资料</string>
|
||||
<string name="rcv_group_event_member_deleted">已删除 <xliff:g id="member profile" example="alice (Alice)">%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="profile_will_be_sent_to_contact_sending_link">您的资料将被发送到收到您链接的联系人那里。</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">您的个人资料将发送给您收到此链接的联系人。</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">正在尝试连接到用于从该联系人接收消息的服务器(错误:<xliff:g id="errorMsg">%1$s</xliff:g>)。</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">您已连接到用于接收该联系人消息的服务器。</string>
|
||||
<string name="description_you_shared_one_time_link">您分享了一次性链接</string>
|
||||
@@ -628,7 +628,7 @@
|
||||
\n<xliff:g id="appName">SimpleX</xliff:g> 服务器无法看见您的资料。</string>
|
||||
<string name="your_profile_is_stored_on_your_device">您的资料、联系人和发送的消息存储在您的设备上。</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">该资料仅与您的联系人共享。</string>
|
||||
<string name="chat_preferences_on">在</string>
|
||||
<string name="chat_preferences_on">开启</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。</string>
|
||||
<string name="messages_section_description">此设置适用于您当前聊天资料中的消息</string>
|
||||
<string name="database_restore_error">恢复数据库错误</string>
|
||||
@@ -644,4 +644,383 @@
|
||||
<string name="v4_5_multiple_chat_profiles">多个聊天资料</string>
|
||||
<string name="database_initialization_error_desc">数据库不能正常工作。点击了解更多</string>
|
||||
<string name="message_delivery_error_title">消息传递错误</string>
|
||||
<string name="ttl_hour">%d 小时</string>
|
||||
<string name="ttl_hours">%d 小时</string>
|
||||
<string name="ttl_month">%d 月</string>
|
||||
<string name="ttl_sec">%d 秒</string>
|
||||
<string name="how_simplex_works"><xliff:g id="appName">SimpleX</xliff:g> 是如何工作的</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">确保 WebRTC ICE 服务器地址格式正确、每行分开且不重复。</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">许多人问:<i>如果<xliff:g id="appName">SimpleX</xliff:g>没有用户标识符,它是怎样传递信息的?</i></string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">确保 SMP 服务器地址格式正确、每行分开且不重复。</string>
|
||||
<string name="markdown_help">Markdown 帮助</string>
|
||||
<string name="mark_code_verified">标记为已验证</string>
|
||||
<string name="make_private_connection">建立私密连接</string>
|
||||
<string name="group_preview_join_as">以 %s 身份加入</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="mark_read">标记为已读</string>
|
||||
<string name="mark_unread">标记为未读</string>
|
||||
<string name="markdown_in_messages">在消息中使用 Markdown</string>
|
||||
<string name="file_with_path">文件:%s</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">成员将被移出群组——此操作无法撤消!</string>
|
||||
<string name="v4_5_message_draft">消息草稿</string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="marked_deleted_description">标记为已删除</string>
|
||||
<string name="ttl_week">%d 星期</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">您将停止接收来自该群组的消息。聊天记录将被保留。</string>
|
||||
<string name="group_member_role_member">成员</string>
|
||||
<string name="member_info_section_title_member">成员</string>
|
||||
<string name="ttl_weeks">%d 星期</string>
|
||||
<string name="ttl_min">%d 分钟</string>
|
||||
<string name="ttl_months">%d 月</string>
|
||||
<string name="network_and_servers">网络和服务器</string>
|
||||
<string name="network_settings_title">网络设置</string>
|
||||
<string name="moderated_description">已被管理员移除</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Onion 主机将在可用时使用。</string>
|
||||
<string name="network_use_onion_hosts_no_desc">将不会使用 Onion 主机。</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 手机:点击<b>在手机应用程序中打开</b>,然后在应用程序中点击<b>连接</b>。</string>
|
||||
<string name="delete_message_mark_deleted_warning">消息将被标记为删除。收件人将能够揭示此消息。</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">更多改进即将推出!</string>
|
||||
<string name="no_contacts_selected">未选择联系人</string>
|
||||
<string name="one_time_link">一次性邀请链接</string>
|
||||
<string name="feature_off">关闭</string>
|
||||
<string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion 主机将在可用时使用。</string>
|
||||
<string name="chat_item_ttl_none">从不</string>
|
||||
<string name="feature_offered_item">已提供 %s</string>
|
||||
<string name="feature_offered_item_with_param">已提供 %s:%2s</string>
|
||||
<string name="new_in_version">%s 中的新功能</string>
|
||||
<string name="add_contact">一次性邀请链接</string>
|
||||
<string name="ok">好的</string>
|
||||
<string name="no_details">没有细节</string>
|
||||
<string name="only_stored_on_members_devices">(仅由群组成员存储)</string>
|
||||
<string name="only_you_can_send_voice">只有您可以发送语音消息。</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">消息将被删除——此操作无法撤消!</string>
|
||||
<string name="images_limit_desc">一次只能发送10张图片</string>
|
||||
<string name="icon_descr_more_button">更多</string>
|
||||
<string name="new_database_archive">新数据库存档</string>
|
||||
<string name="old_database_archive">旧数据库存档</string>
|
||||
<string name="new_member_role">新成员角色</string>
|
||||
<string name="no_contacts_to_add">没有联系人可添加</string>
|
||||
<string name="network_status">网络状态</string>
|
||||
<string name="chat_preferences_off">关闭</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">将不会使用 Onion 主机。</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">连接需要 Onion 主机。</string>
|
||||
<string name="no_received_app_files">没有收到或发送的文件</string>
|
||||
<string name="sender_cancelled_file_transfer">发送人已取消文件传输。</string>
|
||||
<string name="share_verb">分享</string>
|
||||
<string name="send_live_message">发送实时消息</string>
|
||||
<string name="this_text_is_available_in_settings">此文本在设置中可用</string>
|
||||
<string name="icon_descr_received_msg_status_unread">未读</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">保存的 WebRTC ICE 服务器将被删除。</string>
|
||||
<string name="is_verified">%s 已验证</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">用于新连接</string>
|
||||
<string name="network_disable_socks">使用直接互联网连接?</string>
|
||||
<string name="network_use_onion_hosts_required">必须</string>
|
||||
<string name="save_and_notify_contact">保存并通知联系人</string>
|
||||
<string name="save_and_notify_contacts">保存并通知联系人</string>
|
||||
<string name="read_more_in_github">在我们的 GitHub 仓库中阅读更多内容。</string>
|
||||
<string name="reject">拒绝</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">为了保护隐私,而不是所有其他平台使用的用户 ID,<xliff:g id="appName">SimpleX</xliff:g> 具有消息队列的标识符,每个联系人都是分开的。</string>
|
||||
<string name="network_option_tcp_connection_timeout">TCP 连接超时</string>
|
||||
<string name="feature_received_prohibited">收到,禁止</string>
|
||||
<string name="accept_feature_set_1_day">设定1天</string>
|
||||
<string name="v4_2_security_assessment_desc">SimpleX Chat 安全性由 Trail of Bits 审核。</string>
|
||||
<string name="simplex_link_mode_browser_warning">在浏览器中打开链接可能会降低连接的隐私和安全性。SimpleX 上不受信任的链接将显示为红色。</string>
|
||||
<string name="restore_database_alert_desc">恢复数据库备份后请输入之前的密码。 此操作无法撤消。</string>
|
||||
<string name="contact_developers">请更新应用程序并联系开发者。</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">开源协议和代码——任何人都可以运行服务器。</string>
|
||||
<string name="paste_button">粘贴</string>
|
||||
<string name="network_option_ping_count">PING 次数</string>
|
||||
<string name="prohibit_sending_voice">禁止发送语音消息。</string>
|
||||
<string name="network_option_ping_interval">PING 间隔</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。</string>
|
||||
<string name="network_option_protocol_timeout">协议超时</string>
|
||||
<string name="reject_contact_button">拒绝</string>
|
||||
<string name="reply_verb">回复</string>
|
||||
<string name="network_options_reset_to_defaults">重置为默认</string>
|
||||
<string name="run_chat_section">运行聊天程序</string>
|
||||
<string name="save_archive">保存存档</string>
|
||||
<string name="scan_code">扫码</string>
|
||||
<string name="scan_code_from_contacts_app">从您联系人的应用程序中扫描安全码。</string>
|
||||
<string name="security_code">安全码</string>
|
||||
<string name="secret">秘密</string>
|
||||
<string name="v4_2_security_assessment">安全评估</string>
|
||||
<string name="ntf_channel_messages">SimpleX 消息</string>
|
||||
<string name="is_not_verified">%s 未验证</string>
|
||||
<string name="v4_5_italian_interface_descr">感谢用户——通过 Weblate 做出贡献!</string>
|
||||
<string name="first_platform_without_user_ids">第一个没有任何用户标识符的平台——专为隐私保护设计。</string>
|
||||
<string name="group_is_decentralized">该小组是完全分散式的——它只对成员可见。</string>
|
||||
<string name="image_decoding_exception_desc">图像无法解码。 请尝试不同的图像或联系开发者。</string>
|
||||
<string name="theme">主题</string>
|
||||
<string name="delete_files_and_media_desc">此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">角色将更改为“%s”。 该成员将收到新的邀请。</string>
|
||||
<string name="enable_automatic_deletion_message">此操作无法撤消——早于所选的发送和接收的消息将被删除。 这可能需要几分钟时间。</string>
|
||||
<string name="this_QR_code_is_not_a_link">此二维码不是链接!</string>
|
||||
<string name="switch_receiving_address_desc">此功能是实验性的! 它仅在其他客户端安装了 4.2 版时才有效。 地址更改完成后,您应该会在对话中看到该消息——请检查您是否仍能收到来自该联系人(或群组成员)的消息。</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">此链接不是有效的连接链接!</string>
|
||||
<string name="to_start_a_new_chat_help_header">开始新的聊天</string>
|
||||
<string name="to_verify_compare">要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。</string>
|
||||
<string name="unmute_chat">取消静音</string>
|
||||
<string name="update_onion_hosts_settings_question">更新 .onion 主机设置?</string>
|
||||
<string name="update_network_session_mode_question">更新传输隔离模式?</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(从剪贴板扫描或粘贴)</string>
|
||||
<string name="smp_server_test_secure_queue">保护队列</string>
|
||||
<string name="reveal_verb">揭示</string>
|
||||
<string name="la_notice_turn_on">打开</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">发送失败</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">未经授权发送</string>
|
||||
<string name="images_limit_title">太多图片!</string>
|
||||
<string name="icon_descr_server_status_pending">待办的</string>
|
||||
<string name="switch_receiving_address_question">切换接收地址吗?</string>
|
||||
<string name="ask_your_contact_to_enable_voice">请让您的联系人启用发送语音消息。</string>
|
||||
<string name="icon_descr_record_voice_message">录制语音消息</string>
|
||||
<string name="icon_descr_send_message">发消息</string>
|
||||
<string name="reset_verb">重置</string>
|
||||
<string name="send_verb">发送</string>
|
||||
<string name="send_live_message_desc">发送实时消息——它会在您键入时为收件人更新</string>
|
||||
<string name="add_contact_or_create_group">开始新聊天</string>
|
||||
<string name="to_share_with_your_contact">(与您的联系人分享)</string>
|
||||
<string name="to_connect_via_link_title">通过链接连接</string>
|
||||
<string name="set_contact_name">设置联系人姓名</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="show_QR_code">显示二维码</string>
|
||||
<string name="chat_with_the_founder">发送问题和想法</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">保护您的隐私和安全的消息传递和应用程序平台。</string>
|
||||
<string name="strikethrough">删去</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">人们只能通过您共享的链接与您建立联系。</string>
|
||||
<string name="next_generation_of_private_messaging">下一代私密通讯软件</string>
|
||||
<string name="paste_the_link_you_received">粘贴收到的链接</string>
|
||||
<string name="alert_title_skipped_messages">已跳过消息</string>
|
||||
<string name="settings_section_title_support">支持 SIMPLEX CHAT</string>
|
||||
<string name="send_link_previews">发送链接预览</string>
|
||||
<string name="settings_section_title_socks">SOCKS 代理</string>
|
||||
<string name="stop_chat_question">停止聊天程序?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">停止聊天以便导出、导入或删除聊天数据库。在聊天停止期间,您将无法收发消息。</string>
|
||||
<string name="restore_database">恢复数据库备份</string>
|
||||
<string name="restore_database_alert_title">恢复数据库备份?</string>
|
||||
<string name="button_remove_member">删除成员</string>
|
||||
<string name="button_send_direct_message">发送私信</string>
|
||||
<string name="receiving_via">接收通过</string>
|
||||
<string name="sending_via">发送通过</string>
|
||||
<string name="conn_stats_section_title_servers">服务器</string>
|
||||
<string name="switch_receiving_address">切换接收地址</string>
|
||||
<string name="v4_5_message_draft_descr">保留最后的消息草稿及其附件。</string>
|
||||
<string name="v4_5_private_filenames">私密文件名</string>
|
||||
<string name="v4_4_french_interface_descr">感谢用户——通过 Weblate 做出贡献!</string>
|
||||
<string name="v4_5_transport_isolation">传输隔离</string>
|
||||
<string name="paste_connection_link_below_to_connect">将您收到的链接粘贴到下面的框中以与您的联系人联系。</string>
|
||||
<string name="share_invitation_link">分享邀请链接</string>
|
||||
<string name="this_string_is_not_a_connection_link">此字符串不是连接链接!</string>
|
||||
<string name="send_us_an_email">给我们发电子邮件</string>
|
||||
<string name="smp_servers">SMP 服务器</string>
|
||||
<string name="sending_files_not_yet_supported">尚不支持发送文件</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">正在尝试连接到用于从该联系人接收消息的服务器。</string>
|
||||
<string name="receiving_files_not_yet_supported">尚不支持接收文件</string>
|
||||
<string name="unknown_message_format">未知消息格式</string>
|
||||
<string name="smp_servers_test_server">测试服务器</string>
|
||||
<string name="smp_servers_test_servers">测试服务器</string>
|
||||
<string name="simplex_link_contact">SimpleX 联系地址</string>
|
||||
<string name="simplex_link_invitation">SimpleX 一次性邀请</string>
|
||||
<string name="simplex_link_group">SimpleX 群组链接</string>
|
||||
<string name="simplex_link_mode">SimpleX 链接</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">发送人可能已删除连接请求。</string>
|
||||
<string name="smp_servers_preset_server">预设服务器</string>
|
||||
<string name="image_descr_qr_code">二维码</string>
|
||||
<string name="network_session_mode_transport_isolation">传输隔离</string>
|
||||
<string name="share_link">分享链接</string>
|
||||
<string name="select_contacts">选择联系人</string>
|
||||
<string name="skip_inviting_button">跳过邀请成员</string>
|
||||
<string name="switch_verb">转变</string>
|
||||
<string name="prohibit_sending_voice_messages">禁止发送语音消息。</string>
|
||||
<string name="only_your_contact_can_send_voice">只有您的联系人可以发送语音消息。</string>
|
||||
<string name="prohibit_direct_messages">禁止直接向成员发送私信。</string>
|
||||
<string name="protect_app_screen">保护应用程序屏幕</string>
|
||||
<string name="settings_section_title_themes">主题</string>
|
||||
<string name="stop_chat_to_enable_database_actions">停止聊天以启用数据库操作。</string>
|
||||
<string name="chat_item_ttl_seconds">%s 秒</string>
|
||||
<string name="alert_message_no_group">该群组已不存在。</string>
|
||||
<string name="group_invitation_tap_to_join">点击加入</string>
|
||||
<string name="stop_chat_confirmation">停止</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">重新启动应用程序以使用导入的聊天数据库。</string>
|
||||
<string name="group_member_role_owner">群主</string>
|
||||
<string name="group_member_status_removed">已删除</string>
|
||||
<string name="role_in_group">角色</string>
|
||||
<string name="network_option_seconds_label">秒</string>
|
||||
<string name="network_options_revert">恢复</string>
|
||||
<string name="reset_color">重置颜色</string>
|
||||
<string name="save_color">保存颜色</string>
|
||||
<string name="v4_5_reduced_battery_usage">减少电池使用量</string>
|
||||
<string name="v4_5_private_filenames_descr">为了保护时区,图像/语音文件使用 UTC。</string>
|
||||
<string name="use_chat">使用聊天</string>
|
||||
<string name="read_more_in_github_with_link">在我们的 <font color="#0088ff">GitHub 存储库</font> 中阅读更多内容。</string>
|
||||
<string name="auth_open_chat_console">打开聊天控制台</string>
|
||||
<string name="auth_stop_chat">Stop chat</string>
|
||||
<string name="toast_permission_denied">权限被拒绝!</string>
|
||||
<string name="chat_help_tap_button">点击按钮</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="smp_servers_preset_address">预设服务器地址</string>
|
||||
<string name="smp_servers_scan_qr">扫描服务器二维码</string>
|
||||
<string name="smp_servers_test_failed">服务器测试失败!</string>
|
||||
<string name="smp_servers_test_some_failed">一些服务器未通过测试:</string>
|
||||
<string name="star_on_github">在 GitHub 上加星</string>
|
||||
<string name="save_and_notify_group_members">保存并通知群组成员</string>
|
||||
<string name="icon_descr_speaker_off">扬声器关闭</string>
|
||||
<string name="icon_descr_speaker_on">扬声器开启</string>
|
||||
<string name="rcv_group_event_user_deleted">已将您移除</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">更新设置会将客户端重新连接到所有服务器。</string>
|
||||
<string name="theme_system">系统</string>
|
||||
<string name="v4_4_live_messages_desc">对方会在您键入时看到更新。</string>
|
||||
<string name="view_security_code">查看安全码</string>
|
||||
<string name="voice_message_with_duration">语音消息 (<xliff:g id="duration">%1$s</xliff:g>)</string>
|
||||
<string name="waiting_for_image">等待图像中</string>
|
||||
<string name="welcome">欢迎!</string>
|
||||
<string name="personal_welcome">欢迎 <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">当您的联系人设备在线时,您将可以连接,请稍等或稍后查看!</string>
|
||||
<string name="rate_the_app">评价此应用程序</string>
|
||||
<string name="network_enable_socks">使用 SOCKS 代理?</string>
|
||||
<string name="network_socks_toggle">使用 SOCKS 代理(端口 9050)</string>
|
||||
<string name="total_files_count_and_size">%d 个文件,总大小为 %s</string>
|
||||
<string name="you_joined_this_group">您已加入此群组</string>
|
||||
<string name="you_are_invited_to_group">您被邀请加入群组</string>
|
||||
<string name="chat_preferences_default">默认(%s)</string>
|
||||
<string name="voice_prohibited_in_this_chat">此聊天中禁止语音消息。</string>
|
||||
<string name="voice_messages_are_prohibited">语音信息在该群组中被禁用。</string>
|
||||
<string name="verify_security_code">验证安全码</string>
|
||||
<string name="using_simplex_chat_servers">使用 <xliff:g id="appNameFull">SimpleX Chat</xliff:g> 服务器。</string>
|
||||
<string name="simplex_link_connection">通过 <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
<string name="group_invitation_item_description">邀请至群组 <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> 地址</string>
|
||||
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> 团队</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> 成员</string>
|
||||
<string name="chat_preferences_yes">是</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">您可以将您的地址作为链接或二维码共享——任何人都可以连接到您。 如果您以后删除它,您不会丢失您的联系人。</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">您可以控制通过哪些服务器<b>接收</b>消息,您的联系人 - 您用来向他们发送消息的服务器。</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">您的联系人可以从应用程序中扫描二维码。</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">您将在组主设备上线时连接到该群组,请稍等或稍后再检查!</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">当您启动应用或在应用程序驻留后台超过30 秒后,您将需要进行身份验证。</string>
|
||||
<string name="archive_created_on_ts">创建于 <xliff:g id="archive_ts">%1$s</xliff:g></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="you_accepted_connection">您已接受连接</string>
|
||||
<string name="your_SMP_servers">您的 SMP 服务器</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> 已跳过消息</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="whats_new">更新内容</string>
|
||||
<string name="group_preview_you_are_invited">您被邀请加入群组</string>
|
||||
<string name="you_have_no_chats">您没有聊天记录</string>
|
||||
<string name="icon_descr_waiting_for_image">等待图像中</string>
|
||||
<string name="voice_message">语音消息</string>
|
||||
<string name="voice_messages_prohibited">语音消息禁止发送!</string>
|
||||
<string name="you_need_to_allow_to_send_voice">您需要允许您的联系人发送语音消息才能发送它们。</string>
|
||||
<string name="scan_QR_code">扫描二维码</string>
|
||||
<string name="you_invited_your_contact">您邀请了您的联系人</string>
|
||||
<string name="contact_wants_to_connect_with_you">想要与您连接!</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">您的联系人需要在线才能完成连接。
|
||||
\n您可以取消此连接并删除联系人(稍后尝试使用新链接)。</string>
|
||||
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> 标志</string>
|
||||
<string name="your_simplex_contact_address">您的 <xliff:g id="appName">SimpleX</xliff:g> 联系地址</string>
|
||||
<string name="install_simplex_chat_for_terminal">为终端安装 <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="use_simplex_chat_servers__question">使用 <xliff:g id="appNameFull">SimpleX Chat</xliff:g> 服务器?</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">我们不会在服务器上存储您的任何联系人或消息(一旦发送)。</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE 服务器</string>
|
||||
<string name="relay_server_protects_ip">中继服务器保护您的 IP 地址,但它可以观察通话的持续时间。</string>
|
||||
<string name="relay_server_if_necessary">中继服务器仅在必要时使用。其他人可能会观察到您的IP地址。</string>
|
||||
<string name="your_ice_servers">您的 ICE 服务器</string>
|
||||
<string name="icon_descr_video_off">视频关闭</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">您可以通过应用设置/数据库或重启应用开始聊天。</string>
|
||||
<string name="snd_group_event_changed_member_role">您将 %s 的角色更改为 %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">您将自己的角色更改为 %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">您已更改地址</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">您可以共享链接或二维码——任何人都可以加入该群组。如果您稍后将其删除,您不会失去该组的成员。</string>
|
||||
<string name="conn_level_desc_indirect">间接(<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">您也可以通过点击链接进行连接。 如果它在浏览器中打开,请单击<b>在移动应用程序中打开</b>按钮。</string>
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="you_will_join_group">您将加入此链接指向的群组并连接到其群组成员。</string>
|
||||
<string name="description_via_group_link">通过群组链接</string>
|
||||
<string name="description_via_one_time_link">通过一次性链接</string>
|
||||
<string name="description_via_contact_address_link">通过联系地址链接</string>
|
||||
<string name="simplex_link_mode_browser">通过浏览器</string>
|
||||
<string name="smp_servers_your_server">您的服务器</string>
|
||||
<string name="network_use_onion_hosts_prefer">当可用时</string>
|
||||
<string name="network_use_onion_hosts">使用 .onion 主机</string>
|
||||
<string name="your_ICE_servers">您的 ICE 服务器</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="section_title_welcome_message">欢迎消息</string>
|
||||
<string name="you_control_your_chat">您的聊天由您掌控!</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">您可以使用 markdown 来编排消息格式:</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="ttl_days">%d 天</string>
|
||||
<string name="ttl_w">%dw</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="snd_conn_event_switch_queue_phase_completed_for_member">您更改了 %s 的地址</string>
|
||||
<string name="snd_group_event_user_left">您已离开</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="connection ID" example="1">%1$d</xliff:g> 已选择联系人</string>
|
||||
<string name="chat_preferences_you_allow">您允许</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">带有可选的欢迎消息。</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="waiting_for_file">等待文件中</string>
|
||||
<string name="contact_sent_large_file">您的联系人发送的文件大于当前支持的最大大小 (<xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"maxFileSize\">%1$s</xliff :g>).</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">当您的连接请求被接受后,您将可以连接,请稍等或稍后检查!</string>
|
||||
<string name="smp_servers_use_server">使用服务器</string>
|
||||
<string name="smp_servers_your_server_address">您的服务器地址</string>
|
||||
<string name="icon_descr_video_on">视频开启</string>
|
||||
<string name="v4_3_voice_messages_desc">最多 40 秒,立即收到。</string>
|
||||
<string name="v4_4_verify_connection_security">验证连接安全</string>
|
||||
<string name="moderated_item_description">由 %s 审核</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="delete_member_message__question">删除成员消息?</string>
|
||||
<string name="group_member_role_observer">观察者</string>
|
||||
<string name="you_are_observer">您是观察者</string>
|
||||
<string name="error_updating_link_for_group">更新群组链接错误</string>
|
||||
<string name="observer_cant_send_message_title">您无法发送消息!</string>
|
||||
<string name="initial_member_role">初始角色</string>
|
||||
<string name="observer_cant_send_message_desc">请联系群组管理员。</string>
|
||||
<string name="language_system">系统</string>
|
||||
<string name="password_to_show">用于显示的密码</string>
|
||||
<string name="save_profile_password">保存个人资料密码</string>
|
||||
<string name="button_add_welcome_message">添加欢迎消息</string>
|
||||
<string name="user_hide">隐藏</string>
|
||||
<string name="make_profile_private">将个人资料设置为私密!</string>
|
||||
<string name="user_mute">静音</string>
|
||||
<string name="save_and_update_group_profile">保存并更新组配置文件</string>
|
||||
<string name="dont_show_again">不再显示</string>
|
||||
<string name="muted_when_inactive">不活跃时静音!</string>
|
||||
<string name="v4_6_audio_video_calls">语音和视频通话</string>
|
||||
<string name="v4_6_chinese_spanish_interface">中文和西班牙文界面</string>
|
||||
<string name="v4_6_reduced_battery_usage">进一步减少电池使用</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="confirm_password">确认密码</string>
|
||||
<string name="error_updating_user_privacy">更新用户隐私错误</string>
|
||||
<string name="cant_delete_user_profile">无法删除用户资料!</string>
|
||||
<string name="error_saving_user_password">保存用户密码错误</string>
|
||||
<string name="enter_password_to_show">在搜索中输入密码</string>
|
||||
<string name="v4_6_group_welcome_message">群组欢迎消息</string>
|
||||
<string name="v4_6_group_moderation">群组管理员移除</string>
|
||||
<string name="hidden_profile_password">隐藏的个人资料密码</string>
|
||||
<string name="v4_6_hidden_chat_profiles">隐藏的聊天资料</string>
|
||||
<string name="hide_profile">隐藏个人资料</string>
|
||||
<string name="smp_save_servers_question">保存服务器?</string>
|
||||
<string name="to_reveal_profile_enter_password">要显示您的隐藏的个人资料,请在您的聊天个人资料页面的搜索字段中输入完整密码。</string>
|
||||
<string name="save_welcome_message_question">保存欢迎信息?</string>
|
||||
<string name="tap_to_activate_profile">点击以激活个人资料。</string>
|
||||
<string name="should_be_at_least_one_profile">应该至少有一个用户资料。</string>
|
||||
<string name="user_unhide">解除隐藏</string>
|
||||
<string name="v4_6_group_welcome_message_descr">设置向新成员显示的消息!</string>
|
||||
<string name="v4_6_audio_video_calls_descr">支持蓝牙和其他改进。</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">感谢用户——通过 Weblate 做出贡献!</string>
|
||||
<string name="should_be_at_least_one_visible_profile">应该至少有一个可见的用户资料。</string>
|
||||
<string name="user_unmute">解除静音</string>
|
||||
<string name="button_welcome_message">欢迎信息</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">您可以隐藏或静音用户配置文件——长按以显示菜单。</string>
|
||||
<string name="group_welcome_title">欢迎信息</string>
|
||||
</resources>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,4 +2,6 @@
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="highOrLowLight">#8b8786</color>
|
||||
<color name="window_background_dark">#121212</color>
|
||||
</resources>
|
||||
@@ -22,6 +22,7 @@
|
||||
<!-- 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>
|
||||
@@ -102,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>
|
||||
@@ -182,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>
|
||||
|
||||
@@ -222,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>
|
||||
@@ -451,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>
|
||||
@@ -527,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>
|
||||
@@ -623,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>
|
||||
@@ -688,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>
|
||||
@@ -868,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>
|
||||
@@ -890,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>
|
||||
@@ -911,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>
|
||||
@@ -919,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>
|
||||
|
||||
@@ -947,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>
|
||||
@@ -983,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>
|
||||
@@ -1007,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>
|
||||
@@ -1133,4 +1182,16 @@
|
||||
<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>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en"/>
|
||||
<locale android:name="ru"/>
|
||||
<locale android:name="de"/>
|
||||
<locale android:name="fr"/>
|
||||
<locale android:name="it"/>
|
||||
<locale android:name="nl"/>
|
||||
<locale android:name="cs"/>
|
||||
<locale android:name="zh-CN"/>
|
||||
</locale-config>
|
||||
@@ -8,6 +8,7 @@ buildscript {
|
||||
compose_version = localProperties['compose_version'] ?: '1.2.0-beta02'
|
||||
kotlin_version = localProperties['kotlin_version'] ?: '1.6.21'
|
||||
gradle_plugin_version = localProperties['gradle_plugin_version'] ?: '7.2.0'
|
||||
abi_filter = localProperties['abi_filter'] ?: 'arm64-v8a'
|
||||
|
||||
// Name that will be shown for debug build. By default it is from strings
|
||||
app_name = localProperties['app_name'] ?: "@string/app_name"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,18 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Intents
|
||||
import SimpleXChat
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var alertManager = AlertManager.shared
|
||||
@ObservedObject var callController = CallController.shared
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Binding var doAuthenticate: Bool
|
||||
@Binding var userAuthorized: Bool?
|
||||
@Binding var canConnectCall: Bool
|
||||
@Binding var lastSuccessfulUnlock: TimeInterval?
|
||||
@AppStorage(DEFAULT_SHOW_LA_NOTICE) private var prefShowLANotice = false
|
||||
@AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@@ -23,41 +27,64 @@ struct ContentView: View {
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if prefPerformLA && userAuthorized != true {
|
||||
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
|
||||
} else if let status = chatModel.chatDbStatus, status != .ok {
|
||||
DatabaseErrorView(status: status)
|
||||
} else if !chatModel.v3DBMigration.startChat {
|
||||
MigrateToAppGroupView()
|
||||
} else if let step = chatModel.onboardingStage {
|
||||
if case .onboardingComplete = step,
|
||||
chatModel.currentUser != nil {
|
||||
mainView().privacySensitive(protectScreen)
|
||||
} else {
|
||||
OnboardingView(onboarding: step)
|
||||
}
|
||||
contentView()
|
||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||
callView(call)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if doAuthenticate { runAuthenticate() }
|
||||
if prefPerformLA { requestNtfAuthorization() }
|
||||
initAuthenticate()
|
||||
}
|
||||
.onChange(of: doAuthenticate) { _ in
|
||||
initAuthenticate()
|
||||
}
|
||||
.onChange(of: doAuthenticate) { _ in if doAuthenticate { runAuthenticate() } }
|
||||
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
|
||||
}
|
||||
|
||||
@ViewBuilder private func contentView() -> some View {
|
||||
if prefPerformLA && userAuthorized != true {
|
||||
lockButton()
|
||||
} else if let status = chatModel.chatDbStatus, status != .ok {
|
||||
DatabaseErrorView(status: status)
|
||||
} else if !chatModel.v3DBMigration.startChat {
|
||||
MigrateToAppGroupView()
|
||||
} else if let step = chatModel.onboardingStage {
|
||||
if case .onboardingComplete = step,
|
||||
chatModel.currentUser != nil {
|
||||
mainView()
|
||||
} else {
|
||||
OnboardingView(onboarding: step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func callView(_ call: Call) -> some View {
|
||||
if CallController.useCallKit() {
|
||||
ActiveCallView(call: call, canConnectCall: Binding.constant(true))
|
||||
.onDisappear {
|
||||
if userAuthorized == false && doAuthenticate { runAuthenticate() }
|
||||
}
|
||||
} else {
|
||||
ActiveCallView(call: call, canConnectCall: $canConnectCall)
|
||||
if prefPerformLA && userAuthorized != true {
|
||||
Rectangle()
|
||||
.fill(colorScheme == .dark ? .black : .white)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
lockButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func lockButton() -> some View {
|
||||
Button(action: runAuthenticate) { Label("Unlock", systemImage: "lock") }
|
||||
}
|
||||
|
||||
private func mainView() -> some View {
|
||||
ZStack(alignment: .top) {
|
||||
ChatListView()
|
||||
ChatListView().privacySensitive(protectScreen)
|
||||
.onAppear {
|
||||
NtfManager.shared.requestAuthorization(
|
||||
onDeny: {
|
||||
if (!notificationAlertShown) {
|
||||
notificationAlertShown = true
|
||||
alertManager.showAlert(notificationAlert())
|
||||
}
|
||||
},
|
||||
onAuthorized: { notificationAlertShown = false }
|
||||
)
|
||||
if !prefPerformLA { requestNtfAuthorization() }
|
||||
// Local Authentication notice is to be shown on next start after onboarding is complete
|
||||
if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
|
||||
prefLANoticeShown = true
|
||||
@@ -74,11 +101,42 @@ struct ContentView: View {
|
||||
.sheet(isPresented: $showWhatsNew) {
|
||||
WhatsNewView()
|
||||
}
|
||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||
ActiveCallView(call: call)
|
||||
}
|
||||
IncomingCallView()
|
||||
}
|
||||
.onContinueUserActivity("INStartCallIntent", perform: processUserActivity)
|
||||
.onContinueUserActivity("INStartAudioCallIntent", perform: processUserActivity)
|
||||
.onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity)
|
||||
}
|
||||
|
||||
private func processUserActivity(_ activity: NSUserActivity) {
|
||||
let intent = activity.interaction?.intent
|
||||
if let intent = intent as? INStartCallIntent {
|
||||
callToRecentContact(intent.contacts, intent.callCapability == .videoCall ? .video : .audio)
|
||||
} else if let intent = intent as? INStartAudioCallIntent {
|
||||
callToRecentContact(intent.contacts, .audio)
|
||||
} else if let intent = intent as? INStartVideoCallIntent {
|
||||
callToRecentContact(intent.contacts, .video)
|
||||
}
|
||||
}
|
||||
|
||||
private func callToRecentContact(_ contacts: [INPerson]?, _ mediaType: CallMediaType) {
|
||||
logger.debug("callToRecentContact")
|
||||
if let contactId = contacts?.first?.personHandle?.value,
|
||||
let chat = chatModel.getChat(contactId),
|
||||
case let .direct(contact) = chat.chatInfo {
|
||||
logger.debug("callToRecentContact: schedule call")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
CallController.shared.startCall(contact, mediaType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func initAuthenticate() {
|
||||
if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
|
||||
userAuthorized = false
|
||||
} else if doAuthenticate {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
|
||||
private func runAuthenticate() {
|
||||
@@ -98,16 +156,31 @@ struct ContentView: View {
|
||||
switch (laResult) {
|
||||
case .success:
|
||||
userAuthorized = true
|
||||
canConnectCall = true
|
||||
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
|
||||
case .failed:
|
||||
break
|
||||
case .unavailable:
|
||||
userAuthorized = true
|
||||
prefPerformLA = false
|
||||
canConnectCall = true
|
||||
AlertManager.shared.showAlert(laUnavailableTurningOffAlert())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requestNtfAuthorization() {
|
||||
NtfManager.shared.requestAuthorization(
|
||||
onDeny: {
|
||||
if (!notificationAlertShown) {
|
||||
notificationAlertShown = true
|
||||
alertManager.showAlert(notificationAlert())
|
||||
}
|
||||
},
|
||||
onAuthorized: { notificationAlertShown = false }
|
||||
)
|
||||
}
|
||||
|
||||
func laNoticeAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("SimpleX Lock"),
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import UIKit
|
||||
import SimpleXChat
|
||||
|
||||
hs_init(0, nil)
|
||||
|
||||
let totalBytes = 20
|
||||
let ivTagBytes = 28
|
||||
var base: UnsafeMutableRawPointer = malloc(totalBytes)
|
||||
|
||||
let assume = base.assumingMemoryBound(to: UInt8.self)
|
||||
assume[0] = 0 // key frame
|
||||
for i in 1..<totalBytes {
|
||||
assume[i] = UInt8(i)
|
||||
}
|
||||
let unencrypted = NSData(bytesNoCopy: base, length: totalBytes)
|
||||
let aesKey = "PI-bV-FTgRqZM_lsDH9T21a0yRVMsvLFmvilJ9Ssk3g="
|
||||
|
||||
if var key: [CChar] = aesKey.cString(using: .utf8),
|
||||
let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + ivTagBytes) {
|
||||
debugPrint("AesKey \(aesKey), cString \(key)")
|
||||
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
|
||||
let source_ = Data(bytes: pointer, count: unencrypted.count)
|
||||
//let raw: UInt8 = (unencrypted[0] as UInt8) | ((unencrypted[1] as UInt8) << 8) | ((unencrypted[2] as UInt8) << 16)
|
||||
let isKeyFrame = unencrypted[0] & 1 == 0
|
||||
debugPrint("Is key frame \(isKeyFrame)")
|
||||
let clearTextBytesSize = isKeyFrame ? 10 : 3
|
||||
for i in 0..<48 {
|
||||
debugPrint("Before \(i) \(unencrypted[i])")
|
||||
}
|
||||
if let res = chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + ivTagBytes - clearTextBytesSize)) {
|
||||
printError("encrypt", res)
|
||||
}
|
||||
for i in 0..<48 {
|
||||
debugPrint("After \(i) \(pointer.assumingMemoryBound(to: UInt8.self)[i])")
|
||||
}
|
||||
let res_ = Data(bytes: pointer, count: unencrypted.count + ivTagBytes)
|
||||
print(source_ == res_)
|
||||
|
||||
|
||||
|
||||
// let encryptedBytes = [1, 1, 2, 3, 4, 5, 6, 7, 8, 9,
|
||||
// 250, 245, 192, 217, 164, 251, 23, 40, 36, 214,
|
||||
// 84, 55, 114, 237, 153, 113, 182, 123, 214, 189,
|
||||
// 35, 196, 148, 164, 235, 195, 122, 157, 141, 235,
|
||||
// 5, 92, 44, 35, 37, 244, 90, 254]
|
||||
// var base1: UnsafeMutableRawPointer = malloc(totalBytes + ivTagBytes)
|
||||
//
|
||||
// let assume1 = base1.assumingMemoryBound(to: UInt8.self)
|
||||
// for i in 0..<(totalBytes + ivTagBytes) {
|
||||
// assume1[i] = UInt8(encryptedBytes[i])
|
||||
// }
|
||||
// let encrypted = NSData(bytesNoCopy: base1, length: totalBytes + ivTagBytes)
|
||||
// memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
|
||||
// for i in 0..<48 {
|
||||
// debugPrint("Before decrypt \(i) \(pointer.assumingMemoryBound(to: UInt8.self)[i])")
|
||||
// }
|
||||
|
||||
|
||||
if let res = chat_decrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + ivTagBytes - clearTextBytesSize)) {
|
||||
printError("decrypt", res)
|
||||
}
|
||||
|
||||
|
||||
let decrypted_ = Data(bytes: pointer, count: unencrypted.count)
|
||||
for i in 0..<48 {
|
||||
debugPrint("After decrypt \(i) \(pointer.assumingMemoryBound(to: UInt8.self)[i])")
|
||||
}
|
||||
print(source_ == decrypted_)
|
||||
}
|
||||
|
||||
func printError(_ op: String, _ res: UnsafeMutablePointer<CChar>) {
|
||||
let err = fromCString(res)
|
||||
if err == "" {
|
||||
print("\(op) ok")
|
||||
} else {
|
||||
print("\(op) error: \(err)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='ios' buildActiveScheme='true' importAppTypes='true'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
</playground>
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:Encryption.playground">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -9,7 +9,6 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChat
|
||||
|
||||
final class ChatModel: ObservableObject {
|
||||
@@ -59,7 +58,6 @@ final class ChatModel: ObservableObject {
|
||||
@Published var stopPreviousRecPlay: Bool = false // value is not taken into account, only the fact it switches
|
||||
@Published var draft: ComposeState?
|
||||
@Published var draftChatId: String?
|
||||
var callWebView: WKWebView?
|
||||
|
||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||
|
||||
@@ -69,6 +67,31 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
|
||||
func getUser(_ userId: Int64) -> User? {
|
||||
currentUser?.userId == userId
|
||||
? currentUser
|
||||
: users.first { $0.user.userId == userId }?.user
|
||||
}
|
||||
|
||||
func getUserIndex(_ user: User) -> Int? {
|
||||
users.firstIndex { $0.user.userId == user.userId }
|
||||
}
|
||||
|
||||
func updateUser(_ user: User) {
|
||||
if let i = getUserIndex(user) {
|
||||
users[i].user = user
|
||||
}
|
||||
if currentUser?.userId == user.userId {
|
||||
currentUser = user
|
||||
}
|
||||
}
|
||||
|
||||
func removeUser(_ user: User) {
|
||||
if let i = getUserIndex(user), users[i].user.userId != currentUser?.userId {
|
||||
users.remove(at: i)
|
||||
}
|
||||
}
|
||||
|
||||
func hasChat(_ id: String) -> Bool {
|
||||
chats.first(where: { $0.id == id }) != nil
|
||||
}
|
||||
@@ -545,6 +568,25 @@ final class Chat: ObservableObject, Identifiable {
|
||||
self.chatStats = chatStats
|
||||
}
|
||||
|
||||
var userCanSend: Bool {
|
||||
switch chatInfo {
|
||||
case .direct: return true
|
||||
case let .group(groupInfo):
|
||||
let m = groupInfo.membership
|
||||
return m.memberActive && m.memberRole >= .member
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var userIsObserver: Bool {
|
||||
switch chatInfo {
|
||||
case let .group(groupInfo):
|
||||
let m = groupInfo.membership
|
||||
return m.memberActive && m.memberRole == .observer
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var id: ChatId { get { chatInfo.id } }
|
||||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
||||
@@ -39,7 +39,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
|
||||
if let userId = content.userInfo["userId"] as? Int64,
|
||||
userId != chatModel.currentUser?.userId {
|
||||
changeActiveUser(userId)
|
||||
changeActiveUser(userId, viewPwd: nil)
|
||||
}
|
||||
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
|
||||
let chatId = content.userInfo["chatId"] as? String {
|
||||
@@ -87,13 +87,17 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
switch content.categoryIdentifier {
|
||||
case ntfCategoryMessageReceived:
|
||||
let recent = recentInTheSameChat(content)
|
||||
if model.chatId == nil {
|
||||
let userId = content.userInfo["userId"] as? Int64
|
||||
if let userId = userId, let user = model.getUser(userId), !user.showNotifications {
|
||||
// ... inactive user with disabled notifications
|
||||
return []
|
||||
} else if model.chatId == nil {
|
||||
// in the chat list...
|
||||
if model.currentUser?.userId == (content.userInfo["userId"] as? Int64) {
|
||||
// ... of the current user
|
||||
if model.currentUser?.userId == userId {
|
||||
// ... of the active user
|
||||
return recent ? [] : [.sound, .list]
|
||||
} else {
|
||||
// ... of different user
|
||||
// ... of inactive user
|
||||
return recent ? [.banner] : [.sound, .banner, .list]
|
||||
}
|
||||
} else if model.chatId == content.targetContentIdentifier {
|
||||
|
||||
@@ -132,21 +132,56 @@ func apiCreateActiveUser(_ p: Profile) throws -> User {
|
||||
}
|
||||
|
||||
func listUsers() throws -> [UserInfo] {
|
||||
let r = chatSendCmdSync(.listUsers)
|
||||
return try listUsersResponse(chatSendCmdSync(.listUsers))
|
||||
}
|
||||
|
||||
func listUsersAsync() async throws -> [UserInfo] {
|
||||
return try listUsersResponse(await chatSendCmd(.listUsers))
|
||||
}
|
||||
|
||||
private func listUsersResponse(_ r: ChatResponse) throws -> [UserInfo] {
|
||||
if case let .usersList(users) = r {
|
||||
return users.sorted { $0.user.chatViewName.compare($1.user.chatViewName) == .orderedAscending }
|
||||
}
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetActiveUser(_ userId: Int64) throws -> User {
|
||||
let r = chatSendCmdSync(.apiSetActiveUser(userId: userId))
|
||||
func apiSetActiveUser(_ userId: Int64, viewPwd: String?) throws -> User {
|
||||
let r = chatSendCmdSync(.apiSetActiveUser(userId: userId, viewPwd: viewPwd))
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool) throws {
|
||||
let r = chatSendCmdSync(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues))
|
||||
func apiSetActiveUserAsync(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
let r = await chatSendCmd(.apiSetActiveUser(userId: userId, viewPwd: viewPwd))
|
||||
if case let .activeUser(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User {
|
||||
try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func apiUnhideUser(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
try await setUserPrivacy_(.apiUnhideUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func apiMuteUser(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
try await setUserPrivacy_(.apiMuteUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func apiUnmuteUser(_ userId: Int64, viewPwd: String?) async throws -> User {
|
||||
try await setUserPrivacy_(.apiUnmuteUser(userId: userId, viewPwd: viewPwd))
|
||||
}
|
||||
|
||||
func setUserPrivacy_(_ cmd: ChatCommand) async throws -> User {
|
||||
let r = await chatSendCmd(cmd)
|
||||
if case let .userPrivacy(user) = r { return user }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) async throws {
|
||||
let r = await chatSendCmd(.apiDeleteUser(userId: userId, delSMPQueues: delSMPQueues, viewPwd: viewPwd))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
@@ -209,8 +244,16 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [ChatData] {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetChats: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChats(userId: userId))
|
||||
let userId = try currentUserId("apiGetChats")
|
||||
return try apiChatsResponse(chatSendCmdSync(.apiGetChats(userId: userId)))
|
||||
}
|
||||
|
||||
func apiGetChatsAsync() async throws -> [ChatData] {
|
||||
let userId = try currentUserId("apiGetChats")
|
||||
return try apiChatsResponse(await chatSendCmd(.apiGetChats(userId: userId)))
|
||||
}
|
||||
|
||||
private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] {
|
||||
if case let .apiChats(_, chats) = r { return chats }
|
||||
throw r
|
||||
}
|
||||
@@ -288,6 +331,12 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) async throws -> (ChatItem, ChatItem?) {
|
||||
let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, groupMemberId: groupMemberId, itemId: itemId), bgDelay: msgDelay)
|
||||
if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
|
||||
let r = chatSendCmdSync(.apiGetNtfToken)
|
||||
switch r {
|
||||
@@ -331,20 +380,28 @@ func apiDeleteToken(token: DeviceToken) async throws {
|
||||
}
|
||||
|
||||
func getUserSMPServers() throws -> ([ServerCfg], [String]) {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getUserSMPServers: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId))
|
||||
let userId = try currentUserId("getUserSMPServers")
|
||||
return try userSMPServersResponse(chatSendCmdSync(.apiGetUserSMPServers(userId: userId)))
|
||||
}
|
||||
|
||||
func getUserSMPServersAsync() async throws -> ([ServerCfg], [String]) {
|
||||
let userId = try currentUserId("getUserSMPServersAsync")
|
||||
return try userSMPServersResponse(await chatSendCmd(.apiGetUserSMPServers(userId: userId)))
|
||||
}
|
||||
|
||||
private func userSMPServersResponse(_ r: ChatResponse) throws -> ([ServerCfg], [String]) {
|
||||
if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setUserSMPServers(smpServers: [ServerCfg]) async throws {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setUserSMPServers: no current user") }
|
||||
let userId = try currentUserId("setUserSMPServers")
|
||||
try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers))
|
||||
}
|
||||
|
||||
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
|
||||
let r = await chatSendCmd(.testSMPServer(userId: userId, smpServer: smpServer))
|
||||
let userId = try currentUserId("testSMPServer")
|
||||
let r = await chatSendCmd(.apiTestSMPServer(userId: userId, smpServer: smpServer))
|
||||
if case let .smpTestResult(_, testFailure) = r {
|
||||
if let t = testFailure {
|
||||
return .failure(t)
|
||||
@@ -355,14 +412,22 @@ func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure>
|
||||
}
|
||||
|
||||
func getChatItemTTL() throws -> ChatItemTTL {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getChatItemTTL: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChatItemTTL(userId: userId))
|
||||
let userId = try currentUserId("getChatItemTTL")
|
||||
return try chatItemTTLResponse(chatSendCmdSync(.apiGetChatItemTTL(userId: userId)))
|
||||
}
|
||||
|
||||
func getChatItemTTLAsync() async throws -> ChatItemTTL {
|
||||
let userId = try currentUserId("getChatItemTTLAsync")
|
||||
return try chatItemTTLResponse(await chatSendCmd(.apiGetChatItemTTL(userId: userId)))
|
||||
}
|
||||
|
||||
private func chatItemTTLResponse(_ r: ChatResponse) throws -> ChatItemTTL {
|
||||
if case let .chatItemTTL(_, chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setChatItemTTL: no current user") }
|
||||
let userId = try currentUserId("setChatItemTTL")
|
||||
try await sendCommandOkResp(.apiSetChatItemTTL(userId: userId, seconds: chatItemTTL.seconds))
|
||||
}
|
||||
|
||||
@@ -533,14 +598,14 @@ func clearChat(_ chat: Chat) async {
|
||||
}
|
||||
|
||||
func apiListContacts() throws -> [Contact] {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiListContacts: no current user") }
|
||||
let userId = try currentUserId("apiListContacts")
|
||||
let r = chatSendCmdSync(.apiListContacts(userId: userId))
|
||||
if case let .contactsList(_, contacts) = r { return contacts }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiUpdateProfile: no current user") }
|
||||
let userId = try currentUserId("apiUpdateProfile")
|
||||
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
|
||||
switch r {
|
||||
case .userProfileNoChange: return nil
|
||||
@@ -568,22 +633,30 @@ func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> Pe
|
||||
}
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiCreateUserAddress: no current user") }
|
||||
let userId = try currentUserId("apiCreateUserAddress")
|
||||
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
|
||||
if case let .userContactLinkCreated(_, connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUserAddress() async throws {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiDeleteUserAddress: no current user") }
|
||||
let userId = try currentUserId("apiDeleteUserAddress")
|
||||
let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId))
|
||||
if case .userContactLinkDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() throws -> UserContactLink? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetUserAddress: no current user") }
|
||||
let r = chatSendCmdSync(.apiShowMyAddress(userId: userId))
|
||||
let userId = try currentUserId("apiGetUserAddress")
|
||||
return try userAddressResponse(chatSendCmdSync(.apiShowMyAddress(userId: userId)))
|
||||
}
|
||||
|
||||
func apiGetUserAddressAsync() async throws -> UserContactLink? {
|
||||
let userId = try currentUserId("apiGetUserAddressAsync")
|
||||
return try userAddressResponse(await chatSendCmd(.apiShowMyAddress(userId: userId)))
|
||||
}
|
||||
|
||||
private func userAddressResponse(_ r: ChatResponse) throws -> UserContactLink? {
|
||||
switch r {
|
||||
case let .userContactLink(_, contactLink): return contactLink
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
@@ -592,7 +665,7 @@ func apiGetUserAddress() throws -> UserContactLink? {
|
||||
}
|
||||
|
||||
func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("userAddressAutoAccept: no current user") }
|
||||
let userId = try currentUserId("userAddressAutoAccept")
|
||||
let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept))
|
||||
switch r {
|
||||
case let .userContactLinkUpdated(_, contactLink): return contactLink
|
||||
@@ -787,7 +860,7 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
}
|
||||
|
||||
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiNewGroup: no current user") }
|
||||
let userId = try currentUserId("apiNewGroup")
|
||||
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
|
||||
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
|
||||
throw r
|
||||
@@ -868,9 +941,15 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
|
||||
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
|
||||
if case let .groupLinkCreated(_, _, connReq) = r { return connReq }
|
||||
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) {
|
||||
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole))
|
||||
if case let .groupLinkCreated(_, _, connReq, memberRole) = r { return (connReq, memberRole) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) {
|
||||
let r = await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole))
|
||||
if case let .groupLink(_, _, connReq, memberRole) = r { return (connReq, memberRole) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -880,11 +959,11 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
|
||||
func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? {
|
||||
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
|
||||
switch r {
|
||||
case let .groupLink(_, _, connReq):
|
||||
return connReq
|
||||
case let .groupLink(_, _, connReq, memberRole):
|
||||
return (connReq, memberRole)
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
|
||||
return nil
|
||||
default: throw r
|
||||
@@ -897,7 +976,14 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
||||
throw r
|
||||
}
|
||||
|
||||
func initializeChat(start: Bool, dbKey: String? = nil) throws {
|
||||
private func currentUserId(_ funcName: String) throws -> Int64 {
|
||||
if let userId = ChatModel.shared.currentUser?.userId {
|
||||
return userId
|
||||
}
|
||||
throw RuntimeError("\(funcName): no current user")
|
||||
}
|
||||
|
||||
func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool = true) throws {
|
||||
logger.debug("initializeChat")
|
||||
let m = ChatModel.shared
|
||||
(m.chatDbEncrypted, m.chatDbStatus) = chatMigrateInit(dbKey)
|
||||
@@ -913,13 +999,13 @@ func initializeChat(start: Bool, dbKey: String? = nil) throws {
|
||||
if m.currentUser == nil {
|
||||
m.onboardingStage = .step1_SimpleXInfo
|
||||
} else if start {
|
||||
try startChat()
|
||||
try startChat(refreshInvitations: refreshInvitations)
|
||||
} else {
|
||||
m.chatRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
func startChat() throws {
|
||||
func startChat(refreshInvitations: Bool = true) throws {
|
||||
logger.debug("startChat")
|
||||
let m = ChatModel.shared
|
||||
try setNetworkConfig(getNetCfg())
|
||||
@@ -928,7 +1014,9 @@ func startChat() throws {
|
||||
if justStarted {
|
||||
try getUserChatData()
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
|
||||
try refreshCallInvitations()
|
||||
if (refreshInvitations) {
|
||||
try refreshCallInvitations()
|
||||
}
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
|
||||
if let token = m.deviceToken {
|
||||
registerToken(token: token)
|
||||
@@ -944,21 +1032,38 @@ func startChat() throws {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
}
|
||||
|
||||
func changeActiveUser(_ userId: Int64) {
|
||||
func changeActiveUser(_ userId: Int64, viewPwd: String?) {
|
||||
do {
|
||||
try changeActiveUser_(userId)
|
||||
try changeActiveUser_(userId, viewPwd: viewPwd)
|
||||
} catch let error {
|
||||
logger.error("Unable to set active user: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func changeActiveUser_(_ userId: Int64) throws {
|
||||
private func changeActiveUser_(_ userId: Int64, viewPwd: String?) throws {
|
||||
let m = ChatModel.shared
|
||||
m.currentUser = try apiSetActiveUser(userId)
|
||||
m.currentUser = try apiSetActiveUser(userId, viewPwd: viewPwd)
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
}
|
||||
|
||||
func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws {
|
||||
let currentUser = try await apiSetActiveUserAsync(userId, viewPwd: viewPwd)
|
||||
let users = try await listUsersAsync()
|
||||
await MainActor.run {
|
||||
let m = ChatModel.shared
|
||||
m.currentUser = currentUser
|
||||
m.users = users
|
||||
}
|
||||
try await getUserChatDataAsync()
|
||||
await MainActor.run {
|
||||
if var (_, invitation) = ChatModel.shared.callInvitations.first(where: { _, inv in inv.user.userId == userId }) {
|
||||
invitation.user = currentUser
|
||||
activateCall(invitation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getUserChatData() throws {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
@@ -968,6 +1073,20 @@ func getUserChatData() throws {
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
}
|
||||
|
||||
private func getUserChatDataAsync() async throws {
|
||||
let userAddress = try await apiGetUserAddressAsync()
|
||||
let servers = try await getUserSMPServersAsync()
|
||||
let chatItemTTL = try await getChatItemTTLAsync()
|
||||
let chats = try await apiGetChatsAsync()
|
||||
await MainActor.run {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = userAddress
|
||||
(m.userSMPServers, m.presetSMPServers) = servers
|
||||
m.chatItemTTL = chatItemTTL
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
}
|
||||
}
|
||||
|
||||
class ChatReceiver {
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
@@ -1036,18 +1155,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.removeChat(contact.activeConn.id)
|
||||
}
|
||||
case let .receivedContactRequest(user, contactRequest):
|
||||
if !active(user) { return }
|
||||
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
if m.hasChat(contactRequest.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
} else {
|
||||
m.addChat(Chat(
|
||||
chatInfo: cInfo,
|
||||
chatItems: []
|
||||
))
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
if active(user) {
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
if m.hasChat(contactRequest.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
} else {
|
||||
m.addChat(Chat(
|
||||
chatInfo: cInfo,
|
||||
chatItems: []
|
||||
))
|
||||
}
|
||||
}
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
case let .contactUpdated(user, toContact):
|
||||
if active(user) && m.hasChat(toContact.id) {
|
||||
let cInfo = ChatInfo.direct(contact: toContact)
|
||||
@@ -1180,6 +1299,10 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
if active(user) {
|
||||
m.updateGroup(toGroup)
|
||||
}
|
||||
case let .memberRole(user, groupInfo, _, _, _, _):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
case let .rcvFileStart(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileComplete(user, aChatItem):
|
||||
@@ -1198,19 +1321,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
case let .callInvitation(invitation):
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
activateCall(invitation)
|
||||
|
||||
// This will be called from notification service extension
|
||||
// CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
// "displayName": contact.displayName,
|
||||
// "contactId": contact.id,
|
||||
// "uuid": invitation.callkitUUID
|
||||
// ]) { error in
|
||||
// if let error = error {
|
||||
// logger.error("reportNewIncomingVoIPPushPayload error \(error.localizedDescription)")
|
||||
// } else {
|
||||
// logger.debug("reportNewIncomingVoIPPushPayload success for \(contact.id)")
|
||||
// }
|
||||
// }
|
||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||
withCall(contact) { call in
|
||||
call.callState = .offerReceived
|
||||
@@ -1224,7 +1334,6 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
offer: offer.rtcSession,
|
||||
iceCandidates: offer.rtcIceCandidates,
|
||||
media: callType.media, aesKey: sharedKey,
|
||||
useWorker: true,
|
||||
iceServers: iceServers,
|
||||
relay: useRelay
|
||||
)
|
||||
@@ -1244,7 +1353,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
withCall(contact) { call in
|
||||
m.callCommand = .end
|
||||
// CallController.shared.reportCallRemoteEnded(call: call)
|
||||
CallController.shared.reportCallRemoteEnded(call: call)
|
||||
}
|
||||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
@@ -1295,18 +1404,25 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
||||
|
||||
func refreshCallInvitations() throws {
|
||||
let m = ChatModel.shared
|
||||
let callInvitations = try apiGetCallInvitations()
|
||||
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
|
||||
let callInvitations = try justRefreshCallInvitations()
|
||||
if let (chatId, ntfAction) = m.ntfCallInvitationAction,
|
||||
let invitation = m.callInvitations.removeValue(forKey: chatId) {
|
||||
m.ntfCallInvitationAction = nil
|
||||
CallController.shared.callAction(invitation: invitation, action: ntfAction)
|
||||
} else if let invitation = callInvitations.last {
|
||||
} else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) {
|
||||
activateCall(invitation)
|
||||
}
|
||||
}
|
||||
|
||||
func justRefreshCallInvitations() throws -> [RcvCallInvitation] {
|
||||
let m = ChatModel.shared
|
||||
let callInvitations = try apiGetCallInvitations()
|
||||
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
|
||||
return callInvitations
|
||||
}
|
||||
|
||||
func activateCall(_ callInvitation: RcvCallInvitation) {
|
||||
if !callInvitation.user.showNotifications { return }
|
||||
let m = ChatModel.shared
|
||||
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
||||
if let error = error {
|
||||
|
||||
@@ -81,3 +81,24 @@ func activateChat(appState: AppState = .active) {
|
||||
if ChatModel.ok { apiActivateChat() }
|
||||
}
|
||||
}
|
||||
|
||||
func initChatAndMigrate(refreshInvitations: Bool = true) {
|
||||
let m = ChatModel.shared
|
||||
if (!m.chatInitialized) {
|
||||
do {
|
||||
m.v3DBMigration = v3DBMigrationDefault.get()
|
||||
try initializeChat(start: m.v3DBMigration.startChat, refreshInvitations: refreshInvitations)
|
||||
} catch let error {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startChatAndActivate() {
|
||||
if ChatModel.shared.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
}
|
||||
if .active != appStateGroupDefault.get() {
|
||||
activateChat()
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ struct SimpleXApp: App {
|
||||
@State private var userAuthorized: Bool?
|
||||
@State private var doAuthenticate = false
|
||||
@State private var enteredBackground: TimeInterval? = nil
|
||||
@State private var canConnectCall = false
|
||||
@State private var lastSuccessfulUnlock: TimeInterval? = nil
|
||||
|
||||
init() {
|
||||
hs_init(0, nil)
|
||||
@@ -34,44 +36,43 @@ struct SimpleXApp: App {
|
||||
|
||||
var body: some Scene {
|
||||
return WindowGroup {
|
||||
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized)
|
||||
ContentView(doAuthenticate: $doAuthenticate, userAuthorized: $userAuthorized, canConnectCall: $canConnectCall, lastSuccessfulUnlock: $lastSuccessfulUnlock)
|
||||
.environmentObject(chatModel)
|
||||
.onOpenURL { url in
|
||||
logger.debug("ContentView.onOpenURL: \(url)")
|
||||
chatModel.appOpenUrl = url
|
||||
}
|
||||
.onAppear() {
|
||||
if (!chatModel.chatInitialized) {
|
||||
do {
|
||||
chatModel.v3DBMigration = v3DBMigrationDefault.get()
|
||||
try initializeChat(start: chatModel.v3DBMigration.startChat)
|
||||
} catch let error {
|
||||
fatalError("Failed to start or load chats: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
initChatAndMigrate()
|
||||
}
|
||||
.onChange(of: scenePhase) { phase in
|
||||
logger.debug("scenePhase \(String(describing: scenePhase))")
|
||||
logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))")
|
||||
switch (phase) {
|
||||
case .background:
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
if CallController.useCallKit() && chatModel.activeCall != nil {
|
||||
CallController.shared.shouldSuspendChat = true
|
||||
} else {
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
}
|
||||
if userAuthorized == true {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
canConnectCall = false
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
|
||||
case .active:
|
||||
if chatModel.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
}
|
||||
CallController.shared.shouldSuspendChat = false
|
||||
let appState = appStateGroupDefault.get()
|
||||
activateChat()
|
||||
startChatAndActivate()
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
updateChats()
|
||||
updateCallInvitations()
|
||||
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
|
||||
updateCallInvitations()
|
||||
}
|
||||
}
|
||||
doAuthenticate = authenticationExpired()
|
||||
canConnectCall = !(doAuthenticate && prefPerformLA) || unlockedRecently()
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -111,6 +112,14 @@ struct SimpleXApp: App {
|
||||
}
|
||||
}
|
||||
|
||||
private func unlockedRecently() -> Bool {
|
||||
if let lastSuccessfulUnlock = lastSuccessfulUnlock {
|
||||
return ProcessInfo.processInfo.systemUptime - lastSuccessfulUnlock < 2
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func updateChats() {
|
||||
do {
|
||||
let chats = try apiGetChats()
|
||||
|
||||
@@ -13,37 +13,73 @@ import SimpleXChat
|
||||
struct ActiveCallView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@ObservedObject var call: Call
|
||||
@State private var rtcWebView: WKWebView? = nil
|
||||
@State private var webViewMsg: WVAPIMessage? = nil
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@State private var client: WebRTCClient? = nil
|
||||
@State private var activeCall: WebRTCClient.Call? = nil
|
||||
@State private var localRendererAspectRatio: CGFloat? = nil
|
||||
@Binding var canConnectCall: Bool
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
WebRTCView(rtcWebView: $rtcWebView, webViewMsg: $webViewMsg)
|
||||
.onAppear() { sendCommandToWebView() }
|
||||
.onChange(of: m.callCommand) { _ in sendCommandToWebView() }
|
||||
.onChange(of: rtcWebView) { _ in sendCommandToWebView() }
|
||||
.onChange(of: webViewMsg) { _ in processWebViewMessage() }
|
||||
.background(.black)
|
||||
if let call = m.activeCall, let webView = rtcWebView {
|
||||
ActiveCallOverlay(call: call, webView: webView)
|
||||
if let client = client, [call.peerMedia, call.localMedia].contains(.video), activeCall != nil {
|
||||
GeometryReader { g in
|
||||
let width = g.size.width * 0.3
|
||||
ZStack(alignment: .topTrailing) {
|
||||
CallViewRemote(client: client, activeCall: $activeCall)
|
||||
CallViewLocal(client: client, activeCall: $activeCall, localRendererAspectRatio: $localRendererAspectRatio)
|
||||
.cornerRadius(10)
|
||||
.frame(width: width, height: width / (localRendererAspectRatio ?? 1))
|
||||
.padding([.top, .trailing], 17)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let call = m.activeCall, let client = client {
|
||||
ActiveCallOverlay(call: call, client: client)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
|
||||
createWebRTCClient()
|
||||
dismissAllSheets()
|
||||
}
|
||||
.onChange(of: canConnectCall) { _ in
|
||||
logger.debug("ActiveCallView: canConnectCall changed to \(canConnectCall, privacy: .public)")
|
||||
createWebRTCClient()
|
||||
}
|
||||
.onDisappear {
|
||||
logger.debug("ActiveCallView: disappear")
|
||||
client?.endCall()
|
||||
}
|
||||
.onChange(of: m.callCommand) { _ in sendCommandToClient()}
|
||||
.background(.black)
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
private func sendCommandToWebView() {
|
||||
if m.activeCall != nil,
|
||||
let wv = rtcWebView,
|
||||
let cmd = m.callCommand {
|
||||
m.callCommand = nil
|
||||
sendCallCommand(wv, cmd)
|
||||
private func createWebRTCClient() {
|
||||
if client == nil && canConnectCall {
|
||||
client = WebRTCClient($activeCall, { msg in await MainActor.run { processRtcMessage(msg: msg) } }, $localRendererAspectRatio)
|
||||
sendCommandToClient()
|
||||
}
|
||||
}
|
||||
|
||||
private func processWebViewMessage() {
|
||||
if let msg = webViewMsg,
|
||||
let call = m.activeCall,
|
||||
let webView = rtcWebView {
|
||||
private func sendCommandToClient() {
|
||||
if call == m.activeCall,
|
||||
m.activeCall != nil,
|
||||
let client = client,
|
||||
let cmd = m.callCommand {
|
||||
m.callCommand = nil
|
||||
logger.debug("sendCallCommand: \(cmd.cmdType)")
|
||||
Task {
|
||||
await client.sendCallCommand(command: cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func processRtcMessage(msg: WVAPIMessage) {
|
||||
if call == m.activeCall,
|
||||
let call = m.activeCall,
|
||||
let client = client {
|
||||
logger.debug("ActiveCallView: response \(msg.resp.respType)")
|
||||
switch msg.resp {
|
||||
case let .capabilities(capabilities):
|
||||
@@ -54,7 +90,7 @@ struct ActiveCallView: View {
|
||||
} catch {
|
||||
logger.error("apiSendCallInvitation \(responseError(error))")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
call.callState = .invitationSent
|
||||
call.localCapabilities = capabilities
|
||||
}
|
||||
@@ -67,7 +103,7 @@ struct ActiveCallView: View {
|
||||
} catch {
|
||||
logger.error("apiSendCallOffer \(responseError(error))")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
call.callState = .offerSent
|
||||
call.localCapabilities = capabilities
|
||||
}
|
||||
@@ -79,7 +115,7 @@ struct ActiveCallView: View {
|
||||
} catch {
|
||||
logger.error("apiSendCallAnswer \(responseError(error))")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
call.callState = .negotiated
|
||||
}
|
||||
}
|
||||
@@ -94,17 +130,14 @@ struct ActiveCallView: View {
|
||||
case let .connection(state):
|
||||
if let callStatus = WebRTCCallStatus.init(rawValue: state.connectionState),
|
||||
case .connected = callStatus {
|
||||
// if case .outgoing = call.direction {
|
||||
// CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
// }
|
||||
call.direction == .outgoing
|
||||
? CallController.shared.reportOutgoingCall(call: call, connectedAt: nil)
|
||||
: CallController.shared.reportIncomingCall(call: call, connectedAt: nil)
|
||||
call.callState = .connected
|
||||
// CallKit doesn't work well with WKWebView
|
||||
// This is a hack to enable microphone in WKWebView after CallKit takes over it
|
||||
if CallController.useCallKit {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
|
||||
m.callCommand = .camera(camera: call.localCamera)
|
||||
}
|
||||
}
|
||||
}
|
||||
if state.connectionState == "closed" {
|
||||
closeCallView(client)
|
||||
m.activeCall = nil
|
||||
}
|
||||
Task {
|
||||
do {
|
||||
@@ -117,7 +150,7 @@ struct ActiveCallView: View {
|
||||
call.callState = .connected
|
||||
call.connectionInfo = connectionInfo
|
||||
case .ended:
|
||||
closeCallView(webView)
|
||||
closeCallView(client)
|
||||
call.callState = .ended
|
||||
if let uuid = call.callkitUUID {
|
||||
CallController.shared.endCall(callUUID: uuid)
|
||||
@@ -126,17 +159,8 @@ struct ActiveCallView: View {
|
||||
switch msg.command {
|
||||
case .answer:
|
||||
call.callState = .negotiated
|
||||
case let .camera(camera):
|
||||
call.localCamera = camera
|
||||
Task {
|
||||
// This disables microphone if it was disabled before flipping the camera
|
||||
await webView.setMicrophoneCaptureState(call.audioEnabled ? .active : .muted)
|
||||
// This compensates for the bug on some devices when remote video does not appear
|
||||
// await webView.setCameraCaptureState(.muted)
|
||||
// await webView.setCameraCaptureState(call.videoEnabled ? .active : .muted)
|
||||
}
|
||||
case .end:
|
||||
closeCallView(webView)
|
||||
closeCallView(client)
|
||||
m.activeCall = nil
|
||||
default: ()
|
||||
}
|
||||
@@ -148,11 +172,9 @@ struct ActiveCallView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func closeCallView(_ webView: WKWebView) {
|
||||
m.showCallView = false
|
||||
Task {
|
||||
await webView.setMicrophoneCaptureState(.muted)
|
||||
await webView.setCameraCaptureState(.muted)
|
||||
private func closeCallView(_ client: WebRTCClient) {
|
||||
if m.activeCall != nil {
|
||||
m.showCallView = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +182,7 @@ struct ActiveCallView: View {
|
||||
struct ActiveCallOverlay: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@ObservedObject var call: Call
|
||||
var webView: WKWebView
|
||||
var client: WebRTCClient
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -210,6 +232,8 @@ struct ActiveCallOverlay: View {
|
||||
toggleAudioButton()
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
endCallButton()
|
||||
toggleSpeakerButton()
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.padding(.bottom, 60)
|
||||
.padding(.horizontal, 48)
|
||||
@@ -241,7 +265,7 @@ struct ActiveCallOverlay: View {
|
||||
|
||||
private func endCallButton() -> some View {
|
||||
let cc = CallController.shared
|
||||
return callButton("phone.down.fill", size: 60) {
|
||||
return callButton("phone.down.fill", width: 60, height: 60) {
|
||||
if let uuid = call.callkitUUID {
|
||||
cc.endCall(callUUID: uuid)
|
||||
} else {
|
||||
@@ -254,7 +278,7 @@ struct ActiveCallOverlay: View {
|
||||
private func toggleAudioButton() -> some View {
|
||||
controlButton(call, call.audioEnabled ? "mic.fill" : "mic.slash") {
|
||||
Task {
|
||||
await webView.setMicrophoneCaptureState(call.audioEnabled ? .muted : .active)
|
||||
client.setAudioEnabled(!call.audioEnabled)
|
||||
DispatchQueue.main.async {
|
||||
call.audioEnabled = !call.audioEnabled
|
||||
}
|
||||
@@ -262,10 +286,21 @@ struct ActiveCallOverlay: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleSpeakerButton() -> some View {
|
||||
controlButton(call, call.speakerEnabled ? "speaker.wave.2.fill" : "speaker.wave.1.fill") {
|
||||
Task {
|
||||
client.setSpeakerEnabledAndConfigureSession(!call.speakerEnabled)
|
||||
DispatchQueue.main.async {
|
||||
call.speakerEnabled = !call.speakerEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleVideoButton() -> some View {
|
||||
controlButton(call, call.videoEnabled ? "video.fill" : "video.slash") {
|
||||
Task {
|
||||
await webView.setCameraCaptureState(call.videoEnabled ? .muted : .active)
|
||||
client.setVideoEnabled(!call.videoEnabled)
|
||||
DispatchQueue.main.async {
|
||||
call.videoEnabled = !call.videoEnabled
|
||||
}
|
||||
@@ -274,40 +309,31 @@ struct ActiveCallOverlay: View {
|
||||
}
|
||||
|
||||
@ViewBuilder private func flipCameraButton() -> some View {
|
||||
let cmd = WCallCommand.camera(camera: call.localCamera == .user ? .environment : .user)
|
||||
controlButton(call, "arrow.triangle.2.circlepath") {
|
||||
if call.audioEnabled {
|
||||
chatModel.callCommand = cmd
|
||||
} else {
|
||||
Task {
|
||||
// Microphone has to be enabled before flipping the camera to avoid prompt for user permission when getUserMedia is called in webview
|
||||
await webView.setMicrophoneCaptureState(.active)
|
||||
DispatchQueue.main.async {
|
||||
chatModel.callCommand = cmd
|
||||
}
|
||||
client.flipCamera()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func controlButton(_ call: Call, _ imageName: String, _ perform: @escaping () -> Void) -> some View {
|
||||
if call.hasMedia {
|
||||
callButton(imageName, size: 40, perform)
|
||||
callButton(imageName, width: 50, height: 38, perform)
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.85)
|
||||
} else {
|
||||
Color.clear.frame(width: 40, height: 40)
|
||||
Color.clear.frame(width: 50, height: 38)
|
||||
}
|
||||
}
|
||||
|
||||
private func callButton(_ imageName: String, size: CGFloat, _ perform: @escaping () -> Void) -> some View {
|
||||
private func callButton(_ imageName: String, width: CGFloat, height: CGFloat, _ perform: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
perform()
|
||||
} label: {
|
||||
Image(systemName: imageName)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: size, maxHeight: size)
|
||||
.frame(maxWidth: width, maxHeight: height)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,9 +341,9 @@ struct ActiveCallOverlay: View {
|
||||
struct ActiveCallOverlay_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group{
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), webView: WKWebView())
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
.background(.black)
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), webView: WKWebView())
|
||||
ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil)))
|
||||
.background(.black)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,192 +7,317 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
//import CallKit
|
||||
import CallKit
|
||||
import StoreKit
|
||||
import PushKit
|
||||
import AVFoundation
|
||||
import SimpleXChat
|
||||
import WebRTC
|
||||
|
||||
//class CallController: NSObject, CXProviderDelegate, ObservableObject {
|
||||
class CallController: NSObject, ObservableObject {
|
||||
static let useCallKit = false
|
||||
class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, ObservableObject {
|
||||
static let shared = CallController()
|
||||
// private let provider = CXProvider(configuration: CallController.configuration)
|
||||
// private let controller = CXCallController()
|
||||
static let isInChina = SKStorefront().countryCode == "CHN"
|
||||
static func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
||||
|
||||
private let provider = CXProvider(configuration: {
|
||||
let configuration = CXProviderConfiguration()
|
||||
configuration.supportsVideo = true
|
||||
configuration.supportedHandleTypes = [.generic]
|
||||
configuration.includesCallsInRecents = UserDefaults.standard.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS)
|
||||
configuration.maximumCallGroups = 1
|
||||
configuration.maximumCallsPerCallGroup = 1
|
||||
configuration.iconTemplateImageData = UIImage(named: "icon-transparent")?.pngData()
|
||||
return configuration
|
||||
}())
|
||||
private let controller = CXCallController()
|
||||
private let callManager = CallManager()
|
||||
@Published var activeCallInvitation: RcvCallInvitation?
|
||||
var shouldSuspendChat: Bool = false
|
||||
var fulfillOnConnect: CXAnswerCallAction? = nil
|
||||
|
||||
// PKPushRegistry will be used from notification service extension
|
||||
// let registry = PKPushRegistry(queue: nil)
|
||||
|
||||
// static let configuration: CXProviderConfiguration = {
|
||||
// let configuration = CXProviderConfiguration()
|
||||
// configuration.supportsVideo = true
|
||||
// configuration.supportedHandleTypes = [.generic]
|
||||
// configuration.includesCallsInRecents = true // TODO disable or add option
|
||||
// configuration.maximumCallsPerCallGroup = 1
|
||||
// return configuration
|
||||
// }()
|
||||
// PKPushRegistry is used from notification service extension
|
||||
private let registry = PKPushRegistry(queue: nil)
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
// self.provider.setDelegate(self, queue: nil)
|
||||
// self.registry.delegate = self
|
||||
// self.registry.desiredPushTypes = [.voIP]
|
||||
provider.setDelegate(self, queue: nil)
|
||||
registry.delegate = self
|
||||
registry.desiredPushTypes = [.voIP]
|
||||
}
|
||||
|
||||
// func providerDidReset(_ provider: CXProvider) {
|
||||
// }
|
||||
func providerDidReset(_ provider: CXProvider) {
|
||||
logger.debug("CallController.providerDidReset")
|
||||
}
|
||||
|
||||
// func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
// logger.debug("CallController.provider CXStartCallAction")
|
||||
// if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
// action.fulfill()
|
||||
// provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
// }
|
||||
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
||||
logger.debug("CallController.provider CXStartCallAction")
|
||||
if callManager.startOutgoingCall(callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
// func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
// logger.debug("CallController.provider CXAnswerCallAction")
|
||||
// if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
// action.fulfill()
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
// }
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
logger.debug("CallController.provider CXAnswerCallAction")
|
||||
if callManager.answerIncomingCall(callUUID: action.callUUID) {
|
||||
// WebRTC call should be in connected state to fulfill.
|
||||
// Otherwise no audio and mic working on lockscreen
|
||||
fulfillOnConnect = action
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
// func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
// logger.debug("CallController.provider CXEndCallAction")
|
||||
// callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
// if ok {
|
||||
// action.fulfill()
|
||||
// } else {
|
||||
// action.fail()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
||||
logger.debug("CallController.provider CXEndCallAction")
|
||||
// Should be nil here if connection was in connected state
|
||||
fulfillOnConnect?.fail()
|
||||
fulfillOnConnect = nil
|
||||
callManager.endCall(callUUID: action.callUUID) { ok in
|
||||
if ok {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
self.suspendOnEndCall()
|
||||
}
|
||||
}
|
||||
|
||||
// func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
// print("timed out", #function)
|
||||
// action.fulfill()
|
||||
// }
|
||||
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) {
|
||||
action.fulfill()
|
||||
} else {
|
||||
action.fail()
|
||||
}
|
||||
}
|
||||
|
||||
// func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
// print("received", #function)
|
||||
//// do {
|
||||
//// try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
|
||||
//// logger.debug("audioSession category set")
|
||||
//// try audioSession.setActive(true)
|
||||
//// logger.debug("audioSession activated")
|
||||
//// } catch {
|
||||
//// print(error)
|
||||
//// logger.error("failed activating audio session")
|
||||
//// }
|
||||
// }
|
||||
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
||||
logger.debug("timed out: \(String(describing: action))")
|
||||
action.fulfill()
|
||||
}
|
||||
|
||||
// func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
// print("received", #function)
|
||||
// }
|
||||
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||
logger.debug("CallController: activating audioSession and audio in WebRTCClient")
|
||||
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
|
||||
RTCAudioSession.sharedInstance().isAudioEnabled = true
|
||||
do {
|
||||
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .mixWithOthers)
|
||||
logger.debug("audioSession category set")
|
||||
try audioSession.setActive(true)
|
||||
logger.debug("audioSession activated")
|
||||
} catch {
|
||||
print(error)
|
||||
logger.error("failed activating audio session")
|
||||
}
|
||||
}
|
||||
|
||||
// func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
//
|
||||
// }
|
||||
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||
logger.debug("CallController: deactivating audioSession and audio in WebRTCClient")
|
||||
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
|
||||
RTCAudioSession.sharedInstance().isAudioEnabled = false
|
||||
do {
|
||||
try audioSession.setActive(false)
|
||||
logger.debug("audioSession deactivated")
|
||||
} catch {
|
||||
print(error)
|
||||
logger.error("failed deactivating audio session")
|
||||
}
|
||||
suspendOnEndCall()
|
||||
}
|
||||
|
||||
// This will be needed when we have notification service extension
|
||||
// func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
||||
// if type == .voIP {
|
||||
// // Extract the call information from the push notification payload
|
||||
// if let displayName = payload.dictionaryPayload["displayName"] as? String,
|
||||
// let contactId = payload.dictionaryPayload["contactId"] as? String,
|
||||
// let uuidStr = payload.dictionaryPayload["uuid"] as? String,
|
||||
// let uuid = UUID(uuidString: uuidStr) {
|
||||
// let callUpdate = CXCallUpdate()
|
||||
// callUpdate.remoteHandle = CXHandle(type: .phoneNumber, value: displayName)
|
||||
// provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { error in
|
||||
// if error != nil {
|
||||
// let m = ChatModel.shared
|
||||
// m.callInvitations.removeValue(forKey: contactId)
|
||||
// }
|
||||
// // Tell PushKit that the notification is handled.
|
||||
// completion()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
func suspendOnEndCall() {
|
||||
if shouldSuspendChat {
|
||||
// The delay allows to accept the second call before suspending a chat
|
||||
// see `.onChange(of: scenePhase)` in SimpleXApp
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
logger.debug("CallController: shouldSuspendChat \(String(describing: self?.shouldSuspendChat), privacy: .public)")
|
||||
if ChatModel.shared.activeCall == nil && self?.shouldSuspendChat == true {
|
||||
self?.shouldSuspendChat = false
|
||||
suspendChat()
|
||||
BGManager.shared.schedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(pushRegistry:didUpdatePushCredentials:forType:)
|
||||
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
||||
logger.debug("CallController: didUpdate push credentials for type \(type.rawValue, privacy: .public)")
|
||||
}
|
||||
|
||||
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
||||
logger.debug("CallController: did receive push with type \(type.rawValue, privacy: .public)")
|
||||
if type != .voIP {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
logger.debug("CallController: initializing chat")
|
||||
if (!ChatModel.shared.chatInitialized) {
|
||||
initChatAndMigrate(refreshInvitations: false)
|
||||
}
|
||||
startChatAndActivate()
|
||||
shouldSuspendChat = true
|
||||
// There are no invitations in the model, as it was processed by NSE
|
||||
_ = try? justRefreshCallInvitations()
|
||||
// logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))")
|
||||
// Extract the call information from the push notification payload
|
||||
let m = ChatModel.shared
|
||||
if let contactId = payload.dictionaryPayload["contactId"] as? String,
|
||||
let invitation = m.callInvitations[contactId] {
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
if let uuid = invitation.callkitUUID {
|
||||
logger.debug("CallController: report pushkit call via CallKit")
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error != nil {
|
||||
m.callInvitations.removeValue(forKey: contactId)
|
||||
}
|
||||
// Tell PushKit that the notification is handled.
|
||||
completion()
|
||||
}
|
||||
} else {
|
||||
reportExpiredCall(update: update, completion)
|
||||
}
|
||||
} else {
|
||||
reportExpiredCall(payload: payload, completion)
|
||||
}
|
||||
}
|
||||
|
||||
// This function fulfils the requirement to always report a call when PushKit notification is received,
|
||||
// even when there is no more active calls by the time PushKit payload is processed.
|
||||
// See the note in the bottom of this article:
|
||||
// https://developer.apple.com/documentation/pushkit/pkpushregistrydelegate/2875784-pushregistry
|
||||
private func reportExpiredCall(update: CXCallUpdate, _ completion: @escaping () -> Void) {
|
||||
logger.debug("CallController: report expired pushkit call via CallKit")
|
||||
let uuid = UUID()
|
||||
provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
||||
if error == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
self.provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
}
|
||||
}
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func reportExpiredCall(payload: PKPushPayload, _ completion: @escaping () -> Void) {
|
||||
let update = CXCallUpdate()
|
||||
let displayName = payload.dictionaryPayload["displayName"] as? String
|
||||
let media = payload.dictionaryPayload["media"] as? String
|
||||
update.localizedCallerName = displayName ?? NSLocalizedString("Unknown caller", comment: "callkit banner")
|
||||
update.hasVideo = media == CallMediaType.video.rawValue
|
||||
reportExpiredCall(update: update, completion)
|
||||
}
|
||||
|
||||
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
|
||||
logger.debug("CallController.reportNewIncomingCall")
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
// let update = CXCallUpdate()
|
||||
// update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.displayName)
|
||||
// update.hasVideo = invitation.peerMedia == .video
|
||||
// provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
// } else {
|
||||
logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID), privacy: .public)")
|
||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||
let update = cxCallUpdate(invitation: invitation)
|
||||
provider.reportNewIncomingCall(with: uuid, update: update, completion: completion)
|
||||
}
|
||||
} else {
|
||||
NtfManager.shared.notifyCallInvitation(invitation)
|
||||
if invitation.callTs.timeIntervalSinceNow >= -180 {
|
||||
activeCallInvitation = invitation
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
// if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
// provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
// }
|
||||
// }
|
||||
private func cxCallUpdate(invitation: RcvCallInvitation) -> CXCallUpdate {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: invitation.contact.id)
|
||||
update.hasVideo = invitation.callType.media == .video
|
||||
update.localizedCallerName = invitation.contact.displayName
|
||||
return update
|
||||
}
|
||||
|
||||
func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
logger.debug("CallController: reporting incoming call connected")
|
||||
if CallController.useCallKit() {
|
||||
// Fulfilling this action only after connect, otherwise there are no audio and mic on lockscreen
|
||||
fulfillOnConnect?.fulfill()
|
||||
fulfillOnConnect = nil
|
||||
}
|
||||
}
|
||||
|
||||
func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) {
|
||||
logger.debug("CallController: reporting outgoing call connected")
|
||||
if CallController.useCallKit(), let uuid = call.callkitUUID {
|
||||
provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected)
|
||||
}
|
||||
}
|
||||
|
||||
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
// } else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
logger.debug("CallController: reporting remote ended")
|
||||
if CallController.useCallKit(), let uuid = invitation.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
} else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
activeCallInvitation = nil
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// func reportCallRemoteEnded(call: Call) {
|
||||
// if CallController.useCallKit, let uuid = call.callkitUUID {
|
||||
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
// }
|
||||
// }
|
||||
func reportCallRemoteEnded(call: Call) {
|
||||
logger.debug("CallController: reporting remote ended")
|
||||
if CallController.useCallKit(), let uuid = call.callkitUUID {
|
||||
provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
}
|
||||
}
|
||||
|
||||
func startCall(_ contact: Contact, _ media: CallMediaType) {
|
||||
logger.debug("CallController.startCall")
|
||||
let uuid = callManager.newOutgoingCall(contact, media)
|
||||
// if CallController.useCallKit {
|
||||
// let handle = CXHandle(type: .generic, value: contact.displayName)
|
||||
// let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
// action.isVideo = media == .video
|
||||
// requestTransaction(with: action)
|
||||
// } else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
logger.debug("CallController.startCall: call started")
|
||||
} else {
|
||||
logger.error("CallController.startCall: no active call")
|
||||
if CallController.useCallKit() {
|
||||
let handle = CXHandle(type: .generic, value: contact.id)
|
||||
let action = CXStartCallAction(call: uuid, handle: handle)
|
||||
action.isVideo = media == .video
|
||||
requestTransaction(with: action) {
|
||||
let update = CXCallUpdate()
|
||||
update.remoteHandle = CXHandle(type: .generic, value: contact.id)
|
||||
update.hasVideo = media == .video
|
||||
update.localizedCallerName = contact.displayName
|
||||
self.provider.reportCall(with: uuid, updated: update)
|
||||
}
|
||||
} else if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
if callManager.startOutgoingCall(callUUID: uuid) {
|
||||
logger.debug("CallController.startCall: call started")
|
||||
} else {
|
||||
logger.error("CallController.startCall: no active call")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func answerCall(invitation: RcvCallInvitation) {
|
||||
callManager.answerIncomingCall(invitation: invitation)
|
||||
logger.debug("CallController: answering a call")
|
||||
if CallController.useCallKit(), let callUUID = invitation.callkitUUID {
|
||||
requestTransaction(with: CXAnswerCallAction(call: callUUID))
|
||||
} else {
|
||||
callManager.answerIncomingCall(invitation: invitation)
|
||||
}
|
||||
if invitation.contact.id == self.activeCallInvitation?.contact.id {
|
||||
self.activeCallInvitation = nil
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(callUUID: UUID) {
|
||||
// if CallController.useCallKit {
|
||||
// requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
// } else {
|
||||
logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)")
|
||||
if CallController.useCallKit() {
|
||||
requestTransaction(with: CXEndCallAction(call: callUUID))
|
||||
} else {
|
||||
callManager.endCall(callUUID: callUUID) { ok in
|
||||
if ok {
|
||||
logger.debug("CallController.endCall: call ended")
|
||||
} else {
|
||||
logger.error("CallController.endCall: no actove call pr call invitation to end")
|
||||
logger.error("CallController.endCall: no active call pr call invitation to end")
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(invitation: RcvCallInvitation) {
|
||||
logger.debug("CallController: ending the call with invitation")
|
||||
callManager.endCall(invitation: invitation) {
|
||||
if invitation.contact.id == self.activeCallInvitation?.contact.id {
|
||||
DispatchQueue.main.async {
|
||||
@@ -203,6 +328,7 @@ class CallController: NSObject, ObservableObject {
|
||||
}
|
||||
|
||||
func endCall(call: Call, completed: @escaping () -> Void) {
|
||||
logger.debug("CallController: ending the call with call instance")
|
||||
callManager.endCall(call: call, completed: completed)
|
||||
}
|
||||
|
||||
@@ -213,15 +339,24 @@ class CallController: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// private func requestTransaction(with action: CXAction) {
|
||||
// let t = CXTransaction()
|
||||
// t.addAction(action)
|
||||
// controller.request(t) { error in
|
||||
// if let error = error {
|
||||
// logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription)")
|
||||
// } else {
|
||||
// logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
func showInRecents(_ show: Bool) {
|
||||
let conf = provider.configuration
|
||||
conf.includesCallsInRecents = show
|
||||
provider.configuration = conf
|
||||
}
|
||||
|
||||
func hasActiveCalls() -> Bool {
|
||||
controller.callObserver.calls.count > 0
|
||||
}
|
||||
|
||||
private func requestTransaction(with action: CXAction, onSuccess: @escaping () -> Void = {}) {
|
||||
controller.request(CXTransaction(action: action)) { error in
|
||||
if let error = error {
|
||||
logger.error("CallController.requestTransaction error requesting transaction: \(error.localizedDescription, privacy: .public)")
|
||||
} else {
|
||||
logger.debug("CallController.requestTransaction requested transaction successfully")
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import SimpleXChat
|
||||
class CallManager {
|
||||
func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID {
|
||||
let uuid = UUID()
|
||||
ChatModel.shared.activeCall = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media)
|
||||
let call = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media)
|
||||
call.speakerEnabled = media == .video
|
||||
ChatModel.shared.activeCall = call
|
||||
return uuid
|
||||
}
|
||||
|
||||
@@ -20,7 +22,7 @@ class CallManager {
|
||||
let m = ChatModel.shared
|
||||
if let call = m.activeCall, call.callkitUUID == callUUID {
|
||||
m.showCallView = true
|
||||
m.callCommand = .capabilities(media: call.localMedia, useWorker: true)
|
||||
m.callCommand = .capabilities(media: call.localMedia)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -37,7 +39,7 @@ class CallManager {
|
||||
func answerIncomingCall(invitation: RcvCallInvitation) {
|
||||
let m = ChatModel.shared
|
||||
m.callInvitations.removeValue(forKey: invitation.contact.id)
|
||||
m.activeCall = Call(
|
||||
let call = Call(
|
||||
direction: .incoming,
|
||||
contact: invitation.contact,
|
||||
callkitUUID: invitation.callkitUUID,
|
||||
@@ -45,18 +47,32 @@ class CallManager {
|
||||
localMedia: invitation.callType.media,
|
||||
sharedKey: invitation.sharedKey
|
||||
)
|
||||
m.showCallView = true
|
||||
call.speakerEnabled = invitation.callType.media == .video
|
||||
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||
let iceServers = getIceServers()
|
||||
logger.debug("answerIncomingCall useRelay: \(useRelay)")
|
||||
logger.debug("answerIncomingCall iceServers: \(String(describing: iceServers))")
|
||||
m.callCommand = .start(
|
||||
media: invitation.callType.media,
|
||||
aesKey: invitation.sharedKey,
|
||||
useWorker: true,
|
||||
iceServers: iceServers,
|
||||
relay: useRelay
|
||||
)
|
||||
// When in active call user wants to accept another call, this can only work after delay (to hide and show activeCallView)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + (m.activeCall == nil ? 0 : 1)) {
|
||||
m.activeCall = call
|
||||
m.showCallView = true
|
||||
|
||||
m.callCommand = .start(
|
||||
media: invitation.callType.media,
|
||||
aesKey: invitation.sharedKey,
|
||||
iceServers: iceServers,
|
||||
relay: useRelay
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool {
|
||||
if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID {
|
||||
let m = ChatModel.shared
|
||||
m.callCommand = .media(media: media, enable: enable)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
|
||||
@@ -79,17 +95,15 @@ class CallManager {
|
||||
} else {
|
||||
logger.debug("CallManager.endCall: ending call...")
|
||||
m.callCommand = .end
|
||||
m.activeCall = nil
|
||||
m.showCallView = false
|
||||
completed()
|
||||
Task {
|
||||
do {
|
||||
try await apiEndCall(call.contact)
|
||||
} catch {
|
||||
logger.error("CallController.provider apiEndCall error: \(responseError(error))")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
m.activeCall = nil
|
||||
completed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// Created by Avently on 09.02.2023.
|
||||
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WebRTC
|
||||
import SimpleXChat
|
||||
|
||||
struct CallViewRemote: UIViewRepresentable {
|
||||
var client: WebRTCClient
|
||||
var activeCall: Binding<WebRTCClient.Call?>
|
||||
|
||||
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>) {
|
||||
self.client = client
|
||||
self.activeCall = activeCall
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
if let call = activeCall.wrappedValue {
|
||||
let remoteRenderer = RTCMTLVideoView(frame: view.frame)
|
||||
remoteRenderer.videoContentMode = .scaleAspectFill
|
||||
client.addRemoteRenderer(call, remoteRenderer)
|
||||
addSubviewAndResize(remoteRenderer, into: view)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UIView, context: Context) {
|
||||
logger.debug("CallView.updateUIView remote")
|
||||
}
|
||||
}
|
||||
|
||||
struct CallViewLocal: UIViewRepresentable {
|
||||
var client: WebRTCClient
|
||||
var activeCall: Binding<WebRTCClient.Call?>
|
||||
var localRendererAspectRatio: Binding<CGFloat?>
|
||||
|
||||
init(client: WebRTCClient, activeCall: Binding<WebRTCClient.Call?>, localRendererAspectRatio: Binding<CGFloat?>) {
|
||||
self.client = client
|
||||
self.activeCall = activeCall
|
||||
self.localRendererAspectRatio = localRendererAspectRatio
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UIView {
|
||||
let view = UIView()
|
||||
if let call = activeCall.wrappedValue {
|
||||
let localRenderer = RTCEAGLVideoView(frame: .zero)
|
||||
client.addLocalRenderer(call, localRenderer)
|
||||
client.startCaptureLocalVideo(call)
|
||||
addSubviewAndResize(localRenderer, into: view)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UIView, context: Context) {
|
||||
logger.debug("CallView.updateUIView local")
|
||||
}
|
||||
}
|
||||
|
||||
private func addSubviewAndResize(_ view: UIView, into containerView: UIView) {
|
||||
containerView.addSubview(view)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[view]|",
|
||||
options: [],
|
||||
metrics: nil,
|
||||
views: ["view": view]))
|
||||
|
||||
containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[view]|",
|
||||
options: [],
|
||||
metrics: nil,
|
||||
views: ["view": view]))
|
||||
containerView.layoutIfNeeded()
|
||||
}
|
||||
@@ -65,6 +65,7 @@ struct IncomingCallView: View {
|
||||
.padding(.vertical, 12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.onAppear { dismissAllSheets() }
|
||||
}
|
||||
|
||||
private func callButton(_ text: LocalizedStringKey, _ image: String, _ color: Color, action: @escaping () -> Void) -> some View {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import WebRTC
|
||||
|
||||
class Call: ObservableObject, Equatable {
|
||||
static func == (lhs: Call, rhs: Call) -> Bool {
|
||||
@@ -24,8 +25,8 @@ class Call: ObservableObject, Equatable {
|
||||
@Published var peerMedia: CallMediaType?
|
||||
@Published var sharedKey: String?
|
||||
@Published var audioEnabled = true
|
||||
@Published var speakerEnabled = false
|
||||
@Published var videoEnabled: Bool
|
||||
@Published var localCamera = VideoCamera.user
|
||||
@Published var connectionInfo: ConnectionInfo?
|
||||
|
||||
init(
|
||||
@@ -103,21 +104,18 @@ struct WVAPIMessage: Equatable, Decodable, Encodable {
|
||||
}
|
||||
|
||||
enum WCallCommand: Equatable, Encodable, Decodable {
|
||||
case capabilities(media: CallMediaType, useWorker: Bool? = nil)
|
||||
case start(media: CallMediaType, aesKey: String? = nil, useWorker: Bool? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
|
||||
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, useWorker: Bool? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
|
||||
case capabilities(media: CallMediaType)
|
||||
case start(media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
|
||||
case offer(offer: String, iceCandidates: String, media: CallMediaType, aesKey: String? = nil, iceServers: [RTCIceServer]? = nil, relay: Bool? = nil)
|
||||
case answer(answer: String, iceCandidates: String)
|
||||
case ice(iceCandidates: String)
|
||||
case media(media: CallMediaType, enable: Bool)
|
||||
case camera(camera: VideoCamera)
|
||||
case end
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case media
|
||||
case camera
|
||||
case aesKey
|
||||
case useWorker
|
||||
case offer
|
||||
case answer
|
||||
case iceCandidates
|
||||
@@ -135,7 +133,6 @@ enum WCallCommand: Equatable, Encodable, Decodable {
|
||||
case .answer: return "answer"
|
||||
case .ice: return "ice"
|
||||
case .media: return "media"
|
||||
case .camera: return "camera"
|
||||
case .end: return "end"
|
||||
}
|
||||
}
|
||||
@@ -144,24 +141,21 @@ enum WCallCommand: Equatable, Encodable, Decodable {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .capabilities(media, useWorker):
|
||||
case let .capabilities(media):
|
||||
try container.encode("capabilities", forKey: .type)
|
||||
try container.encode(media, forKey: .media)
|
||||
try container.encode(useWorker, forKey: .useWorker)
|
||||
case let .start(media, aesKey, useWorker, iceServers, relay):
|
||||
case let .start(media, aesKey, iceServers, relay):
|
||||
try container.encode("start", forKey: .type)
|
||||
try container.encode(media, forKey: .media)
|
||||
try container.encode(aesKey, forKey: .aesKey)
|
||||
try container.encode(useWorker, forKey: .useWorker)
|
||||
try container.encode(iceServers, forKey: .iceServers)
|
||||
try container.encode(relay, forKey: .relay)
|
||||
case let .offer(offer, iceCandidates, media, aesKey, useWorker, iceServers, relay):
|
||||
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
|
||||
try container.encode("offer", forKey: .type)
|
||||
try container.encode(offer, forKey: .offer)
|
||||
try container.encode(iceCandidates, forKey: .iceCandidates)
|
||||
try container.encode(media, forKey: .media)
|
||||
try container.encode(aesKey, forKey: .aesKey)
|
||||
try container.encode(useWorker, forKey: .useWorker)
|
||||
try container.encode(iceServers, forKey: .iceServers)
|
||||
try container.encode(relay, forKey: .relay)
|
||||
case let .answer(answer, iceCandidates):
|
||||
@@ -175,9 +169,6 @@ enum WCallCommand: Equatable, Encodable, Decodable {
|
||||
try container.encode("media", forKey: .type)
|
||||
try container.encode(media, forKey: .media)
|
||||
try container.encode(enable, forKey: .enable)
|
||||
case let .camera(camera):
|
||||
try container.encode("camera", forKey: .type)
|
||||
try container.encode(camera, forKey: .camera)
|
||||
case .end:
|
||||
try container.encode("end", forKey: .type)
|
||||
}
|
||||
@@ -189,24 +180,21 @@ enum WCallCommand: Equatable, Encodable, Decodable {
|
||||
switch type {
|
||||
case "capabilities":
|
||||
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
||||
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
|
||||
self = .capabilities(media: media, useWorker: useWorker)
|
||||
self = .capabilities(media: media)
|
||||
case "start":
|
||||
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
||||
let aesKey = try? container.decode(String.self, forKey: CodingKeys.aesKey)
|
||||
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
|
||||
let iceServers = try container.decode(([RTCIceServer]?).self, forKey: .iceServers)
|
||||
let relay = try container.decode((Bool?).self, forKey: .relay)
|
||||
self = .start(media: media, aesKey: aesKey, useWorker: useWorker, iceServers: iceServers, relay: relay)
|
||||
self = .start(media: media, aesKey: aesKey, iceServers: iceServers, relay: relay)
|
||||
case "offer":
|
||||
let offer = try container.decode(String.self, forKey: CodingKeys.offer)
|
||||
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
||||
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
||||
let aesKey = try? container.decode(String.self, forKey: CodingKeys.aesKey)
|
||||
let useWorker = try container.decode((Bool?).self, forKey: CodingKeys.useWorker)
|
||||
let iceServers = try container.decode(([RTCIceServer]?).self, forKey: .iceServers)
|
||||
let relay = try container.decode((Bool?).self, forKey: .relay)
|
||||
self = .offer(offer: offer, iceCandidates: iceCandidates, media: media, aesKey: aesKey, useWorker: useWorker, iceServers: iceServers, relay: relay)
|
||||
self = .offer(offer: offer, iceCandidates: iceCandidates, media: media, aesKey: aesKey, iceServers: iceServers, relay: relay)
|
||||
case "answer":
|
||||
let answer = try container.decode(String.self, forKey: CodingKeys.answer)
|
||||
let iceCandidates = try container.decode(String.self, forKey: CodingKeys.iceCandidates)
|
||||
@@ -218,9 +206,6 @@ enum WCallCommand: Equatable, Encodable, Decodable {
|
||||
let media = try container.decode(CallMediaType.self, forKey: CodingKeys.media)
|
||||
let enable = try container.decode(Bool.self, forKey: CodingKeys.enable)
|
||||
self = .media(media: media, enable: enable)
|
||||
case "camera":
|
||||
let camera = try container.decode(VideoCamera.self, forKey: CodingKeys.camera)
|
||||
self = .camera(camera: camera)
|
||||
case "end":
|
||||
self = .end
|
||||
default:
|
||||
@@ -393,6 +378,9 @@ struct RTCIceCandidate: Codable, Equatable {
|
||||
var candidateType: RTCIceCandidateType?
|
||||
var `protocol`: String?
|
||||
var relayProtocol: String?
|
||||
var sdpMid: String?
|
||||
var sdpMLineIndex: Int?
|
||||
var candidate: String
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
|
||||
@@ -456,3 +444,10 @@ func getIceServers() -> [RTCIceServer]? {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWebRTCIceServers() -> [WebRTC.RTCIceServer]? {
|
||||
if let servers = UserDefaults.standard.stringArray(forKey: DEFAULT_WEBRTC_ICE_SERVERS) {
|
||||
return parseRTCIceServers(servers)?.toWebRTCIceServers()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
//
|
||||
// Created by Avently on 09.02.2023.
|
||||
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import WebRTC
|
||||
import LZString
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDelegate, RTCFrameDecryptorDelegate {
|
||||
private static let factory: RTCPeerConnectionFactory = {
|
||||
RTCInitializeSSL()
|
||||
let videoEncoderFactory = RTCDefaultVideoEncoderFactory()
|
||||
let videoDecoderFactory = RTCDefaultVideoDecoderFactory()
|
||||
return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory)
|
||||
}()
|
||||
private static let ivTagBytes: Int = 28
|
||||
private static let enableEncryption: Bool = true
|
||||
|
||||
struct Call {
|
||||
var connection: RTCPeerConnection
|
||||
var iceCandidates: [RTCIceCandidate]
|
||||
var localMedia: CallMediaType
|
||||
var localCamera: RTCVideoCapturer?
|
||||
var localVideoSource: RTCVideoSource?
|
||||
var localStream: RTCVideoTrack?
|
||||
var remoteStream: RTCVideoTrack?
|
||||
var device: AVCaptureDevice.Position = .front
|
||||
var aesKey: String?
|
||||
var frameEncryptor: RTCFrameEncryptor?
|
||||
var frameDecryptor: RTCFrameDecryptor?
|
||||
}
|
||||
|
||||
private let rtcAudioSession = RTCAudioSession.sharedInstance()
|
||||
private let audioQueue = DispatchQueue(label: "audio")
|
||||
private var sendCallResponse: (WVAPIMessage) async -> Void
|
||||
private var activeCall: Binding<Call?>
|
||||
private var localRendererAspectRatio: Binding<CGFloat?>
|
||||
|
||||
@available(*, unavailable)
|
||||
override init() {
|
||||
fatalError("Unimplemented")
|
||||
}
|
||||
|
||||
required init(_ activeCall: Binding<Call?>, _ sendCallResponse: @escaping (WVAPIMessage) async -> Void, _ localRendererAspectRatio: Binding<CGFloat?>) {
|
||||
self.sendCallResponse = sendCallResponse
|
||||
self.activeCall = activeCall
|
||||
self.localRendererAspectRatio = localRendererAspectRatio
|
||||
rtcAudioSession.useManualAudio = CallController.useCallKit()
|
||||
rtcAudioSession.isAudioEnabled = !CallController.useCallKit()
|
||||
logger.debug("WebRTCClient: rtcAudioSession has manual audio \(self.rtcAudioSession.useManualAudio) and audio enabled \(self.rtcAudioSession.isAudioEnabled)}")
|
||||
super.init()
|
||||
}
|
||||
|
||||
let defaultIceServers: [WebRTC.RTCIceServer] = [
|
||||
WebRTC.RTCIceServer(urlStrings: ["stun:stun.simplex.im:443"]),
|
||||
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
||||
WebRTC.RTCIceServer(urlStrings: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z"),
|
||||
]
|
||||
|
||||
func initializeCall(_ iceServers: [WebRTC.RTCIceServer]?, _ remoteIceCandidates: [RTCIceCandidate], _ mediaType: CallMediaType, _ aesKey: String?, _ relay: Bool?) -> Call {
|
||||
let connection = createPeerConnection(iceServers ?? getWebRTCIceServers() ?? defaultIceServers, relay)
|
||||
connection.delegate = self
|
||||
createAudioSender(connection)
|
||||
var localStream: RTCVideoTrack? = nil
|
||||
var remoteStream: RTCVideoTrack? = nil
|
||||
var localCamera: RTCVideoCapturer? = nil
|
||||
var localVideoSource: RTCVideoSource? = nil
|
||||
if mediaType == .video {
|
||||
(localStream, remoteStream, localCamera, localVideoSource) = createVideoSender(connection)
|
||||
}
|
||||
var frameEncryptor: RTCFrameEncryptor? = nil
|
||||
var frameDecryptor: RTCFrameDecryptor? = nil
|
||||
if aesKey != nil {
|
||||
let encryptor = RTCFrameEncryptor.init(sizeChange: Int32(WebRTCClient.ivTagBytes))
|
||||
encryptor.delegate = self
|
||||
frameEncryptor = encryptor
|
||||
connection.senders.forEach { $0.setRtcFrameEncryptor(encryptor) }
|
||||
|
||||
let decryptor = RTCFrameDecryptor.init(sizeChange: -Int32(WebRTCClient.ivTagBytes))
|
||||
decryptor.delegate = self
|
||||
frameDecryptor = decryptor
|
||||
// Has no video receiver in outgoing call if applied here, see [peerConnection(_ connection: RTCPeerConnection, didChange newState]
|
||||
// connection.receivers.forEach { $0.setRtcFrameDecryptor(decryptor) }
|
||||
}
|
||||
return Call(
|
||||
connection: connection,
|
||||
iceCandidates: remoteIceCandidates,
|
||||
localMedia: mediaType,
|
||||
localCamera: localCamera,
|
||||
localVideoSource: localVideoSource,
|
||||
localStream: localStream,
|
||||
remoteStream: remoteStream,
|
||||
aesKey: aesKey,
|
||||
frameEncryptor: frameEncryptor,
|
||||
frameDecryptor: frameDecryptor
|
||||
)
|
||||
}
|
||||
|
||||
func createPeerConnection(_ iceServers: [WebRTC.RTCIceServer], _ relay: Bool?) -> RTCPeerConnection {
|
||||
let constraints = RTCMediaConstraints(mandatoryConstraints: nil,
|
||||
optionalConstraints: ["DtlsSrtpKeyAgreement": kRTCMediaConstraintsValueTrue])
|
||||
|
||||
guard let connection = WebRTCClient.factory.peerConnection(
|
||||
with: getCallConfig(iceServers, relay),
|
||||
constraints: constraints, delegate: nil
|
||||
)
|
||||
else {
|
||||
fatalError("Unable to create RTCPeerConnection")
|
||||
}
|
||||
return connection
|
||||
}
|
||||
|
||||
func getCallConfig(_ iceServers: [WebRTC.RTCIceServer], _ relay: Bool?) -> RTCConfiguration {
|
||||
let config = RTCConfiguration()
|
||||
config.iceServers = iceServers
|
||||
config.sdpSemantics = .unifiedPlan
|
||||
config.continualGatheringPolicy = .gatherContinually
|
||||
config.iceTransportPolicy = relay == true ? .relay : .all
|
||||
// Allows to wait 30 sec before `failing` connection if the answer from remote side is not received in time
|
||||
config.iceInactiveTimeout = 30_000
|
||||
return config
|
||||
}
|
||||
|
||||
func addIceCandidates(_ connection: RTCPeerConnection, _ remoteIceCandidates: [RTCIceCandidate]) {
|
||||
remoteIceCandidates.forEach { candidate in
|
||||
connection.add(candidate.toWebRTCCandidate()) { error in
|
||||
if let error = error {
|
||||
logger.error("Adding candidate error \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendCallCommand(command: WCallCommand) async {
|
||||
var resp: WCallResponse? = nil
|
||||
let pc = activeCall.wrappedValue?.connection
|
||||
switch command {
|
||||
case .capabilities:
|
||||
resp = .capabilities(capabilities: CallCapabilities(encryption: WebRTCClient.enableEncryption))
|
||||
case let .start(media: media, aesKey, iceServers, relay):
|
||||
logger.debug("starting incoming call - create webrtc session")
|
||||
if activeCall.wrappedValue != nil { endCall() }
|
||||
let encryption = WebRTCClient.enableEncryption
|
||||
let call = initializeCall(iceServers?.toWebRTCIceServers(), [], media, encryption ? aesKey : nil, relay)
|
||||
activeCall.wrappedValue = call
|
||||
call.connection.offer { answer in
|
||||
Task {
|
||||
let gotCandidates = await self.waitWithTimeout(10_000, stepMs: 1000, until: { self.activeCall.wrappedValue?.iceCandidates.count ?? 0 > 0 })
|
||||
if gotCandidates {
|
||||
await self.sendCallResponse(.init(
|
||||
corrId: nil,
|
||||
resp: .offer(
|
||||
offer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
||||
iceCandidates: compressToBase64(input: encodeJSON(self.activeCall.wrappedValue?.iceCandidates ?? [])),
|
||||
capabilities: CallCapabilities(encryption: encryption)
|
||||
),
|
||||
command: command)
|
||||
)
|
||||
} else {
|
||||
self.endCall()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
case let .offer(offer, iceCandidates, media, aesKey, iceServers, relay):
|
||||
if activeCall.wrappedValue != nil {
|
||||
resp = .error(message: "accept: call already started")
|
||||
} else if !WebRTCClient.enableEncryption && aesKey != nil {
|
||||
resp = .error(message: "accept: encryption is not supported")
|
||||
} else if let offer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: offer)),
|
||||
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
|
||||
let call = initializeCall(iceServers?.toWebRTCIceServers(), remoteIceCandidates, media, WebRTCClient.enableEncryption ? aesKey : nil, relay)
|
||||
activeCall.wrappedValue = call
|
||||
let pc = call.connection
|
||||
if let type = offer.type, let sdp = offer.sdp {
|
||||
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
|
||||
pc.answer { answer in
|
||||
self.addIceCandidates(pc, remoteIceCandidates)
|
||||
// Task {
|
||||
// try? await Task.sleep(nanoseconds: 32_000 * 1000000)
|
||||
Task {
|
||||
await self.sendCallResponse(.init(
|
||||
corrId: nil,
|
||||
resp: .answer(
|
||||
answer: compressToBase64(input: encodeJSON(CustomRTCSessionDescription(type: answer.type.toSdpType(), sdp: answer.sdp))),
|
||||
iceCandidates: compressToBase64(input: encodeJSON(call.iceCandidates))
|
||||
),
|
||||
command: command)
|
||||
)
|
||||
}
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
resp = .error(message: "accept: remote description is not set")
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .answer(answer, iceCandidates):
|
||||
if pc == nil {
|
||||
resp = .error(message: "answer: call not started")
|
||||
} else if pc?.localDescription == nil {
|
||||
resp = .error(message: "answer: local description is not set")
|
||||
} else if pc?.remoteDescription != nil {
|
||||
resp = .error(message: "answer: remote description already set")
|
||||
} else if let answer: CustomRTCSessionDescription = decodeJSON(decompressFromBase64(input: answer)),
|
||||
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)),
|
||||
let type = answer.type, let sdp = answer.sdp,
|
||||
let pc = pc {
|
||||
if (try? await pc.setRemoteDescription(RTCSessionDescription(type: type.toWebRTCSdpType(), sdp: sdp))) != nil {
|
||||
addIceCandidates(pc, remoteIceCandidates)
|
||||
resp = .ok
|
||||
} else {
|
||||
resp = .error(message: "answer: remote description is not set")
|
||||
}
|
||||
}
|
||||
case let .ice(iceCandidates):
|
||||
if let pc = pc,
|
||||
let remoteIceCandidates: [RTCIceCandidate] = decodeJSON(decompressFromBase64(input: iceCandidates)) {
|
||||
addIceCandidates(pc, remoteIceCandidates)
|
||||
resp = .ok
|
||||
} else {
|
||||
resp = .error(message: "ice: call not started")
|
||||
}
|
||||
case let .media(media, enable):
|
||||
if activeCall.wrappedValue == nil {
|
||||
resp = .error(message: "media: call not started")
|
||||
} else if activeCall.wrappedValue?.localMedia == .audio && media == .video {
|
||||
resp = .error(message: "media: no video")
|
||||
} else {
|
||||
enableMedia(media, enable)
|
||||
resp = .ok
|
||||
}
|
||||
case .end:
|
||||
await sendCallResponse(.init(corrId: nil, resp: .ok, command: command))
|
||||
endCall()
|
||||
}
|
||||
if let resp = resp {
|
||||
await sendCallResponse(.init(corrId: nil, resp: resp, command: command))
|
||||
}
|
||||
}
|
||||
|
||||
func enableMedia(_ media: CallMediaType, _ enable: Bool) {
|
||||
logger.debug("WebRTCClient: enabling media \(media.rawValue) \(enable)")
|
||||
media == .video ? setVideoEnabled(enable) : setAudioEnabled(enable)
|
||||
}
|
||||
|
||||
func addLocalRenderer(_ activeCall: Call, _ renderer: RTCEAGLVideoView) {
|
||||
activeCall.localStream?.add(renderer)
|
||||
// To get width and height of a frame, see videoView(videoView:, didChangeVideoSize)
|
||||
renderer.delegate = self
|
||||
}
|
||||
|
||||
func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
|
||||
guard size.height > 0 else { return }
|
||||
localRendererAspectRatio.wrappedValue = size.width / size.height
|
||||
}
|
||||
|
||||
func frameDecryptor(_ decryptor: RTCFrameDecryptor, mediaType: RTCRtpMediaType, withFrame encrypted: Data) -> Data? {
|
||||
guard encrypted.count > 0 else { return nil }
|
||||
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
|
||||
let pointer: UnsafeMutableRawPointer = malloc(encrypted.count) {
|
||||
memcpy(pointer, (encrypted as NSData).bytes, encrypted.count)
|
||||
let isKeyFrame = encrypted[0] & 1 == 0
|
||||
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
|
||||
logCrypto("decrypt", chat_decrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(encrypted.count - clearTextBytesSize)))
|
||||
return Data(bytes: pointer, count: encrypted.count - WebRTCClient.ivTagBytes)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func frameEncryptor(_ encryptor: RTCFrameEncryptor, mediaType: RTCRtpMediaType, withFrame unencrypted: Data) -> Data? {
|
||||
guard unencrypted.count > 0 else { return nil }
|
||||
if var key: [CChar] = activeCall.wrappedValue?.aesKey?.cString(using: .utf8),
|
||||
let pointer: UnsafeMutableRawPointer = malloc(unencrypted.count + WebRTCClient.ivTagBytes) {
|
||||
memcpy(pointer, (unencrypted as NSData).bytes, unencrypted.count)
|
||||
let isKeyFrame = unencrypted[0] & 1 == 0
|
||||
let clearTextBytesSize = mediaType.rawValue == 0 ? 1 : isKeyFrame ? 10 : 3
|
||||
logCrypto("encrypt", chat_encrypt_media(&key, pointer.advanced(by: clearTextBytesSize), Int32(unencrypted.count + WebRTCClient.ivTagBytes - clearTextBytesSize)))
|
||||
return Data(bytes: pointer, count: unencrypted.count + WebRTCClient.ivTagBytes)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func logCrypto(_ op: String, _ r: UnsafeMutablePointer<CChar>?) {
|
||||
if let r = r {
|
||||
let err = fromCString(r)
|
||||
if err != "" {
|
||||
logger.error("\(op) error: \(err)")
|
||||
// } else {
|
||||
// logger.debug("\(op) ok")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addRemoteRenderer(_ activeCall: Call, _ renderer: RTCVideoRenderer) {
|
||||
activeCall.remoteStream?.add(renderer)
|
||||
}
|
||||
|
||||
func startCaptureLocalVideo(_ activeCall: Call) {
|
||||
guard
|
||||
let capturer = activeCall.localCamera as? RTCCameraVideoCapturer,
|
||||
let camera = (RTCCameraVideoCapturer.captureDevices().first { $0.position == activeCall.device })
|
||||
else {
|
||||
logger.error("Unable to find a camera")
|
||||
return
|
||||
}
|
||||
|
||||
let supported = RTCCameraVideoCapturer.supportedFormats(for: camera)
|
||||
let height: (AVCaptureDevice.Format) -> Int32 = { (format: AVCaptureDevice.Format) in CMVideoFormatDescriptionGetDimensions(format.formatDescription).height }
|
||||
let format = supported.first(where: { height($0) == 1280 })
|
||||
?? supported.first(where: { height($0) >= 480 && height($0) < 1280 })
|
||||
?? supported.first(where: { height($0) > 1280 })
|
||||
guard
|
||||
let format = format,
|
||||
let fps = format.videoSupportedFrameRateRanges.max(by: { $0.maxFrameRate < $1.maxFrameRate })
|
||||
else {
|
||||
logger.error("Unable to find any format for camera or to choose FPS")
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("Format for camera is \(format.description)")
|
||||
|
||||
capturer.stopCapture()
|
||||
capturer.startCapture(with: camera,
|
||||
format: format,
|
||||
fps: Int(min(24, fps.maxFrameRate)))
|
||||
}
|
||||
|
||||
private func createAudioSender(_ connection: RTCPeerConnection) {
|
||||
let streamId = "stream"
|
||||
let audioTrack = createAudioTrack()
|
||||
connection.add(audioTrack, streamIds: [streamId])
|
||||
}
|
||||
|
||||
private func createVideoSender(_ connection: RTCPeerConnection) -> (RTCVideoTrack?, RTCVideoTrack?, RTCVideoCapturer?, RTCVideoSource?) {
|
||||
let streamId = "stream"
|
||||
let (localVideoTrack, localCamera, localVideoSource) = createVideoTrack()
|
||||
connection.add(localVideoTrack, streamIds: [streamId])
|
||||
return (localVideoTrack, connection.transceivers.first { $0.mediaType == .video }?.receiver.track as? RTCVideoTrack, localCamera, localVideoSource)
|
||||
}
|
||||
|
||||
private func createAudioTrack() -> RTCAudioTrack {
|
||||
let audioConstrains = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
|
||||
let audioSource = WebRTCClient.factory.audioSource(with: audioConstrains)
|
||||
let audioTrack = WebRTCClient.factory.audioTrack(with: audioSource, trackId: "audio0")
|
||||
return audioTrack
|
||||
}
|
||||
|
||||
private func createVideoTrack() -> (RTCVideoTrack, RTCVideoCapturer, RTCVideoSource) {
|
||||
let localVideoSource = WebRTCClient.factory.videoSource()
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
let localCamera = RTCFileVideoCapturer(delegate: localVideoSource)
|
||||
#else
|
||||
let localCamera = RTCCameraVideoCapturer(delegate: localVideoSource)
|
||||
#endif
|
||||
|
||||
let localVideoTrack = WebRTCClient.factory.videoTrack(with: localVideoSource, trackId: "video0")
|
||||
return (localVideoTrack, localCamera, localVideoSource)
|
||||
}
|
||||
|
||||
func endCall() {
|
||||
guard let call = activeCall.wrappedValue else { return }
|
||||
logger.debug("WebRTCClient: ending the call")
|
||||
activeCall.wrappedValue = nil
|
||||
call.connection.close()
|
||||
call.connection.delegate = nil
|
||||
call.frameEncryptor?.delegate = nil
|
||||
call.frameDecryptor?.delegate = nil
|
||||
audioSessionToDefaults()
|
||||
}
|
||||
|
||||
func waitWithTimeout(_ timeoutMs: UInt64, stepMs: UInt64, until success: () -> Bool) async -> Bool {
|
||||
let startedAt = DispatchTime.now()
|
||||
while !success() && startedAt.uptimeNanoseconds + timeoutMs * 1000000 > DispatchTime.now().uptimeNanoseconds {
|
||||
guard let _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) else { break }
|
||||
}
|
||||
return success()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension WebRTC.RTCPeerConnection {
|
||||
func mediaConstraints() -> RTCMediaConstraints {
|
||||
RTCMediaConstraints(
|
||||
mandatoryConstraints: [kRTCMediaConstraintsOfferToReceiveAudio: kRTCMediaConstraintsValueTrue,
|
||||
kRTCMediaConstraintsOfferToReceiveVideo: kRTCMediaConstraintsValueTrue],
|
||||
optionalConstraints: nil)
|
||||
}
|
||||
|
||||
func offer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
|
||||
offer(for: mediaConstraints()) { (sdp, error) in
|
||||
guard let sdp = sdp else {
|
||||
return
|
||||
}
|
||||
self.setLocalDescription(sdp, completionHandler: { (error) in
|
||||
completion(sdp)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func answer(_ completion: @escaping (_ sdp: RTCSessionDescription) -> Void) {
|
||||
answer(for: mediaConstraints()) { (sdp, error) in
|
||||
guard let sdp = sdp else {
|
||||
return
|
||||
}
|
||||
self.setLocalDescription(sdp, completionHandler: { (error) in
|
||||
completion(sdp)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WebRTCClient: RTCPeerConnectionDelegate {
|
||||
func peerConnection(_ connection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {
|
||||
logger.debug("Connection new signaling state: \(stateChanged.rawValue)")
|
||||
}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection, didAdd stream: RTCMediaStream) {
|
||||
logger.debug("Connection did add stream")
|
||||
}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection, didRemove stream: RTCMediaStream) {
|
||||
logger.debug("Connection did remove stream")
|
||||
}
|
||||
|
||||
func peerConnectionShouldNegotiate(_ connection: RTCPeerConnection) {
|
||||
logger.debug("Connection should negotiate")
|
||||
}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection, didChange newState: RTCIceConnectionState) {
|
||||
debugPrint("Connection new connection state: \(newState.toString() ?? "" + newState.rawValue.description) \(connection.receivers)")
|
||||
|
||||
guard let call = activeCall.wrappedValue,
|
||||
let connectionStateString = newState.toString(),
|
||||
let iceConnectionStateString = connection.iceConnectionState.toString(),
|
||||
let iceGatheringStateString = connection.iceGatheringState.toString(),
|
||||
let signalingStateString = connection.signalingState.toString()
|
||||
else {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
await sendCallResponse(.init(
|
||||
corrId: nil,
|
||||
resp: .connection(state: ConnectionState(
|
||||
connectionState: connectionStateString,
|
||||
iceConnectionState: iceConnectionStateString,
|
||||
iceGatheringState: iceGatheringStateString,
|
||||
signalingState: signalingStateString)
|
||||
),
|
||||
command: nil)
|
||||
)
|
||||
|
||||
switch newState {
|
||||
case .checking:
|
||||
if let frameDecryptor = activeCall.wrappedValue?.frameDecryptor {
|
||||
connection.receivers.forEach { $0.setRtcFrameDecryptor(frameDecryptor) }
|
||||
}
|
||||
let enableSpeaker: Bool
|
||||
switch call.localMedia {
|
||||
case .video: enableSpeaker = true
|
||||
default: enableSpeaker = false
|
||||
}
|
||||
setSpeakerEnabledAndConfigureSession(enableSpeaker)
|
||||
case .disconnected, .failed: endCall()
|
||||
default: do {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {
|
||||
logger.debug("connection new gathering state: \(newState.toString() ?? "" + newState.rawValue.description)")
|
||||
}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection, didGenerate candidate: WebRTC.RTCIceCandidate) {
|
||||
// logger.debug("Connection generated candidate \(candidate.debugDescription)")
|
||||
activeCall.wrappedValue?.iceCandidates.append(candidate.toCandidate(nil, nil, nil))
|
||||
}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection, didRemove candidates: [WebRTC.RTCIceCandidate]) {
|
||||
logger.debug("Connection did remove candidates")
|
||||
}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) {}
|
||||
|
||||
func peerConnection(_ connection: RTCPeerConnection,
|
||||
didChangeLocalCandidate local: WebRTC.RTCIceCandidate,
|
||||
remoteCandidate remote: WebRTC.RTCIceCandidate,
|
||||
lastReceivedMs lastDataReceivedMs: Int32,
|
||||
changeReason reason: String) {
|
||||
// logger.debug("Connection changed candidate \(reason) \(remote.debugDescription) \(remote.description)")
|
||||
sendConnectedEvent(connection, local: local, remote: remote)
|
||||
}
|
||||
|
||||
func sendConnectedEvent(_ connection: WebRTC.RTCPeerConnection, local: WebRTC.RTCIceCandidate, remote: WebRTC.RTCIceCandidate) {
|
||||
connection.statistics { (stats: RTCStatisticsReport) in
|
||||
stats.statistics.values.forEach { stat in
|
||||
// logger.debug("Stat \(stat.debugDescription)")
|
||||
if stat.type == "candidate-pair", stat.values["state"] as? String == "succeeded",
|
||||
let localId = stat.values["localCandidateId"] as? String,
|
||||
let remoteId = stat.values["remoteCandidateId"] as? String,
|
||||
let localStats = stats.statistics[localId],
|
||||
let remoteStats = stats.statistics[remoteId],
|
||||
local.sdp.contains("\((localStats.values["ip"] as? String ?? "--")) \((localStats.values["port"] as? String ?? "--"))") &&
|
||||
remote.sdp.contains("\((remoteStats.values["ip"] as? String ?? "--")) \((remoteStats.values["port"] as? String ?? "--"))")
|
||||
{
|
||||
Task {
|
||||
await self.sendCallResponse(.init(
|
||||
corrId: nil,
|
||||
resp: .connected(connectionInfo: ConnectionInfo(
|
||||
localCandidate: local.toCandidate(
|
||||
RTCIceCandidateType.init(rawValue: localStats.values["candidateType"] as! String),
|
||||
localStats.values["protocol"] as? String,
|
||||
localStats.values["relayProtocol"] as? String
|
||||
),
|
||||
remoteCandidate: remote.toCandidate(
|
||||
RTCIceCandidateType.init(rawValue: remoteStats.values["candidateType"] as! String),
|
||||
remoteStats.values["protocol"] as? String,
|
||||
remoteStats.values["relayProtocol"] as? String
|
||||
))),
|
||||
command: nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WebRTCClient {
|
||||
func setAudioEnabled(_ enabled: Bool) {
|
||||
setTrackEnabled(RTCAudioTrack.self, enabled)
|
||||
}
|
||||
|
||||
func setSpeakerEnabledAndConfigureSession( _ enabled: Bool) {
|
||||
logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled)")
|
||||
audioQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.rtcAudioSession.lockForConfiguration()
|
||||
defer {
|
||||
self.rtcAudioSession.unlockForConfiguration()
|
||||
}
|
||||
do {
|
||||
try self.rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue)
|
||||
try self.rtcAudioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue)
|
||||
try self.rtcAudioSession.overrideOutputAudioPort(enabled ? .speaker : .none)
|
||||
try self.rtcAudioSession.setActive(true)
|
||||
logger.debug("WebRTCClient: configuring session with speaker enabled \(enabled) success")
|
||||
} catch let error {
|
||||
logger.debug("Error configuring AVAudioSession: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func audioSessionToDefaults() {
|
||||
logger.debug("WebRTCClient: audioSession to defaults")
|
||||
audioQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.rtcAudioSession.lockForConfiguration()
|
||||
defer {
|
||||
self.rtcAudioSession.unlockForConfiguration()
|
||||
}
|
||||
do {
|
||||
try self.rtcAudioSession.setCategory(AVAudioSession.Category.ambient.rawValue)
|
||||
try self.rtcAudioSession.setMode(AVAudioSession.Mode.default.rawValue)
|
||||
try self.rtcAudioSession.overrideOutputAudioPort(.none)
|
||||
try self.rtcAudioSession.setActive(false)
|
||||
logger.debug("WebRTCClient: audioSession to defaults success")
|
||||
} catch let error {
|
||||
logger.debug("Error configuring AVAudioSession with defaults: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setVideoEnabled(_ enabled: Bool) {
|
||||
setTrackEnabled(RTCVideoTrack.self, enabled)
|
||||
}
|
||||
|
||||
func flipCamera() {
|
||||
switch activeCall.wrappedValue?.device {
|
||||
case .front: activeCall.wrappedValue?.device = .back
|
||||
case .back: activeCall.wrappedValue?.device = .front
|
||||
default: ()
|
||||
}
|
||||
if let call = activeCall.wrappedValue {
|
||||
startCaptureLocalVideo(call)
|
||||
}
|
||||
}
|
||||
|
||||
private func setTrackEnabled<T: RTCMediaStreamTrack>(_ type: T.Type, _ enabled: Bool) {
|
||||
activeCall.wrappedValue?.connection.transceivers
|
||||
.compactMap { $0.sender.track as? T }
|
||||
.forEach { $0.isEnabled = enabled }
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomRTCSessionDescription: Codable {
|
||||
public var type: RTCSdpType?
|
||||
public var sdp: String?
|
||||
}
|
||||
|
||||
enum RTCSdpType: String, Codable {
|
||||
case answer
|
||||
case offer
|
||||
case pranswer
|
||||
case rollback
|
||||
}
|
||||
|
||||
extension RTCIceCandidate {
|
||||
func toWebRTCCandidate() -> WebRTC.RTCIceCandidate {
|
||||
WebRTC.RTCIceCandidate(
|
||||
sdp: candidate,
|
||||
sdpMLineIndex: Int32(sdpMLineIndex ?? 0),
|
||||
sdpMid: sdpMid
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension WebRTC.RTCIceCandidate {
|
||||
func toCandidate(_ candidateType: RTCIceCandidateType?, _ protocol: String?, _ relayProtocol: String?) -> RTCIceCandidate {
|
||||
RTCIceCandidate(
|
||||
candidateType: candidateType,
|
||||
protocol: `protocol`,
|
||||
relayProtocol: relayProtocol,
|
||||
sdpMid: sdpMid,
|
||||
sdpMLineIndex: Int(sdpMLineIndex),
|
||||
candidate: sdp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension [RTCIceServer] {
|
||||
func toWebRTCIceServers() -> [WebRTC.RTCIceServer] {
|
||||
self.map {
|
||||
WebRTC.RTCIceServer(
|
||||
urlStrings: $0.urls,
|
||||
username: $0.username,
|
||||
credential: $0.credential
|
||||
) }
|
||||
}
|
||||
}
|
||||
|
||||
extension RTCSdpType {
|
||||
func toWebRTCSdpType() -> WebRTC.RTCSdpType {
|
||||
switch self {
|
||||
case .answer: return WebRTC.RTCSdpType.answer
|
||||
case .offer: return WebRTC.RTCSdpType.offer
|
||||
case .pranswer: return WebRTC.RTCSdpType.prAnswer
|
||||
case .rollback: return WebRTC.RTCSdpType.rollback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WebRTC.RTCSdpType {
|
||||
func toSdpType() -> RTCSdpType {
|
||||
switch self {
|
||||
case .answer: return RTCSdpType.answer
|
||||
case .offer: return RTCSdpType.offer
|
||||
case .prAnswer: return RTCSdpType.pranswer
|
||||
case .rollback: return RTCSdpType.rollback
|
||||
default: return RTCSdpType.answer // should never be here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RTCPeerConnectionState {
|
||||
func toString() -> String? {
|
||||
switch self {
|
||||
case .new: return "new"
|
||||
case .connecting: return "connecting"
|
||||
case .connected: return "connected"
|
||||
case .failed: return"failed"
|
||||
case .disconnected: return "disconnected"
|
||||
case .closed: return "closed"
|
||||
default: return nil // unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RTCIceConnectionState {
|
||||
func toString() -> String? {
|
||||
switch self {
|
||||
case .new: return "new"
|
||||
case .checking: return "checking"
|
||||
case .connected: return "connected"
|
||||
case .completed: return "completed"
|
||||
case .failed: return "failed"
|
||||
case .disconnected: return "disconnected"
|
||||
case .closed: return "closed"
|
||||
default: return nil // unknown or unused on the other side
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RTCIceGatheringState {
|
||||
func toString() -> String? {
|
||||
switch self {
|
||||
case .new: return "new"
|
||||
case .gathering: return "gathering"
|
||||
case .complete: return "complete"
|
||||
default: return nil // unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RTCSignalingState {
|
||||
func toString() -> String? {
|
||||
switch self {
|
||||
case .stable: return "stable"
|
||||
case .haveLocalOffer: return "have-local-offer"
|
||||
case .haveLocalPrAnswer: return "have-local-pranswer"
|
||||
case .haveRemoteOffer: return "have-remote-offer"
|
||||
case .haveRemotePrAnswer: return "have-remote-pranswer"
|
||||
case .closed: return "closed"
|
||||
default: return nil // unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
//
|
||||
// WebRTCView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Ian Davies on 29/04/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WebKit
|
||||
import SimpleXChat
|
||||
|
||||
class WebRTCCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate {
|
||||
var rtcWebView: Binding<WKWebView?>
|
||||
var webViewMsg: Binding<WVAPIMessage?>
|
||||
|
||||
internal init(rtcWebView: Binding<WKWebView?>, webViewMsg: Binding<WVAPIMessage?>) {
|
||||
self.rtcWebView = rtcWebView
|
||||
self.webViewMsg = webViewMsg
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
webView.allowsBackForwardNavigationGestures = false
|
||||
self.rtcWebView.wrappedValue = webView
|
||||
ChatModel.shared.callWebView = webView
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decideMediaCapturePermissionsFor origin : WKSecurityOrigin, initiatedBy frame: WKFrameInfo, type: WKMediaCaptureType) async -> WKPermissionDecision {
|
||||
print("webView", #function)
|
||||
return .grant
|
||||
}
|
||||
|
||||
// receive message from WKWebView
|
||||
func userContentController(
|
||||
_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage
|
||||
) {
|
||||
logger.debug("WebRTCCoordinator.userContentController")
|
||||
switch message.name {
|
||||
case "webrtc":
|
||||
if let msgStr = message.body as? String,
|
||||
let msg: WVAPIMessage = decodeJSON(msgStr) {
|
||||
// this is the binding that communicates messages from webview to swift view
|
||||
webViewMsg.wrappedValue = msg
|
||||
if case .invalid = msg.resp {
|
||||
logger.error("WebRTCCoordinator.userContentController: invalid message \(String(describing: message.body))")
|
||||
}
|
||||
} else {
|
||||
logger.error("WebRTCCoordinator.userContentController: message parsing error \(String(describing: message.body))")
|
||||
}
|
||||
case "logger":
|
||||
if let msgStr = message.body as? String {
|
||||
logger.error("WebRTCCoordinator console.log: \(msgStr)")
|
||||
} else {
|
||||
logger.error("WebRTCCoordinator console.log: \(String(describing: message.body))")
|
||||
}
|
||||
default:
|
||||
logger.error("WebRTCCoordinator.userContentController: invalid message.name \(message.name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WebRTCView: UIViewRepresentable {
|
||||
@State private var coordinator: WebRTCCoordinator?
|
||||
@Binding var rtcWebView: WKWebView?
|
||||
@Binding var webViewMsg: WVAPIMessage?
|
||||
|
||||
func makeCoordinator() -> WebRTCCoordinator {
|
||||
WebRTCCoordinator(rtcWebView: $rtcWebView, webViewMsg: $webViewMsg)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let wkCoordinator = makeCoordinator()
|
||||
DispatchQueue.main.async { coordinator = wkCoordinator }
|
||||
|
||||
let wkController = WKUserContentController()
|
||||
|
||||
let cfg = WKWebViewConfiguration()
|
||||
cfg.userContentController = wkController
|
||||
cfg.mediaTypesRequiringUserActionForPlayback = []
|
||||
cfg.allowsInlineMediaPlayback = true
|
||||
|
||||
let addScript = { (handler: String, source: String) in
|
||||
let script = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
|
||||
wkController.addUserScript(script)
|
||||
wkController.add(wkCoordinator, name: handler)
|
||||
}
|
||||
|
||||
addScript("webrtc", "sendMessageToNative = (msg) => webkit.messageHandlers.webrtc.postMessage(JSON.stringify(msg))")
|
||||
addScript("logger", "console.log = (arg) => webkit.messageHandlers.logger.postMessage(JSON.stringify(arg))")
|
||||
|
||||
let wkWebView = WKWebView(frame: .zero, configuration: cfg)
|
||||
wkWebView.navigationDelegate = wkCoordinator
|
||||
guard let path: String = Bundle.main.path(forResource: "call", ofType: "html", inDirectory: "www") else {
|
||||
logger.error("WebRTCView.makeUIView call.html not found")
|
||||
return wkWebView
|
||||
}
|
||||
let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
|
||||
wkWebView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
|
||||
return wkWebView
|
||||
}
|
||||
|
||||
func updateUIView(_ webView: WKWebView, context: Context) {
|
||||
logger.debug("WebRTCView.updateUIView")
|
||||
}
|
||||
}
|
||||
|
||||
func sendCallCommand(_ webView: WKWebView, _ command: WCallCommand) {
|
||||
logger.debug("sendCallCommand: \(command.cmdType)")
|
||||
let apiCmd = encodeJSON(WVAPICall(command: command))
|
||||
let js = "processCommand(\(apiCmd))"
|
||||
webView.evaluateJavaScript(js)
|
||||
}
|
||||
|
||||
//struct CallViewDebug: View {
|
||||
// @State private var commandStr = ""
|
||||
// @State private var rtcWebView: WKWebView? = nil
|
||||
// @State private var webViewMsg: WVAPIMessage? = nil
|
||||
// @FocusState private var keyboardVisible: Bool
|
||||
//
|
||||
// var body: some View {
|
||||
// VStack(spacing: 30) {
|
||||
// WebRTCView(rtcWebView: $rtcWebView, webViewMsg: $webViewMsg).frame(maxHeight: 260)
|
||||
// .onChange(of: webViewMsg) { _ in
|
||||
// if let resp = webViewMsg {
|
||||
// commandStr = encodeJSON(resp)
|
||||
// }
|
||||
// }
|
||||
// TextEditor(text: $commandStr)
|
||||
// .focused($keyboardVisible)
|
||||
// .disableAutocorrection(true)
|
||||
// .textInputAutocapitalization(.never)
|
||||
// .padding(.horizontal, 5)
|
||||
// .padding(.top, 2)
|
||||
// .frame(height: 112)
|
||||
// .overlay(
|
||||
// RoundedRectangle(cornerRadius: 10)
|
||||
// .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
|
||||
// )
|
||||
// HStack(spacing: 20) {
|
||||
// Button("Copy") {
|
||||
// UIPasteboard.general.string = commandStr
|
||||
// }
|
||||
// Button("Paste") {
|
||||
// commandStr = UIPasteboard.general.string ?? ""
|
||||
// }
|
||||
// Button("Clear") {
|
||||
// commandStr = ""
|
||||
// }
|
||||
// Button("Send") {
|
||||
// if let wv = rtcWebView,
|
||||
// let command: WCallCommand = decodeJSON(commandStr) {
|
||||
// sendCallCommand(wv, command)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// HStack(spacing: 20) {
|
||||
// Button("Capabilities") {
|
||||
// if let wv = rtcWebView {
|
||||
// sendCallCommand(wv, .capabilities(useWorker: true))
|
||||
// }
|
||||
// }
|
||||
// Button("Start") {
|
||||
// if let wv = rtcWebView {
|
||||
// sendCallCommand(wv, .start(media: .video))
|
||||
// }
|
||||
// }
|
||||
// Button("Accept") {
|
||||
//
|
||||
// }
|
||||
// Button("Answer") {
|
||||
//
|
||||
// }
|
||||
// Button("ICE") {
|
||||
//
|
||||
// }
|
||||
// Button("End") {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -29,8 +29,12 @@ struct FramedItemView: View {
|
||||
var body: some View {
|
||||
let v = ZStack(alignment: .bottomTrailing) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if chatItem.meta.itemDeleted != nil {
|
||||
framedItemHeader(icon: "trash", caption: Text("marked deleted").italic())
|
||||
if let di = chatItem.meta.itemDeleted {
|
||||
if case let .moderated(byGroupMember) = di {
|
||||
framedItemHeader(icon: "flag", caption: Text("moderated by \(byGroupMember.chatViewName)").italic())
|
||||
} else {
|
||||
framedItemHeader(icon: "trash", caption: Text("marked deleted").italic())
|
||||
}
|
||||
} else if chatItem.meta.isLive {
|
||||
framedItemHeader(caption: Text("LIVE"))
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ struct MarkedDeletedItemView: View {
|
||||
if showMember, let member = chatItem.memberDisplayName {
|
||||
Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption)
|
||||
}
|
||||
Text("marked deleted")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
if case let .moderated(byGroupMember) = chatItem.meta.itemDeleted {
|
||||
markedDeletedText("moderated by \(byGroupMember.chatViewName)")
|
||||
} else {
|
||||
markedDeletedText("marked deleted")
|
||||
}
|
||||
CIMetaView(chatItem: chatItem)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
@@ -32,6 +33,14 @@ struct MarkedDeletedItemView: View {
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
func markedDeletedText(_ s: LocalizedStringKey) -> some View {
|
||||
Text(s)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.italic()
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
|
||||
struct MarkedDeletedItemView_Previews: PreviewProvider {
|
||||
|
||||
@@ -492,8 +492,13 @@ struct ChatView: View {
|
||||
if !live || !ci.meta.isLive {
|
||||
menu.append(deleteUIAction())
|
||||
}
|
||||
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
|
||||
menu.append(moderateUIAction(groupInfo))
|
||||
}
|
||||
} else if ci.meta.itemDeleted != nil {
|
||||
menu.append(revealUIAction())
|
||||
if !ci.isDeletedContent {
|
||||
menu.append(revealUIAction())
|
||||
}
|
||||
menu.append(deleteUIAction())
|
||||
} else if ci.isDeletedContent {
|
||||
menu.append(deleteUIAction())
|
||||
@@ -595,7 +600,29 @@ struct ChatView: View {
|
||||
deletingItem = ci
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func moderateUIAction(_ groupInfo: GroupInfo) -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Moderate", comment: "chat item action"),
|
||||
image: UIImage(systemName: "flag"),
|
||||
attributes: [.destructive]
|
||||
) { _ in
|
||||
AlertManager.shared.showAlert(Alert(
|
||||
title: Text("Delete member message?"),
|
||||
message: Text(
|
||||
groupInfo.fullGroupPreferences.fullDelete.on
|
||||
? "The message will be deleted for all members."
|
||||
: "The message will be marked as moderated for all members."
|
||||
),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deletingItem = ci
|
||||
deleteMessage(.cidmBroadcast)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func revealUIAction() -> UIAction {
|
||||
UIAction(
|
||||
title: NSLocalizedString("Reveal", comment: "chat item action"),
|
||||
@@ -638,12 +665,23 @@ struct ChatView: View {
|
||||
logger.debug("ChatView deleteMessage: in Task")
|
||||
do {
|
||||
if let di = deletingItem {
|
||||
let (deletedItem, toItem) = try await apiDeleteChatItem(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
itemId: di.id,
|
||||
mode: mode
|
||||
)
|
||||
var deletedItem: ChatItem
|
||||
var toItem: ChatItem?
|
||||
if case .cidmBroadcast = mode,
|
||||
let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) {
|
||||
(deletedItem, toItem) = try await apiDeleteMemberChatItem(
|
||||
groupId: groupInfo.apiId,
|
||||
groupMemberId: groupMember.groupMemberId,
|
||||
itemId: di.id
|
||||
)
|
||||
} else {
|
||||
(deletedItem, toItem) = try await apiDeleteChatItem(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
itemId: di.id,
|
||||
mode: mode
|
||||
)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
deletingItem = nil
|
||||
if let toItem = toItem {
|
||||
|
||||
@@ -258,36 +258,52 @@ struct ComposeView: View {
|
||||
Image(systemName: "paperclip")
|
||||
.resizable()
|
||||
}
|
||||
.disabled(composeState.attachmentDisabled)
|
||||
.disabled(composeState.attachmentDisabled || !chat.userCanSend)
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.bottom, 12)
|
||||
.padding(.leading, 12)
|
||||
SendMessageView(
|
||||
composeState: $composeState,
|
||||
sendMessage: {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
},
|
||||
sendLiveMessage: sendLiveMessage,
|
||||
updateLiveMessage: updateLiveMessage,
|
||||
cancelLiveMessage: {
|
||||
composeState.liveMessage = nil
|
||||
chatModel.removeLiveDummy()
|
||||
},
|
||||
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
||||
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
||||
startVoiceMessageRecording: {
|
||||
Task {
|
||||
await startVoiceMessageRecording()
|
||||
}
|
||||
},
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
||||
onImagesAdded: { images in if !images.isEmpty { chosenImages = images }},
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.background(.background)
|
||||
ZStack(alignment: .leading) {
|
||||
SendMessageView(
|
||||
composeState: $composeState,
|
||||
sendMessage: {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
},
|
||||
sendLiveMessage: sendLiveMessage,
|
||||
updateLiveMessage: updateLiveMessage,
|
||||
cancelLiveMessage: {
|
||||
composeState.liveMessage = nil
|
||||
chatModel.removeLiveDummy()
|
||||
},
|
||||
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
||||
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
||||
startVoiceMessageRecording: {
|
||||
Task {
|
||||
await startVoiceMessageRecording()
|
||||
}
|
||||
},
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
||||
onImagesAdded: { images in if !images.isEmpty { chosenImages = images }},
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
.background(.background)
|
||||
.disabled(!chat.userCanSend)
|
||||
|
||||
if chat.userIsObserver {
|
||||
Text("you are observer")
|
||||
.italic()
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "You can't send messages!",
|
||||
message: "Please contact group admin."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: composeState.message) { _ in
|
||||
@@ -299,6 +315,13 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: chat.userCanSend) { canSend in
|
||||
if !canSend {
|
||||
cancelCurrentVoiceRecording()
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
.confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) {
|
||||
Button("Take picture") {
|
||||
showTakePhoto = true
|
||||
|
||||
@@ -17,6 +17,7 @@ struct GroupChatInfoView: View {
|
||||
@ObservedObject private var alertManager = AlertManager.shared
|
||||
@State private var alert: GroupChatInfoViewAlert? = nil
|
||||
@State private var groupLink: String?
|
||||
@State private var groupLinkMemberRole: GroupMemberRole = .member
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var connectionStats: ConnectionStats?
|
||||
@State private var connectionCode: String?
|
||||
@@ -44,6 +45,7 @@ struct GroupChatInfoView: View {
|
||||
Section {
|
||||
if groupInfo.canEdit {
|
||||
editGroupButton()
|
||||
addOrEditWelcomeMessage()
|
||||
}
|
||||
groupPreferencesButton($groupInfo)
|
||||
} header: {
|
||||
@@ -107,7 +109,9 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
.onAppear {
|
||||
do {
|
||||
groupLink = try apiGetGroupLink(groupInfo.groupId)
|
||||
if let link = try apiGetGroupLink(groupInfo.groupId) {
|
||||
(groupLink, groupLinkMemberRole) = link
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
|
||||
}
|
||||
@@ -187,7 +191,7 @@ struct GroupChatInfoView: View {
|
||||
|
||||
private func groupLinkButton() -> some View {
|
||||
NavigationLink {
|
||||
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink)
|
||||
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole)
|
||||
.navigationBarTitle("Group link")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
@@ -212,6 +216,18 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func addOrEditWelcomeMessage() -> some View {
|
||||
NavigationLink {
|
||||
GroupWelcomeView(groupId: groupInfo.groupId, groupInfo: $groupInfo)
|
||||
.navigationTitle("Welcome message")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
groupInfo.groupProfile.description == nil
|
||||
? Label("Add welcome message", systemImage: "plus.message")
|
||||
: Label("Welcome message", systemImage: "message")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteGroupButton() -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .deleteGroupAlert
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct GroupLinkView: View {
|
||||
var groupId: Int64
|
||||
@Binding var groupLink: String?
|
||||
@Binding var groupLinkMemberRole: GroupMemberRole
|
||||
@State private var creatingLink = false
|
||||
@State private var alert: GroupLinkAlert?
|
||||
|
||||
@@ -33,6 +34,15 @@ struct GroupLinkView: View {
|
||||
Text("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.")
|
||||
.padding(.bottom)
|
||||
if let groupLink = groupLink {
|
||||
HStack {
|
||||
Text("Initial role")
|
||||
Picker("Initial role", selection: $groupLinkMemberRole) {
|
||||
ForEach([GroupMemberRole.member, GroupMemberRole.observer]) { role in
|
||||
Text(role.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
QRCode(uri: groupLink)
|
||||
HStack {
|
||||
Button {
|
||||
@@ -85,6 +95,16 @@ struct GroupLinkView: View {
|
||||
return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
.onChange(of: groupLinkMemberRole) { _ in
|
||||
Task {
|
||||
do {
|
||||
_ = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
|
||||
} catch let error {
|
||||
let a = getErrorAlert(error, "Error updating group link")
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if groupLink == nil && !creatingLink {
|
||||
createGroupLink()
|
||||
@@ -100,7 +120,7 @@ struct GroupLinkView: View {
|
||||
let link = try await apiCreateGroupLink(groupId)
|
||||
await MainActor.run {
|
||||
creatingLink = false
|
||||
groupLink = link
|
||||
(groupLink, groupLinkMemberRole) = link
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
|
||||
@@ -120,8 +140,8 @@ struct GroupLinkView_Previews: PreviewProvider {
|
||||
@State var noGroupLink: String? = nil
|
||||
|
||||
return Group {
|
||||
GroupLinkView(groupId: 1, groupLink: $groupLink)
|
||||
GroupLinkView(groupId: 1, groupLink: $noGroupLink)
|
||||
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))
|
||||
GroupLinkView(groupId: 1, groupLink: $noGroupLink, groupLinkMemberRole: Binding.constant(.member))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// GroupWelcomeView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Avently on 21/03/2022.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct GroupWelcomeView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
var groupId: Int64
|
||||
@Binding var groupInfo: GroupInfo
|
||||
@State private var welcomeText: String = ""
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
@State private var showSaveDialog = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
TextEditor(text: $welcomeText)
|
||||
.focused($keyboardVisible)
|
||||
.padding(.horizontal, -5)
|
||||
.padding(.top, -8)
|
||||
.frame(height: 90, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
Section {
|
||||
saveButton()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
welcomeText = groupInfo.groupProfile.description ?? ""
|
||||
}
|
||||
.modifier(BackButton {
|
||||
if welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil) {
|
||||
dismiss()
|
||||
} else {
|
||||
showSaveDialog = true
|
||||
}
|
||||
})
|
||||
.confirmationDialog("Save welcome message?", isPresented: $showSaveDialog) {
|
||||
Button("Save and update group profile") {
|
||||
save()
|
||||
dismiss()
|
||||
}
|
||||
Button("Exit without saving") { dismiss() }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func saveButton() -> some View {
|
||||
Button("Save and update group profile") {
|
||||
save()
|
||||
}
|
||||
.disabled(welcomeText == groupInfo.groupProfile.description || (welcomeText == "" && groupInfo.groupProfile.description == nil))
|
||||
}
|
||||
|
||||
private func save() {
|
||||
Task {
|
||||
do {
|
||||
var welcome: String? = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if welcome?.count == 0 {
|
||||
welcome = nil
|
||||
}
|
||||
var groupProfileUpdated = groupInfo.groupProfile
|
||||
groupProfileUpdated.description = welcome
|
||||
groupInfo = try await apiUpdateGroup(groupId, groupProfileUpdated)
|
||||
m.updateGroup(groupInfo)
|
||||
welcomeText = welcome ?? ""
|
||||
} catch let error {
|
||||
logger.error("apiUpdateGroup error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupWelcomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupWelcomeView(groupId: 1, groupInfo: Binding.constant(GroupInfo.sampleData))
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ struct ChatListView: View {
|
||||
.frame(width: 32, height: 32)
|
||||
.padding(.trailing, 4)
|
||||
let allRead = chatModel.users
|
||||
.filter { !$0.user.activeUser }
|
||||
.filter { u in !u.user.activeUser && !u.user.hidden }
|
||||
.allSatisfy { u in u.unreadCount == 0 }
|
||||
if !allRead {
|
||||
unreadBadge(size: 12)
|
||||
|
||||
@@ -29,7 +29,9 @@ struct UserPicker: View {
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
ScrollViewReader { sp in
|
||||
let users = m.users.sorted { u, _ in u.user.activeUser }
|
||||
let users = m.users
|
||||
.filter({ u in u.user.activeUser || !u.user.hidden })
|
||||
.sorted { u, _ in u.user.activeUser }
|
||||
VStack(spacing: 0) {
|
||||
ForEach(users) { u in
|
||||
userView(u)
|
||||
@@ -97,14 +99,18 @@ struct UserPicker: View {
|
||||
userPickerVisible.toggle()
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
try changeActiveUser_(user.userId)
|
||||
userPickerVisible = false
|
||||
} catch {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Error switching profile!",
|
||||
message: "Error: \(responseError(error))"
|
||||
)
|
||||
Task {
|
||||
do {
|
||||
try await changeActiveUserAsync_(user.userId, viewPwd: nil)
|
||||
await MainActor.run { userPickerVisible = false }
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Error switching profile!",
|
||||
message: "Error: \(responseError(error))"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
@@ -120,7 +126,9 @@ struct UserPicker: View {
|
||||
if user.activeUser {
|
||||
Image(systemName: "checkmark")
|
||||
} else if u.unreadCount > 0 {
|
||||
unreadCounter(u.unreadCount)
|
||||
unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary)
|
||||
} else if !user.showNtfs {
|
||||
Image(systemName: "speaker.slash")
|
||||
}
|
||||
}
|
||||
.padding(.trailing)
|
||||
@@ -146,13 +154,13 @@ struct UserPicker: View {
|
||||
}
|
||||
}
|
||||
|
||||
func unreadCounter(_ unread: Int) -> some View {
|
||||
private func unreadCounter(_ unread: Int, color: Color) -> some View {
|
||||
unreadCountText(unread)
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
.frame(minWidth: 18, minHeight: 18)
|
||||
.background(Color.accentColor)
|
||||
.background(color)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user