mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
312 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c90d911d2a | |||
| 2473d14baa | |||
| a36f2147d8 | |||
| 3837e92556 | |||
| 76505afff2 | |||
| 89c9a01b20 | |||
| 68517cf852 | |||
| fbbad55a0f | |||
| bcca27bfdb | |||
| c55a7692c5 | |||
| a8aa829e4c | |||
| d5af03ce18 | |||
| 101ef7a81a | |||
| 71daeed81a | |||
| d44324eb4d | |||
| 93d2ef66cf | |||
| 8a78943e94 | |||
| d0f0013755 | |||
| f22ee1a6cf | |||
| 4a58ca60ac | |||
| b206868730 | |||
| bd4c10b224 | |||
| 46d15d1811 | |||
| ea64be55e1 | |||
| c2cd58f63d | |||
| f21fc76ced | |||
| 13bd51b97d | |||
| a1ed0a84b8 | |||
| 4815e447fa | |||
| d80cad57b6 | |||
| a58be6ebb6 | |||
| dfa0272065 | |||
| 9723c47b25 | |||
| 3eb51eca58 | |||
| 86151d4ec2 | |||
| 717d05c4a3 | |||
| 3c43c5d254 | |||
| 0bae260fae | |||
| e694848cd5 | |||
| f5f61c5806 | |||
| 96c1c1d439 | |||
| 3e560278b6 | |||
| 6e131e0bad | |||
| bd158f3b0d | |||
| a96fb2f8d1 | |||
| 148261a1ee | |||
| 22b7aa90b2 | |||
| 88d9e70ef8 | |||
| a0cfc19063 | |||
| 451aab46dc | |||
| 3b3db562cd | |||
| 15ed6ea4a6 | |||
| 9f661ff4e2 | |||
| 52c39c9641 | |||
| 0dab486ac8 | |||
| d640f4a5d5 | |||
| 1c47bfbf44 | |||
| db3fc4ee7b | |||
| 74df35d3b0 | |||
| c01c629f73 | |||
| 25e4a1e86d | |||
| e27013071b | |||
| 2679bc2e94 | |||
| 93ab713748 | |||
| bc1d86e303 | |||
| b386346cf1 | |||
| b6db41dd50 | |||
| 6bca013e67 | |||
| e538826cd8 | |||
| a7a56ea1d9 | |||
| 77d0f70270 | |||
| 2a20f78877 | |||
| b027708828 | |||
| a0bf298b66 | |||
| e3b22d83ad | |||
| ab4e4e1db9 | |||
| a393bc8163 | |||
| 3bc4fd222c | |||
| 1b01dcec6d | |||
| 1d5c361b9a | |||
| 4cd396a0d2 | |||
| bcc80be8e9 | |||
| 5d12217eab | |||
| 4b0046a60b | |||
| 114b76e3f8 | |||
| c72aa5d074 | |||
| 2e9882b0bd | |||
| 8ff8f9d695 | |||
| 1e3c2024bb | |||
| 8bec0004cc | |||
| c337a6888d | |||
| e72d4638d2 | |||
| 7dd4dc3b40 | |||
| 980c7a9ddd | |||
| cb5f26d354 | |||
| 04d886e546 | |||
| ed12ccaac2 | |||
| e9e9286fbb | |||
| f4ffa5237c | |||
| ef15dca0b4 | |||
| 396b3ae639 | |||
| c409f58067 | |||
| 69ca731641 | |||
| 006a30e65c | |||
| 0a9aa8b3d2 | |||
| 69196084fc | |||
| cf4105e256 | |||
| ad6aa10cd2 | |||
| ba29d0242e | |||
| ca64ed9784 | |||
| a227e21fcf | |||
| 7c9c358ce2 | |||
| 84237f79fc | |||
| 80e0bfb61f | |||
| 153f80fe64 | |||
| 19881703e5 | |||
| a668bd5736 | |||
| e8cab01c03 | |||
| 3f72633d22 | |||
| 5c7ad0926c | |||
| 7eca44bb84 | |||
| 2f39cfd86f | |||
| 2fdc23274d | |||
| 91a39cae23 | |||
| 882966d5d3 | |||
| 3ed5e6e50b | |||
| df6cec6a32 | |||
| 24c47657f4 | |||
| cf6afb7687 | |||
| 774af334fd | |||
| 9dc6c1327f | |||
| af414d7f6e | |||
| a040fa65bb | |||
| 9fc26ca799 | |||
| 9dad55ce8d | |||
| 6e0addbea3 | |||
| e452edb781 | |||
| a3283708e7 | |||
| e833d66557 | |||
| 71f5b51350 | |||
| 20cec4db11 | |||
| 2085dc5d60 | |||
| 9290fcc6b2 | |||
| 0c3d643408 | |||
| 6d5c3ae484 | |||
| cccdcef914 | |||
| e63e158b2d | |||
| 892b91e958 | |||
| fb04108b11 | |||
| 424328b9d1 | |||
| e73f5c40cf | |||
| 4c960bdc44 | |||
| dcb82951ed | |||
| 138cce4436 | |||
| 98417dafc4 | |||
| e8374be19c | |||
| 62a2f61751 | |||
| 7323bb4333 | |||
| 2d47175f94 | |||
| a6d7604d21 | |||
| 9e3573fc76 | |||
| 41e873d5ca | |||
| 13ebaf587e | |||
| 61e20550bc | |||
| d1cc5c1769 | |||
| 16b041c8c6 | |||
| 810f248c74 | |||
| 813fecddfe | |||
| 5a7d61c964 | |||
| ad1b091b18 | |||
| cba24983e6 | |||
| 4dc2a1b72d | |||
| 3d4e4e2ef9 | |||
| 113c67ec95 | |||
| a2e887024f | |||
| 37262b3ed5 | |||
| dca4fe7701 | |||
| 88c9334d18 | |||
| 58f06aa821 | |||
| ae5deab8d3 | |||
| bb0482104c | |||
| edfece3206 | |||
| c32cf8055d | |||
| 72ec03a822 | |||
| d89e0efedd | |||
| 707e8592d9 | |||
| fa9e0086f6 | |||
| a1b27e9a99 | |||
| f68d8fd97c | |||
| abff42a264 | |||
| c1ced70836 | |||
| e14966d36e | |||
| 97943fc609 | |||
| 0f143b2e77 | |||
| be10dcbcfc | |||
| 97dbec927c | |||
| b4879ca2a3 | |||
| 15884c0169 | |||
| 9f2d5486b6 | |||
| 80f0108b41 | |||
| c37a7ebfe7 | |||
| 02c2c65d41 | |||
| 7c4700b238 | |||
| 54190ffff9 | |||
| 17eed9662e | |||
| bb116bccb4 | |||
| 6cc267689e | |||
| 0e6909845f | |||
| 96ad9faa85 | |||
| 768c497025 | |||
| 3ec29d8ef4 | |||
| 6c4b92531f | |||
| 46d6159da5 | |||
| aab6e1c52f | |||
| c0a01318b5 | |||
| 13090ff6ed | |||
| 90a20cd52f | |||
| 74245d3f2b | |||
| e48452ccff | |||
| 39370ba1ef | |||
| a02cfb4f41 | |||
| 4370012b8a | |||
| 20c33aea72 | |||
| c11a1aa0e6 | |||
| 166b789f3c | |||
| bbc26e272c | |||
| 6c839f8075 | |||
| be91f97c83 | |||
| e085cb7350 | |||
| 12574bed96 | |||
| 2137893111 | |||
| e552a28a4d | |||
| b20031d875 | |||
| 558b3fa356 | |||
| cb337cef10 | |||
| cd63f81292 | |||
| 6205b03943 | |||
| 82924ce8c6 | |||
| b1067c339c | |||
| 0d6e4b48f6 | |||
| 84d2c408ce | |||
| de434b730e | |||
| 1251dbc4b0 | |||
| d115ad228b | |||
| 28d6f62b74 | |||
| 2b9238144b | |||
| a2e1b7ae0a | |||
| a00bb6d5ef | |||
| da12b651e4 | |||
| a936c14cf2 | |||
| e6aad24e5f | |||
| 8dac96f415 | |||
| aae0802ec8 | |||
| 74a20ef70c | |||
| a2a29628a7 | |||
| 0b046315ac | |||
| 372d7ffaa9 | |||
| ece928d57e | |||
| e1740a8be4 | |||
| 36eba01ef4 | |||
| 9e045a44db | |||
| b7d42ef889 | |||
| e55cd82ec3 | |||
| 34e08b2058 | |||
| 5e9b7366cc | |||
| 64fb1f0b85 | |||
| 84e43c57f6 | |||
| ffa37b1684 | |||
| 86271fe109 | |||
| 5dab099b5c | |||
| 199e61e5c6 | |||
| 76b4fd34c1 | |||
| b159496257 | |||
| c0fb29d5f7 | |||
| 4ab7e5e1c8 | |||
| 9e847c2e1f | |||
| d105e59655 | |||
| f128ebac87 | |||
| b4de9c266b | |||
| e410fc7736 | |||
| f5bd6eb4c3 | |||
| cee403c1ed | |||
| 8786e2147a | |||
| 6b8705e9f4 | |||
| acfb98bd81 | |||
| c240456b80 | |||
| 9e1641a154 | |||
| 17cd3cdca4 | |||
| aa264690ab | |||
| 0e837ae392 | |||
| 68525b4131 | |||
| 8775db7c97 | |||
| f266debd56 | |||
| 044c7a8191 | |||
| 677c6aeb2e | |||
| 7b8f5be821 | |||
| 21765905a7 | |||
| 70a9c01477 | |||
| 678dbec3e2 | |||
| bd4c7dffbf | |||
| 1eb4030080 | |||
| bcc64442e9 | |||
| 1246b9e376 | |||
| d6e9a87d58 | |||
| cddd3cd673 | |||
| e00ef7c7da | |||
| 1a201cfadf | |||
| a4ecb41743 | |||
| e347f5329c | |||
| 741b3e8848 | |||
| 7b4710d198 | |||
| c77f6100c5 |
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- sqlcipher
|
||||
- users
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 20
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ stack.yaml.lock
|
||||
|
||||
# Temporary test files
|
||||
tests/tmp
|
||||
tests/tmp*
|
||||
logs/
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
[](https://github.com/simplex-chat/simplex-chat/releases)
|
||||
[](https://www.reddit.com/r/SimpleXChat)
|
||||
[](https://mastodon.social/@simplex)
|
||||
[](https://twitter.com/SimpleXChat)
|
||||
|
||||
[<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)
|
||||
|
||||
@@ -44,6 +43,7 @@
|
||||
- [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)
|
||||
@@ -86,13 +86,15 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent updates:
|
||||
|
||||
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration.](./blog/20221206-simplex-chat-v4.3-voice-messages.md)
|
||||
[Jan 03, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||
|
||||
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md)
|
||||
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md)
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
[Sep 1, 2022. v3.2: incognito mode, support .onion server hostnames, setting contact names, changing color scheme, etc. Implementation audit is arranged for October!](./blog/20220901-simplex-chat-v3.2-incognito-mode.md).
|
||||
|
||||
[All updates](./blog)
|
||||
|
||||
@@ -149,7 +151,7 @@ What is already implemented:
|
||||
|
||||
We plan to add soon:
|
||||
|
||||
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers termporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`.
|
||||
3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
|
||||
|
||||
@@ -191,17 +193,21 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- ✅ View deleted messages, full message deletion by sender (with recipient opt-in per contact).
|
||||
- ✅ Block screenshots and view in recent apps.
|
||||
- ✅ Advanced server configuration.
|
||||
- ✅ Disappearing messages (with recipient opt-in per-contact).
|
||||
- ✅ "Live" messages.
|
||||
- ✅ Contact verification via a separate out-of-band channel.
|
||||
- 🏗 Multiple user profiles in the same chat database.
|
||||
- 🏗 Optionally avoid re-using the same TCP session for multiple connections.
|
||||
- 🏗 File server to optimize for efficient and private sending of large files.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Contact verification via a separate out-of-band channel.
|
||||
- 🏗 Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Optionally avoid re-using the same TCP session for multiple connections.
|
||||
- 🏗 Reduced battery and traffic usage in large groups.
|
||||
- 🏗 Preserve message drafts.
|
||||
- 🏗 Support older Android OS and 32-bit CPUs.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Access password/pin (with optional alternative access password).
|
||||
- Media server to optimize sending large files to groups.
|
||||
- Video messages.
|
||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- Multiple user profiles in the same chat database.
|
||||
- Feeds/broadcasts.
|
||||
- Unconfirmed: disappearing messages (with recipient opt-in per-contact).
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
||||
- Supporting the same profile on multiple devices.
|
||||
@@ -213,24 +219,44 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
|
||||
## Join a user group
|
||||
|
||||
You can join a general group with more than 100 members: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FWHV0YU1sYlU7NqiEHkHDB6gxO1ofTync%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAWbebOqVYuBXaiqHcXYjEHCpYi6VzDlu6CVaijDTmsQU%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22mL-7Divb94GGmGmRBef5Dg%3D%3D%22%7D).
|
||||
You can join a general English-speaking group: [#SimpleX-Group](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FcIS0gu1h0Y8pZpQkDaSz7HZGSHcKpMB9%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAKzzWAJYrVt1zdgRp4pD3FBst6eK7233DJeNElENLJRA%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%228mazMhefXoM5HxWBfZnvwQ%3D%3D%22%7D). Just bear in mind that it has ~300 members now, and that it is fully decentralized, so sending a message and connecting to all members in this group will take some time, only join it if you:
|
||||
- want to see how larger groups work.
|
||||
- traffic is not a concern (sending each message is ~5mb).
|
||||
|
||||
You can also join smaller groups by countries/languages: [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FmIorjTDPG24jdLKXwutS6o9hdQQRZwfQ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA9N0BZaECrAw3we3S1Wq4QO7NERBuPt9447immrB50wo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22S8aISlOgkTMytSox9gAM2Q%3D%3D%22%7D) (German), [\#SimpleX-US](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FlTWmQplLEaoJyHnEL1-B3f2PtDsikcTs%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-hMBlsQjNxK2vaVhqW_UyAVtuoYqgYTigK4B9dJ9CGc%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22G0UtRHIn0TmPoo08h_cbTA%3D%3D%22%7D) (US/English), [\#SimpleX-France](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F11r6XyjwVMj0WDIUMbmNDXO996M_EN_1%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAXDmc2Lrj9WQOjEcWa0DeQHF3HcYOp9b68s8M_BJ7gEk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22EZCeSYpeIBkaQwCcpcF00w%3D%3D%22%7D), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FZSYM278L5WoZiApx3925EAjSXcsAVNVu%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA7RJ2wfT8zdfOLyE5OtWLEAPowj-q6F2HB0ExbATw8Gk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22fsVoklNGptt7n-droqJYUQ%3D%3D%22%7D) (Russian), [#SimpleX-NL](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmP0LbswSbfxoVkkxiWE2NYnBCgZ9Snvj%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAVwZuSsw4Mf52EaBNdNI3RebsLm0jg65ZIkcmH9E5uy8%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22M9xIULUNZx51Wsa5Kdb0Sg%3D%3D%22%7D) (Netherlands/Dutch), [#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaZ_wjh6QAYHB-LjyGtp8bllkzoq880u-%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA-_Wulzc3j16i7t77XJ5wgwxeW8_Ea8GxetMo7K4MgjI%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22QWmXdrFzIeMd2OoEPMFkBQ%3D%3D%22%7D) (Italian).
|
||||
You can also join a new and smaller English-speaking group if you want to ask questions without too much traffic: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D)
|
||||
|
||||
There are also several groups in languages other than English, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users. We do not always answer questions there, so please ask them in one of the English-speaking groups.
|
||||
|
||||
- [\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking).
|
||||
- [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking).
|
||||
- [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking).
|
||||
- [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
|
||||
|
||||
You can join these groups either by opening these links in the app or by opening them in a desktop browser and scanning QR code.
|
||||
|
||||
Let us know if you'd like to add some other countries to the list.
|
||||
|
||||
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: link to be added
|
||||
Italian: [@unbranched](https://github.com/unbranched)
|
||||
Russian: project team
|
||||
|
||||
Languages in progress: Chinese, Hindi, 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 and get in touch with us!
|
||||
|
||||
## Contribute
|
||||
|
||||
We would love to have you join the development! You can contribute to SimpleX Chat with:
|
||||
|
||||
- developing features - please connect to us via chat so we can help you get started.
|
||||
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
|
||||
- translate UI to some language - we are currently setting up the UI to simplify it, please get in touch and let us know if you would be able to support and update the translations.
|
||||
- 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
|
||||
|
||||
@@ -250,6 +276,7 @@ It is possible to donate via:
|
||||
- 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,
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 78
|
||||
versionName "4.3.2"
|
||||
versionCode 98
|
||||
versionName "4.5"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
|
||||
@@ -22,6 +22,7 @@ var TransformOperation;
|
||||
TransformOperation["Decrypt"] = "decrypt";
|
||||
})(TransformOperation || (TransformOperation = {}));
|
||||
let activeCall;
|
||||
let answerTimeout = 30000;
|
||||
const processCommand = (function () {
|
||||
const defaultIceServers = [
|
||||
{ urls: ["stun:stun.simplex.im:443"] },
|
||||
@@ -100,9 +101,16 @@ const processCommand = (function () {
|
||||
const iceCandidates = getIceCandidates(pc, config);
|
||||
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
|
||||
await setupMediaStreams(call);
|
||||
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
|
||||
pc.addEventListener("connectionstatechange", connectionStateChange);
|
||||
return call;
|
||||
async function connectionStateChange() {
|
||||
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
|
||||
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
|
||||
if (pc.connectionState !== "failed")
|
||||
connectionHandler();
|
||||
}
|
||||
async function connectionHandler() {
|
||||
sendMessageToNative({
|
||||
resp: {
|
||||
type: "connection",
|
||||
@@ -115,6 +123,7 @@ const processCommand = (function () {
|
||||
},
|
||||
});
|
||||
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
||||
clearConnectionTimeout();
|
||||
pc.removeEventListener("connectionstatechange", connectionStateChange);
|
||||
if (activeCall) {
|
||||
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
|
||||
@@ -122,6 +131,7 @@ const processCommand = (function () {
|
||||
endCall();
|
||||
}
|
||||
else if (pc.connectionState == "connected") {
|
||||
clearConnectionTimeout();
|
||||
const stats = (await pc.getStats());
|
||||
for (const stat of stats.values()) {
|
||||
const { type, state } = stat;
|
||||
@@ -141,6 +151,12 @@ const processCommand = (function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
function clearConnectionTimeout() {
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
function serialize(x) {
|
||||
return LZString.compressToBase64(JSON.stringify(x));
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.*
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.SplashView
|
||||
@@ -42,7 +43,6 @@ import chat.simplex.app.views.newchat.*
|
||||
import chat.simplex.app.views.onboarding.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainActivity: FragmentActivity() {
|
||||
@@ -374,13 +374,6 @@ fun MainPage(
|
||||
.collect {
|
||||
if (it != null) currentChatId = it
|
||||
else onComposed()
|
||||
|
||||
// Deletes files that were not sent but already stored in files directory.
|
||||
// Currently, it's voice records only
|
||||
if (it == null && chatModel.filesToDelete.isNotEmpty()) {
|
||||
chatModel.filesToDelete.forEach { it.delete() }
|
||||
chatModel.filesToDelete.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,7 +387,8 @@ fun MainPage(
|
||||
}
|
||||
}
|
||||
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
|
||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel)
|
||||
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
|
||||
onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
|
||||
}
|
||||
ModalManager.shared.showInView()
|
||||
val invitation = chatModel.activeCallInvitation.value
|
||||
@@ -404,20 +398,31 @@ fun MainPage(
|
||||
}
|
||||
|
||||
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
val userId = getUserIdFromIntent(intent)
|
||||
when (intent?.action) {
|
||||
NtfManager.OpenChatAction -> {
|
||||
val chatId = intent.getStringExtra("chatId")
|
||||
Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId")
|
||||
if (chatId != null) {
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
chatModel.clearOverlays.value = true
|
||||
if (cInfo != null) withApi { openChat(cInfo, chatModel) }
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
chatModel.controller.changeActiveUser(userId)
|
||||
}
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
chatModel.clearOverlays.value = true
|
||||
if (cInfo != null) openChat(cInfo, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
NtfManager.ShowChatsAction -> {
|
||||
Log.d(TAG, "processNotificationIntent: ShowChatsAction")
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
chatModel.controller.changeActiveUser(userId)
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
}
|
||||
}
|
||||
NtfManager.AcceptCallAction -> {
|
||||
val chatId = intent.getStringExtra("chatId")
|
||||
@@ -478,7 +483,6 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
// TODO open from chat list view
|
||||
chatModel.appOpenUrl.value = uri
|
||||
} else {
|
||||
withUriAction(uri) { linkType ->
|
||||
|
||||
@@ -39,7 +39,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
var isAppOnForeground: Boolean = false
|
||||
|
||||
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() ?: ""
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
@@ -90,6 +90,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
context = this
|
||||
initChatController()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
@@ -115,8 +116,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
* after calling [ChatController.showBackgroundServiceNoticeIfNeeded] notification mode in prefs can be changed.
|
||||
* It can happen when app was started and a user enables battery optimization while app in background
|
||||
* */
|
||||
if (chatModel.chatRunning.value != false && appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
|
||||
if (chatModel.chatRunning.value != false &&
|
||||
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
|
||||
appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name
|
||||
) {
|
||||
SimplexService.start(applicationContext)
|
||||
}
|
||||
}
|
||||
else -> isAppOnForeground = false
|
||||
}
|
||||
|
||||
@@ -2,18 +2,23 @@ package chat.simplex.app.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.*
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.call.*
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.usersettings.NotificationPreviewMode
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.*
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
@@ -21,6 +26,8 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.*
|
||||
|
||||
/*
|
||||
* Without this annotation an animation from ChatList to ChatView has 1 frame per the whole animation. Don't delete it
|
||||
@@ -29,6 +36,7 @@ import java.io.File
|
||||
class ChatModel(val controller: ChatController) {
|
||||
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
|
||||
val currentUser = mutableStateOf<User?>(null)
|
||||
val users = mutableStateListOf<UserInfo>()
|
||||
val userCreated = mutableStateOf<Boolean?>(null)
|
||||
val chatRunning = mutableStateOf<Boolean?>(null)
|
||||
val chatDbChanged = mutableStateOf<Boolean>(false)
|
||||
@@ -36,6 +44,8 @@ class ChatModel(val controller: ChatController) {
|
||||
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
|
||||
val chatDbDeleted = mutableStateOf(false)
|
||||
val chats = mutableStateListOf<Chat>()
|
||||
// map of connections network statuses, key is agent connection id
|
||||
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
|
||||
|
||||
// current chat
|
||||
val chatId = mutableStateOf<String?>(null)
|
||||
@@ -75,19 +85,15 @@ class ChatModel(val controller: ChatController) {
|
||||
// currently showing QR code
|
||||
val connReqInv = mutableStateOf(null as String?)
|
||||
|
||||
var draft = mutableStateOf(null as ComposeState?)
|
||||
var draftChatId = mutableStateOf(null as String?)
|
||||
|
||||
// working with external intents
|
||||
val sharedContent = mutableStateOf(null as SharedContent?)
|
||||
|
||||
val filesToDelete = mutableSetOf<File>()
|
||||
val simplexLinkMode = mutableStateOf(controller.appPrefs.simplexLinkMode.get())
|
||||
|
||||
fun updateUserProfile(profile: LocalProfile) {
|
||||
val user = currentUser.value
|
||||
if (user != null) {
|
||||
currentUser.value = user.copy(profile = profile)
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
@@ -101,7 +107,7 @@ class ChatModel(val controller: ChatController) {
|
||||
|
||||
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directContact)
|
||||
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
|
||||
|
||||
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
|
||||
|
||||
@@ -114,17 +120,8 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
|
||||
fun updateChats(newChats: List<Chat>) {
|
||||
val mergedChats = arrayListOf<Chat>()
|
||||
for (newChat in newChats) {
|
||||
val i = getChatIndex(newChat.chatInfo.id)
|
||||
if (i >= 0) {
|
||||
mergedChats.add(newChat.copy(serverInfo = chats[i].serverInfo))
|
||||
} else {
|
||||
mergedChats.add(newChat)
|
||||
}
|
||||
}
|
||||
chats.clear()
|
||||
chats.addAll(mergedChats)
|
||||
chats.addAll(newChats)
|
||||
|
||||
val cId = chatId.value
|
||||
// If chat is null, it was deleted in background after apiGetChats call
|
||||
@@ -133,14 +130,6 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNetworkStatus(id: ChatId, status: Chat.NetworkStatus) {
|
||||
val i = getChatIndex(id)
|
||||
if (i >= 0) {
|
||||
val chat = chats[i]
|
||||
chats[i] = chat.copy(serverInfo = chat.serverInfo.copy(networkStatus = status))
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceChat(id: String, chat: Chat) {
|
||||
val i = getChatIndex(id)
|
||||
if (i >= 0) {
|
||||
@@ -151,7 +140,7 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
}
|
||||
|
||||
fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
// update previews
|
||||
val i = getChatIndex(cInfo.id)
|
||||
val chat: Chat
|
||||
@@ -162,6 +151,7 @@ class ChatModel(val controller: ChatController) {
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId
|
||||
increaseUnreadCounter(currentUser.value!!)
|
||||
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, minUnreadItemId = minUnreadId)
|
||||
}
|
||||
else
|
||||
@@ -175,11 +165,17 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItems.add(cItem)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
|
||||
suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean {
|
||||
// update previews
|
||||
val i = getChatIndex(cInfo.id)
|
||||
val chat: Chat
|
||||
@@ -206,7 +202,9 @@ class ChatModel(val controller: ChatController) {
|
||||
chatItems[itemIndex] = cItem
|
||||
return false
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
withContext(Dispatchers.Main) {
|
||||
chatItems.add(cItem)
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
@@ -242,6 +240,7 @@ class ChatModel(val controller: ChatController) {
|
||||
// clear preview
|
||||
val i = getChatIndex(cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
}
|
||||
// clear current chat
|
||||
@@ -250,6 +249,33 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCurrentUser(newProfile: Profile, preferences: FullChatPreferences? = null) {
|
||||
val current = currentUser.value ?: return
|
||||
val updated = current.copy(
|
||||
profile = newProfile.toLocalProfile(current.profile.profileId),
|
||||
fullPreferences = preferences ?: current.fullPreferences
|
||||
)
|
||||
val indexInUsers = users.indexOfFirst { it.user.userId == current.userId }
|
||||
if (indexInUsers != -1) {
|
||||
users[indexInUsers] = UserInfo(updated, users[indexInUsers].unreadCount)
|
||||
}
|
||||
currentUser.value = updated
|
||||
}
|
||||
|
||||
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
|
||||
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
|
||||
withContext(Dispatchers.Main) {
|
||||
chatItems.add(cItem)
|
||||
}
|
||||
return cItem
|
||||
}
|
||||
|
||||
fun removeLiveDummy() {
|
||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatItemsRead(cInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) {
|
||||
val markedRead = markItemsReadInCurrentChat(cInfo, range)
|
||||
// update preview
|
||||
@@ -258,9 +284,11 @@ class ChatModel(val controller: ChatController) {
|
||||
val chat = chats[chatIdx]
|
||||
val lastId = chat.chatItems.lastOrNull()?.id
|
||||
if (lastId != null) {
|
||||
val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0,
|
||||
unreadCount = unreadCount,
|
||||
// Can't use minUnreadItemId currently since chat items can have unread items between read items
|
||||
//minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1
|
||||
)
|
||||
@@ -276,7 +304,13 @@ class ChatModel(val controller: ChatController) {
|
||||
while (i < chatItems.count()) {
|
||||
val item = chatItems[i]
|
||||
if (item.meta.itemStatus is CIStatus.RcvNew && (range == null || (range.from <= item.id && item.id <= range.to))) {
|
||||
chatItems[i] = item.withStatus(CIStatus.RcvRead())
|
||||
val newItem = item.withStatus(CIStatus.RcvRead())
|
||||
chatItems[i] = newItem
|
||||
if (newItem.meta.itemLive != true && newItem.meta.itemTimed?.ttl != null) {
|
||||
chatItems[i] = newItem.copy(meta = newItem.meta.copy(itemTimed = newItem.meta.itemTimed.copy(
|
||||
deleteAt = Clock.System.now() + newItem.meta.itemTimed.ttl.toDuration(DurationUnit.SECONDS)))
|
||||
)
|
||||
}
|
||||
markedRead++
|
||||
}
|
||||
i += 1
|
||||
@@ -290,13 +324,30 @@ class ChatModel(val controller: ChatController) {
|
||||
if (chatIndex == -1) return
|
||||
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
decreaseUnreadCounter(currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0),
|
||||
unreadCount = unreadCount,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun increaseUnreadCounter(user: User) {
|
||||
changeUnreadCounter(user, 1)
|
||||
}
|
||||
|
||||
fun decreaseUnreadCounter(user: User, by: Int = 1) {
|
||||
changeUnreadCounter(user, -by)
|
||||
}
|
||||
|
||||
private fun changeUnreadCounter(user: User, by: Int) {
|
||||
val i = users.indexOfFirst { it.user.userId == user.userId }
|
||||
if (i != -1) {
|
||||
users[i] = UserInfo(user, users[i].unreadCount + by)
|
||||
}
|
||||
}
|
||||
|
||||
// func popChat(_ id: String) {
|
||||
// if let i = getChatIndex(id) {
|
||||
// popChat_(i)
|
||||
@@ -341,6 +392,20 @@ class ChatModel(val controller: ChatController) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
|
||||
networkStatuses[contact.activeConn.agentConnId] = status
|
||||
}
|
||||
|
||||
fun contactNetworkStatus(contact: Contact): NetworkStatus =
|
||||
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
|
||||
|
||||
fun addTerminalItem(item: TerminalItem) {
|
||||
if (terminalItems.size >= 500) {
|
||||
terminalItems.removeAt(0)
|
||||
}
|
||||
terminalItems.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
enum class ChatType(val type: String) {
|
||||
@@ -376,6 +441,19 @@ data class User(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserInfo(
|
||||
val user: User,
|
||||
val unreadCount: Int
|
||||
) {
|
||||
companion object {
|
||||
val sampleData = UserInfo(
|
||||
user = User.sampleData,
|
||||
unreadCount = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
typealias ChatId = String
|
||||
|
||||
interface NamedChat {
|
||||
@@ -396,8 +474,8 @@ interface SomeChat {
|
||||
val sendMsgEnabled: Boolean
|
||||
val ntfsEnabled: Boolean
|
||||
val incognito: Boolean
|
||||
val voiceMessageAllowed: Boolean
|
||||
val fullDeletionAllowed: Boolean
|
||||
fun featureEnabled(feature: ChatFeature): Boolean
|
||||
val timedMessagesTTL: Int?
|
||||
val createdAt: Instant
|
||||
val updatedAt: Instant
|
||||
}
|
||||
@@ -407,37 +485,12 @@ data class Chat (
|
||||
val chatInfo: ChatInfo,
|
||||
val chatItems: List<ChatItem>,
|
||||
val chatStats: ChatStats = ChatStats(),
|
||||
val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown())
|
||||
) {
|
||||
val id: String get() = chatInfo.id
|
||||
|
||||
@Serializable
|
||||
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data class ServerInfo(val networkStatus: NetworkStatus)
|
||||
|
||||
@Serializable
|
||||
sealed class NetworkStatus {
|
||||
val statusString: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.server_connected)
|
||||
is Error -> generalGetString(R.string.server_error)
|
||||
else -> generalGetString(R.string.server_connecting)
|
||||
}
|
||||
val statusExplanation: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
|
||||
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
|
||||
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
|
||||
}
|
||||
|
||||
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
|
||||
@Serializable @SerialName("connected") class Connected: NetworkStatus()
|
||||
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
||||
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val sampleData = Chat(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
@@ -459,8 +512,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val sendMsgEnabled get() = contact.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contact.ntfsEnabled
|
||||
override val incognito get() = contact.incognito
|
||||
override val voiceMessageAllowed get() = contact.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = contact.fullDeletionAllowed
|
||||
override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature)
|
||||
override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL
|
||||
override val createdAt get() = contact.createdAt
|
||||
override val updatedAt get() = contact.updatedAt
|
||||
override val displayName get() = contact.displayName
|
||||
@@ -483,8 +536,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val sendMsgEnabled get() = groupInfo.sendMsgEnabled
|
||||
override val ntfsEnabled get() = groupInfo.ntfsEnabled
|
||||
override val incognito get() = groupInfo.incognito
|
||||
override val voiceMessageAllowed get() = groupInfo.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = groupInfo.fullDeletionAllowed
|
||||
override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature)
|
||||
override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL
|
||||
override val createdAt get() = groupInfo.createdAt
|
||||
override val updatedAt get() = groupInfo.updatedAt
|
||||
override val displayName get() = groupInfo.displayName
|
||||
@@ -507,8 +560,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val sendMsgEnabled get() = contactRequest.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contactRequest.ntfsEnabled
|
||||
override val incognito get() = contactRequest.incognito
|
||||
override val voiceMessageAllowed get() = contactRequest.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = contactRequest.fullDeletionAllowed
|
||||
override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature)
|
||||
override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL
|
||||
override val createdAt get() = contactRequest.createdAt
|
||||
override val updatedAt get() = contactRequest.updatedAt
|
||||
override val displayName get() = contactRequest.displayName
|
||||
@@ -531,8 +584,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
override val sendMsgEnabled get() = contactConnection.sendMsgEnabled
|
||||
override val ntfsEnabled get() = contactConnection.incognito
|
||||
override val incognito get() = contactConnection.incognito
|
||||
override val voiceMessageAllowed get() = contactConnection.voiceMessageAllowed
|
||||
override val fullDeletionAllowed get() = contactConnection.fullDeletionAllowed
|
||||
override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature)
|
||||
override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL
|
||||
override val createdAt get() = contactConnection.createdAt
|
||||
override val updatedAt get() = contactConnection.updatedAt
|
||||
override val displayName get() = contactConnection.displayName
|
||||
@@ -545,6 +598,51 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
ContactConnection(PendingContactConnection.getSampleData(status, viaContactUri))
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable @SerialName("invalidJSON")
|
||||
class InvalidJSON(val json: String): ChatInfo() {
|
||||
override val chatType get() = ChatType.Direct
|
||||
override val localDisplayName get() = invalidChatName
|
||||
override val id get() = ""
|
||||
override val apiId get() = 0L
|
||||
override val ready get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = false
|
||||
override fun featureEnabled(feature: ChatFeature) = false
|
||||
override val timedMessagesTTL: Int? get() = null
|
||||
override val createdAt get() = Clock.System.now()
|
||||
override val updatedAt get() = Clock.System.now()
|
||||
override val displayName get() = invalidChatName
|
||||
override val fullName get() = invalidChatName
|
||||
override val image get() = null
|
||||
override val localAlias get() = ""
|
||||
|
||||
companion object {
|
||||
private val invalidChatName = generalGetString(R.string.invalid_chat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class NetworkStatus {
|
||||
val statusString: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.server_connected)
|
||||
is Error -> generalGetString(R.string.server_error)
|
||||
else -> generalGetString(R.string.server_connecting)
|
||||
}
|
||||
val statusExplanation: String get() =
|
||||
when (this) {
|
||||
is Connected -> generalGetString(R.string.connected_to_server_to_receive_messages_from_contact)
|
||||
is Error -> String.format(generalGetString(R.string.trying_to_connect_to_server_to_receive_messages_with_error), error)
|
||||
else -> generalGetString(R.string.trying_to_connect_to_server_to_receive_messages)
|
||||
}
|
||||
|
||||
@Serializable @SerialName("unknown") class Unknown: NetworkStatus()
|
||||
@Serializable @SerialName("connected") class Connected: NetworkStatus()
|
||||
@Serializable @SerialName("disconnected") class Disconnected: NetworkStatus()
|
||||
@Serializable @SerialName("error") class Error(val error: String): NetworkStatus()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -568,19 +666,36 @@ data class Contact(
|
||||
override val sendMsgEnabled get() = true
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||
override val incognito get() = contactConnIncognito
|
||||
override val voiceMessageAllowed get() = mergedPreferences.voice.enabled.forUser
|
||||
override val fullDeletionAllowed get() = mergedPreferences.fullDelete.enabled.forUser
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
|
||||
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
|
||||
}
|
||||
override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null }
|
||||
override val displayName get() = localAlias.ifEmpty { profile.displayName }
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
override val localAlias get() = profile.localAlias
|
||||
val verified get() = activeConn.connectionCode != null
|
||||
|
||||
val directContact: Boolean get() =
|
||||
val directOrUsed: Boolean get() =
|
||||
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
|
||||
|
||||
val contactConnIncognito =
|
||||
activeConn.customUserProfileId != null
|
||||
|
||||
fun allowsFeature(feature: ChatFeature): Boolean = when (feature) {
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
|
||||
}
|
||||
|
||||
fun userAllowsFeature(feature: ChatFeature): Boolean = when (feature) {
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO
|
||||
}
|
||||
|
||||
companion object {
|
||||
val sampleData = Contact(
|
||||
contactId = 1,
|
||||
@@ -600,6 +715,8 @@ data class Contact(
|
||||
@Serializable
|
||||
class ContactRef(
|
||||
val contactId: Long,
|
||||
val agentConnId: String,
|
||||
val connId: Long,
|
||||
var localDisplayName: String
|
||||
) {
|
||||
val id: ChatId get() = "@$contactId"
|
||||
@@ -612,13 +729,24 @@ class ContactSubStatus(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val viaGroupLink: Boolean, val customUserProfileId: Long? = null) {
|
||||
data class Connection(
|
||||
val connId: Long,
|
||||
val agentConnId: String,
|
||||
val connStatus: ConnStatus,
|
||||
val connLevel: Int,
|
||||
val viaGroupLink: Boolean,
|
||||
val customUserProfileId: Long? = null,
|
||||
val connectionCode: SecurityCode? = null
|
||||
) {
|
||||
val id: ChatId get() = ":$connId"
|
||||
companion object {
|
||||
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
|
||||
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SecurityCode(val securityCode: String, val verifiedAt: Instant)
|
||||
|
||||
@Serializable
|
||||
data class Profile(
|
||||
override val displayName: String,
|
||||
@@ -691,8 +819,12 @@ data class GroupInfo (
|
||||
override val sendMsgEnabled get() = membership.memberActive
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||
override val incognito get() = membership.memberIncognito
|
||||
override val voiceMessageAllowed get() = fullGroupPreferences.voice.on
|
||||
override val fullDeletionAllowed get() = fullGroupPreferences.fullDelete.on
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
|
||||
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
|
||||
ChatFeature.Voice -> fullGroupPreferences.voice.on
|
||||
}
|
||||
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
|
||||
override val displayName get() = groupProfile.displayName
|
||||
override val fullName get() = groupProfile.fullName
|
||||
override val image get() = groupProfile.image
|
||||
@@ -726,6 +858,7 @@ data class GroupInfo (
|
||||
data class GroupProfile (
|
||||
override val displayName: String,
|
||||
override val fullName: String,
|
||||
val description: String? = null,
|
||||
override val image: String? = null,
|
||||
override val localAlias: String = "",
|
||||
val groupPreferences: GroupPreferences? = null
|
||||
@@ -757,6 +890,7 @@ data class GroupMember (
|
||||
val displayName: String get() = memberProfile.localAlias.ifEmpty { memberProfile.displayName }
|
||||
val fullName: String get() = memberProfile.fullName
|
||||
val image: String? get() = memberProfile.image
|
||||
val verified get() = activeConn?.connectionCode != null
|
||||
|
||||
val chatViewName: String
|
||||
get() = memberProfile.localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") }
|
||||
@@ -937,8 +1071,8 @@ class UserContactRequest (
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = false
|
||||
override val voiceMessageAllowed get() = false
|
||||
override val fullDeletionAllowed get() = false
|
||||
override fun featureEnabled(feature: ChatFeature) = false
|
||||
override val timedMessagesTTL: Int? get() = null
|
||||
override val displayName get() = profile.displayName
|
||||
override val fullName get() = profile.fullName
|
||||
override val image get() = profile.image
|
||||
@@ -975,8 +1109,8 @@ class PendingContactConnection(
|
||||
override val sendMsgEnabled get() = false
|
||||
override val ntfsEnabled get() = false
|
||||
override val incognito get() = customUserProfileId != null
|
||||
override val voiceMessageAllowed get() = false
|
||||
override val fullDeletionAllowed get() = false
|
||||
override fun featureEnabled(feature: ChatFeature) = false
|
||||
override val timedMessagesTTL: Int? get() = null
|
||||
override val localDisplayName get() = String.format(generalGetString(R.string.connection_local_display_name), pccConnId)
|
||||
override val displayName: String get() {
|
||||
if (localAlias.isNotEmpty()) return localAlias
|
||||
@@ -1074,7 +1208,7 @@ data class ChatItem (
|
||||
}
|
||||
}
|
||||
|
||||
val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew
|
||||
val isRcvNew: Boolean get() = meta.isRcvNew
|
||||
|
||||
val memberDisplayName: String? get() =
|
||||
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
|
||||
@@ -1117,10 +1251,13 @@ data class ChatItem (
|
||||
is CIContent.SndConnEventContent -> showNtfDir
|
||||
is CIContent.RcvChatFeature -> false
|
||||
is CIContent.SndChatFeature -> showNtfDir
|
||||
is CIContent.RcvChatPreference -> false
|
||||
is CIContent.SndChatPreference -> showNtfDir
|
||||
is CIContent.RcvGroupFeature -> false
|
||||
is CIContent.SndGroupFeature -> showNtfDir
|
||||
is CIContent.RcvChatFeatureRejected -> showNtfDir
|
||||
is CIContent.RcvGroupFeatureRejected -> showNtfDir
|
||||
is CIContent.InvalidJSON -> false
|
||||
}
|
||||
|
||||
fun withStatus(status: CIStatus): ChatItem = this.copy(meta = meta.copy(itemStatus = status))
|
||||
@@ -1136,11 +1273,12 @@ data class ChatItem (
|
||||
file: CIFile? = null,
|
||||
itemDeleted: Boolean = false,
|
||||
itemEdited: Boolean = false,
|
||||
itemTimed: CITimed? = null,
|
||||
editable: Boolean = true
|
||||
) =
|
||||
ChatItem(
|
||||
chatDir = dir,
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, editable),
|
||||
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, null, editable),
|
||||
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
|
||||
quotedItem = quotedItem,
|
||||
file = file
|
||||
@@ -1195,7 +1333,7 @@ data class ChatItem (
|
||||
)
|
||||
|
||||
fun getChatFeatureSample(feature: ChatFeature, enabled: FeatureEnabled): ChatItem {
|
||||
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled)
|
||||
val content = CIContent.RcvChatFeature(feature = feature, enabled = enabled, param = null)
|
||||
return ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead(), itemDeleted = false, itemEdited = false, editable = false),
|
||||
@@ -1206,7 +1344,8 @@ data class ChatItem (
|
||||
}
|
||||
|
||||
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
|
||||
|
||||
const val TEMP_LIVE_CHAT_ITEM_ID = -2L
|
||||
|
||||
val deletedItemDummy: ChatItem
|
||||
get() = ChatItem(
|
||||
chatDir = CIDirection.DirectRcv(),
|
||||
@@ -1216,14 +1355,46 @@ data class ChatItem (
|
||||
itemText = generalGetString(R.string.deleted_description),
|
||||
itemStatus = CIStatus.RcvRead(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemDeleted = false,
|
||||
itemEdited = false,
|
||||
itemTimed = null,
|
||||
itemLive = false,
|
||||
editable = false
|
||||
),
|
||||
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
)
|
||||
|
||||
fun liveDummy(direct: Boolean): ChatItem = ChatItem(
|
||||
chatDir = if (direct) CIDirection.DirectSnd() else CIDirection.GroupSnd(),
|
||||
meta = CIMeta(
|
||||
itemId = TEMP_LIVE_CHAT_ITEM_ID,
|
||||
itemTs = Clock.System.now(),
|
||||
itemText = "",
|
||||
itemStatus = CIStatus.RcvRead(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemDeleted = false,
|
||||
itemEdited = false,
|
||||
itemTimed = null,
|
||||
itemLive = true,
|
||||
editable = false
|
||||
),
|
||||
content = CIContent.SndMsgContent(MsgContent.MCText("")),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
)
|
||||
|
||||
fun invalidJSON(json: String): ChatItem =
|
||||
ChatItem(
|
||||
chatDir = CIDirection.DirectSnd(),
|
||||
meta = CIMeta.invalidJSON(),
|
||||
content = CIContent.InvalidJSON(json),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1249,16 +1420,33 @@ data class CIMeta (
|
||||
val itemText: String,
|
||||
val itemStatus: CIStatus,
|
||||
val createdAt: Instant,
|
||||
val updatedAt: Instant,
|
||||
val itemDeleted: Boolean,
|
||||
val itemEdited: Boolean,
|
||||
val itemTimed: CITimed?,
|
||||
val itemLive: Boolean?,
|
||||
val editable: Boolean
|
||||
) {
|
||||
val timestampText: String get() = getTimestampText(itemTs)
|
||||
val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now()
|
||||
val isLive: Boolean get() = itemLive == true
|
||||
val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null
|
||||
|
||||
val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew
|
||||
|
||||
fun statusIcon(primaryColor: Color, metaColor: Color = HighOrLowlight): Pair<ImageVector, Color>? =
|
||||
when (itemStatus) {
|
||||
is CIStatus.SndSent -> Icons.Filled.Check to metaColor
|
||||
is CIStatus.SndErrorAuth -> Icons.Filled.Close to Color.Red
|
||||
is CIStatus.SndError -> Icons.Filled.WarningAmber to WarningYellow
|
||||
is CIStatus.RcvNew -> Icons.Filled.Circle to primaryColor
|
||||
else -> null
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getSample(
|
||||
id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(),
|
||||
itemDeleted: Boolean = false, itemEdited: Boolean = false, editable: Boolean = true
|
||||
itemDeleted: Boolean = false, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, editable: Boolean = true
|
||||
): CIMeta =
|
||||
CIMeta(
|
||||
itemId = id,
|
||||
@@ -1266,13 +1454,38 @@ data class CIMeta (
|
||||
itemText = text,
|
||||
itemStatus = status,
|
||||
createdAt = ts,
|
||||
updatedAt = ts,
|
||||
itemDeleted = itemDeleted,
|
||||
itemEdited = itemEdited,
|
||||
itemTimed = itemTimed,
|
||||
itemLive = itemLive,
|
||||
editable = editable
|
||||
)
|
||||
|
||||
fun invalidJSON(): CIMeta =
|
||||
CIMeta(
|
||||
// itemId can not be the same for different items, otherwise ChatView will crash
|
||||
itemId = Random.nextLong(-1000000L, -1000L),
|
||||
itemTs = Clock.System.now(),
|
||||
itemText = "invalid JSON",
|
||||
itemStatus = CIStatus.SndNew(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now(),
|
||||
itemDeleted = false,
|
||||
itemEdited = false,
|
||||
itemTimed = null,
|
||||
itemLive = false,
|
||||
editable = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CITimed(
|
||||
val ttl: Int,
|
||||
val deleteAt: Instant?
|
||||
)
|
||||
|
||||
fun getTimestampText(t: Instant): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
|
||||
@@ -1320,12 +1533,15 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatFeature") class RcvChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndChatFeature") class SndChatFeature(val feature: ChatFeature, val enabled: FeatureEnabled, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatPreference") class RcvChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndChatPreference") class SndChatPreference(val feature: ChatFeature, val allowed: FeatureAllowed, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupFeature") class RcvGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
|
||||
override val text: String get() = when (this) {
|
||||
is SndMsgContent -> msgContent.text
|
||||
@@ -1341,13 +1557,34 @@ sealed class CIContent: ItemContent {
|
||||
is SndGroupEventContent -> sndGroupEvent.text
|
||||
is RcvConnEventContent -> rcvConnEvent.text
|
||||
is SndConnEventContent -> sndConnEvent.text
|
||||
is RcvChatFeature -> "${feature.text}: ${enabled.text}"
|
||||
is SndChatFeature -> "${feature.text}: ${enabled.text}"
|
||||
is RcvGroupFeature -> "${groupFeature.text}: ${preference.enable.text}"
|
||||
is SndGroupFeature -> "${groupFeature.text}: ${preference.enable.text}"
|
||||
is RcvChatFeature -> featureText(feature, enabled.text, param)
|
||||
is SndChatFeature -> featureText(feature, enabled.text, param)
|
||||
is RcvChatPreference -> preferenceText(feature, allowed, param)
|
||||
is SndChatPreference -> preferenceText(feature, allowed, param)
|
||||
is RcvGroupFeature -> featureText(groupFeature, preference.enable.text, param)
|
||||
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
|
||||
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
is InvalidJSON -> "invalid data"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun featureText(feature: Feature, enabled: String, param: Int?): String =
|
||||
if (feature.hasParam) {
|
||||
"${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
|
||||
} else {
|
||||
"${feature.text}: $enabled"
|
||||
}
|
||||
|
||||
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
|
||||
allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
|
||||
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
allowed != FeatureAllowed.NO ->
|
||||
String.format(generalGetString(R.string.feature_offered_item), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
else ->
|
||||
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -1438,14 +1675,8 @@ sealed class MsgContent {
|
||||
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is MCText -> "text $text"
|
||||
is MCLink -> "json ${json.encodeToString(this)}"
|
||||
is MCImage -> "json ${json.encodeToString(this)}"
|
||||
is MCVoice-> "json ${json.encodeToString(this)}"
|
||||
is MCFile -> "json ${json.encodeToString(this)}"
|
||||
is MCUnknown -> "json $json"
|
||||
}
|
||||
val cmdString: String get() =
|
||||
if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -30,7 +30,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
const val RejectCallAction: String = "chat.simplex.app.REJECT_CALL"
|
||||
const val CallNotificationId: Int = -1
|
||||
|
||||
private const val UserIdKey: String = "userId"
|
||||
private const val ChatIdKey: String = "chatId"
|
||||
|
||||
fun getUserIdFromIntent(intent: Intent?): Long? {
|
||||
val userId = intent?.getLongExtra(UserIdKey, -1L)
|
||||
return if (userId == -1L || userId == null) null else userId
|
||||
}
|
||||
}
|
||||
|
||||
private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
@@ -77,8 +83,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyContactRequestReceived(cInfo: ChatInfo.ContactRequest) {
|
||||
fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {
|
||||
notifyMessageReceived(
|
||||
user = user,
|
||||
chatId = cInfo.id,
|
||||
displayName = cInfo.displayName,
|
||||
msgText = generalGetString(R.string.notification_new_contact_request),
|
||||
@@ -87,21 +94,22 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
)
|
||||
}
|
||||
|
||||
fun notifyContactConnected(contact: Contact) {
|
||||
fun notifyContactConnected(user: User, contact: Contact) {
|
||||
notifyMessageReceived(
|
||||
user = user,
|
||||
chatId = contact.id,
|
||||
displayName = contact.displayName,
|
||||
msgText = generalGetString(R.string.notification_contact_connected)
|
||||
)
|
||||
}
|
||||
|
||||
fun notifyMessageReceived(cInfo: ChatInfo, cItem: ChatItem) {
|
||||
fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {
|
||||
if (!cInfo.ntfsEnabled) return
|
||||
|
||||
notifyMessageReceived(chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
notifyMessageReceived(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem))
|
||||
}
|
||||
|
||||
fun notifyMessageReceived(chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
|
||||
fun notifyMessageReceived(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) {
|
||||
Log.d(TAG, "notifyMessageReceived $chatId")
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs)
|
||||
@@ -126,13 +134,14 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
.setColor(0x88FFFF)
|
||||
.setAutoCancel(true)
|
||||
.setVibrate(if (actions.isEmpty()) null else longArrayOf(0, 250, 250, 250))
|
||||
.setContentIntent(chatPendingIntent(OpenChatAction, chatId))
|
||||
.setContentIntent(chatPendingIntent(OpenChatAction, user.userId, chatId))
|
||||
.setSilent(if (actions.isEmpty()) recentNotification else false)
|
||||
|
||||
for (action in actions) {
|
||||
val flags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
val actionIntent = Intent(SimplexApp.context, NtfActionReceiver::class.java)
|
||||
actionIntent.action = action.name
|
||||
actionIntent.putExtra(UserIdKey, user.userId)
|
||||
actionIntent.putExtra(ChatIdKey, chatId)
|
||||
val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags)
|
||||
val actionButton = when (action) {
|
||||
@@ -147,7 +156,7 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
.setGroup(MessageGroup)
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
|
||||
.setGroupSummary(true)
|
||||
.setContentIntent(chatPendingIntent(ShowChatsAction))
|
||||
.setContentIntent(chatPendingIntent(ShowChatsAction, user.userId))
|
||||
.build()
|
||||
|
||||
with(NotificationManagerCompat.from(context)) {
|
||||
@@ -182,9 +191,9 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
val soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + R.raw.ring_once)
|
||||
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
NotificationCompat.Builder(context, CallChannel)
|
||||
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.contact.id))
|
||||
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, contactId))
|
||||
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, contactId, true))
|
||||
.setContentIntent(chatPendingIntent(OpenChatAction, invitation.user.userId, invitation.contact.id))
|
||||
.addAction(R.drawable.ntf_icon, generalGetString(R.string.accept), chatPendingIntent(AcceptCallAction, invitation.user.userId, contactId))
|
||||
.addAction(R.drawable.ntf_icon, generalGetString(R.string.reject), chatPendingIntent(RejectCallAction, invitation.user.userId, contactId, true))
|
||||
.setFullScreenIntent(fullScreenPendingIntent, true)
|
||||
.setSound(soundUri)
|
||||
}
|
||||
@@ -241,12 +250,13 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
}
|
||||
}
|
||||
|
||||
private fun chatPendingIntent(intentAction: String, chatId: String? = null, broadcast: Boolean = false): PendingIntent {
|
||||
private fun chatPendingIntent(intentAction: String, userId: Long, chatId: String? = null, broadcast: Boolean = false): PendingIntent {
|
||||
Log.d(TAG, "chatPendingIntent for $intentAction")
|
||||
val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt()
|
||||
var intent = Intent(context, if (!broadcast) MainActivity::class.java else NtfActionReceiver::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.setAction(intentAction)
|
||||
.putExtra(UserIdKey, userId)
|
||||
if (chatId != null) intent = intent.putExtra(ChatIdKey, chatId)
|
||||
return if (!broadcast) {
|
||||
TaskStackBuilder.create(context).run {
|
||||
@@ -264,18 +274,25 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
|
||||
* */
|
||||
class NtfActionReceiver: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val userId = getUserIdFromIntent(intent)
|
||||
val chatId = intent?.getStringExtra(ChatIdKey) ?: return
|
||||
val cInfo = SimplexApp.context.chatModel.getChat(chatId)?.chatInfo
|
||||
val m = SimplexApp.context.chatModel
|
||||
when (intent.action) {
|
||||
NotificationAction.ACCEPT_CONTACT_REQUEST.name -> {
|
||||
if (cInfo !is ChatInfo.ContactRequest) return
|
||||
acceptContactRequest(cInfo, SimplexApp.context.chatModel)
|
||||
SimplexApp.context.chatModel.controller.ntfManager.cancelNotificationsForChat(chatId)
|
||||
val isCurrentUser = m.currentUser.value?.userId == userId
|
||||
val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) {
|
||||
(m.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val apiId = chatId.replace("<@", "").toLongOrNull() ?: return
|
||||
acceptContactRequest(apiId, cInfo, isCurrentUser, m)
|
||||
m.controller.ntfManager.cancelNotificationsForChat(chatId)
|
||||
}
|
||||
RejectCallAction -> {
|
||||
val invitation = SimplexApp.context.chatModel.callInvitations[chatId]
|
||||
val invitation = m.callInvitations[chatId]
|
||||
if (invitation != null) {
|
||||
SimplexApp.context.chatModel.callManager.endCall(invitation = invitation)
|
||||
m.callManager.endCall(invitation = invitation)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,21 @@
|
||||
package chat.simplex.app.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.SystemClock
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
@@ -31,70 +23,18 @@ import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
|
||||
private val lastSuccessfulAuth: MutableState<Long?> = mutableStateOf(null)
|
||||
|
||||
@Composable
|
||||
fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }
|
||||
val lastSuccessfulAuth = remember { lastSuccessfulAuth }
|
||||
BackHandler(onBack = {
|
||||
lastSuccessfulAuth.value = null
|
||||
close()
|
||||
})
|
||||
val authorized = remember { !chatModel.controller.appPrefs.performLA.get() }
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(lastSuccessfulAuth.value) {
|
||||
if (!authorized && !authorizedPreviously(lastSuccessfulAuth)) {
|
||||
runAuth(lastSuccessfulAuth, context)
|
||||
}
|
||||
}
|
||||
if (authorized || authorizedPreviously(lastSuccessfulAuth)) {
|
||||
LaunchedEffect(Unit) {
|
||||
// Update auth each time user visits this screen in authenticated state just to prolong authorized time
|
||||
lastSuccessfulAuth.value = SystemClock.elapsedRealtime()
|
||||
}
|
||||
TerminalLayout(
|
||||
chatModel.terminalItems,
|
||||
remember { chatModel.terminalItems },
|
||||
composeState,
|
||||
sendCommand = { sendCommand(chatModel, composeState) },
|
||||
close
|
||||
)
|
||||
} else {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
||||
CloseSheetBar(close)
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
runAuth(lastSuccessfulAuth, context)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun authorizedPreviously(lastSuccessfulAuth: State<Long?>): Boolean =
|
||||
lastSuccessfulAuth.value?.let { SystemClock.elapsedRealtime() - it < 30_000 } ?: false
|
||||
|
||||
private fun runAuth(lastSuccessfulAuth: MutableState<Long?>, context: Context) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_open_chat_console),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
lastSuccessfulAuth.value = when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> SystemClock.elapsedRealtime()
|
||||
is LAResult.Error, LAResult.Failed -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) {
|
||||
@@ -102,9 +42,9 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||
val prefPerformLA = chatModel.controller.appPrefs.performLA.get()
|
||||
val s = composeState.value.message
|
||||
if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
|
||||
val resp = CR.ChatCmdError(ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty")))
|
||||
chatModel.terminalItems.add(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.terminalItems.add(TerminalItem.resp(resp))
|
||||
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty")))
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s)))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(resp))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
} else {
|
||||
withApi {
|
||||
@@ -135,7 +75,21 @@ fun TerminalLayout(
|
||||
topBar = { CloseSheetBar(close) },
|
||||
bottomBar = {
|
||||
Box(Modifier.padding(horizontal = 8.dp)) {
|
||||
SendMsgView(composeState, false, false, false, sendCommand, ::onMessageChange, { _, _, _ -> }, {}, {}, textStyle)
|
||||
SendMsgView(
|
||||
composeState = composeState,
|
||||
showVoiceRecordIcon = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = sendCommand,
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
onMessageChange = ::onMessageChange,
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.navigationBarsWithImePadding()
|
||||
@@ -160,7 +114,8 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed() } }
|
||||
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } }
|
||||
val context = LocalContext.current
|
||||
LazyColumn(state = listState, reverseLayout = true) {
|
||||
items(reversedTerminalItems) { item ->
|
||||
Text(
|
||||
@@ -171,7 +126,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
ModalManager.shared.showModal {
|
||||
ModalManager.shared.showModal(endButtons = { ShareButton { shareText(context, item.details) } }) {
|
||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ fun isValidDisplayName(name: String) : Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateProfilePanel(chatModel: ChatModel) {
|
||||
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
|
||||
val displayName = remember { mutableStateOf("") }
|
||||
val fullName = remember { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -72,10 +72,12 @@ fun CreateProfilePanel(chatModel: ChatModel) {
|
||||
ProfileNameField(fullName)
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Row {
|
||||
SimpleButton(
|
||||
text = stringResource(R.string.about_simplex),
|
||||
icon = Icons.Outlined.ArrowBackIosNew
|
||||
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
|
||||
if (chatModel.users.isEmpty()) {
|
||||
SimpleButton(
|
||||
text = stringResource(R.string.about_simplex),
|
||||
icon = Icons.Outlined.ArrowBackIosNew
|
||||
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
|
||||
@@ -83,7 +85,7 @@ fun CreateProfilePanel(chatModel: ChatModel) {
|
||||
val createModifier: Modifier
|
||||
val createColor: Color
|
||||
if (enabled) {
|
||||
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value) }.padding(8.dp)
|
||||
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
|
||||
createColor = MaterialTheme.colors.primary
|
||||
} else {
|
||||
createModifier = Modifier.padding(8.dp)
|
||||
@@ -105,15 +107,22 @@ fun CreateProfilePanel(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String) {
|
||||
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
|
||||
withApi {
|
||||
val user = chatModel.controller.apiCreateActiveUser(
|
||||
Profile(displayName, fullName, null)
|
||||
)
|
||||
chatModel.controller.startChat(user)
|
||||
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
|
||||
SimplexService.start(chatModel.controller.appContext)
|
||||
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
|
||||
) ?: return@withApi
|
||||
chatModel.currentUser.value = user
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.controller.startChat(user)
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
|
||||
} else {
|
||||
val users = chatModel.controller.listUsers()
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(users)
|
||||
chatModel.controller.getUserChatData()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
controller.ntfManager.notifyCallInvitation(invitation)
|
||||
} else {
|
||||
val contact = invitation.contact
|
||||
controller.ntfManager.notifyMessageReceived(chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
||||
controller.ntfManager.notifyMessageReceived(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
invitation,
|
||||
callOnLockScreen,
|
||||
chatModel,
|
||||
rejectCall = { cm.endCall(invitation = invitation) },
|
||||
ignoreCall = {
|
||||
chatModel.activeCallInvitation.value = null
|
||||
@@ -135,6 +136,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
|
||||
openApp = {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
.setAction(OpenChatAction)
|
||||
.putExtra("userId", invitation.user.userId)
|
||||
.putExtra("chatId", invitation.contact.id)
|
||||
context.startActivity(intent)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -149,6 +151,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo
|
||||
fun IncomingCallLockScreenAlertLayout(
|
||||
invitation: RcvCallInvitation,
|
||||
callOnLockScreen: CallOnLockScreen?,
|
||||
chatModel: ChatModel,
|
||||
rejectCall: () -> Unit,
|
||||
ignoreCall: () -> Unit,
|
||||
acceptCall: () -> Unit,
|
||||
@@ -160,7 +163,7 @@ fun IncomingCallLockScreenAlertLayout(
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
IncomingCallInfo(invitation)
|
||||
IncomingCallInfo(invitation, chatModel)
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
if (callOnLockScreen == CallOnLockScreen.ACCEPT) {
|
||||
ProfileImage(size = 192.dp, image = invitation.contact.profile.image)
|
||||
@@ -217,12 +220,14 @@ fun PreviewIncomingCallLockScreenAlert() {
|
||||
.fillMaxSize()) {
|
||||
IncomingCallLockScreenAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
user = User.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
|
||||
sharedKey = null,
|
||||
callTs = Clock.System.now()
|
||||
),
|
||||
callOnLockScreen = null,
|
||||
chatModel = SimplexApp.context.chatModel,
|
||||
rejectCall = {},
|
||||
ignoreCall = {},
|
||||
acceptCall = {},
|
||||
|
||||
+14
-5
@@ -17,9 +17,10 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Contact
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
import chat.simplex.app.views.usersettings.ProfilePreview
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@@ -32,6 +33,7 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||
DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } }
|
||||
IncomingCallAlertLayout(
|
||||
invitation,
|
||||
chatModel,
|
||||
rejectCall = { cm.endCall(invitation = invitation) },
|
||||
ignoreCall = {
|
||||
chatModel.activeCallInvitation.value = null
|
||||
@@ -44,13 +46,14 @@ fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||
@Composable
|
||||
fun IncomingCallAlertLayout(
|
||||
invitation: RcvCallInvitation,
|
||||
chatModel: ChatModel,
|
||||
rejectCall: () -> Unit,
|
||||
ignoreCall: () -> Unit,
|
||||
acceptCall: () -> Unit
|
||||
) {
|
||||
val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight
|
||||
Column(Modifier.fillMaxWidth().background(color).padding(top = 16.dp, bottom = 16.dp, start = 16.dp, end = 8.dp)) {
|
||||
IncomingCallInfo(invitation)
|
||||
IncomingCallInfo(invitation, chatModel)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Row(Modifier.fillMaxWidth().weight(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -66,9 +69,13 @@ fun IncomingCallAlertLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IncomingCallInfo(invitation: RcvCallInvitation) {
|
||||
fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
|
||||
@Composable fun CallIcon(icon: ImageVector, descr: String) = Icon(icon, descr, tint = SimplexGreen)
|
||||
Row {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatModel.users.size > 1) {
|
||||
ProfileImage(size = 32.dp, image = invitation.user.profile.image, color = MaterialTheme.colors.secondary)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
if (invitation.callType.media == CallMediaType.Video) CallIcon(Icons.Filled.Videocam, stringResource(R.string.icon_descr_video_call))
|
||||
else CallIcon(Icons.Filled.Phone, stringResource(R.string.icon_descr_audio_call))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
@@ -101,11 +108,13 @@ fun PreviewIncomingCallAlertLayout() {
|
||||
SimpleXTheme {
|
||||
IncomingCallAlertLayout(
|
||||
invitation = RcvCallInvitation(
|
||||
user = User.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)),
|
||||
sharedKey = null,
|
||||
callTs = Clock.System.now()
|
||||
),
|
||||
chatModel = SimplexApp.context.chatModel,
|
||||
rejectCall = {},
|
||||
ignoreCall = {},
|
||||
acceptCall = {}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.Contact
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -61,39 +62,39 @@ enum class CallState {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
|
||||
@Serializable
|
||||
sealed class WCallCommand {
|
||||
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
|
||||
@Serializable @SerialName("start") class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("end") object End: WCallCommand()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class WCallResponse {
|
||||
@Serializable @SerialName("capabilities") class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("answer") class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("capabilities") data class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("answer") data class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
||||
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
||||
@Serializable @SerialName("error") class Error(val message: String): WCallResponse()
|
||||
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
|
||||
}
|
||||
|
||||
@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
|
||||
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||
@Serializable class RcvCallInvitation(val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||
@Serializable data class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
|
||||
@Serializable data class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||
@Serializable data class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||
@Serializable data class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||
@Serializable data class RcvCallInvitation(val user: User, val contact: Contact, val callType: CallType, val sharedKey: String? = null, val callTs: Instant) {
|
||||
val callTypeText: String get() = generalGetString(when(callType.media) {
|
||||
CallMediaType.Video -> if (sharedKey == null) R.string.video_call_no_encryption else R.string.encrypted_video_call
|
||||
CallMediaType.Audio -> if (sharedKey == null) R.string.audio_call_no_encryption else R.string.encrypted_audio_call
|
||||
@@ -103,8 +104,8 @@ sealed class WCallResponse {
|
||||
CallMediaType.Audio -> R.string.incoming_audio_call
|
||||
})
|
||||
}
|
||||
@Serializable class CallCapabilities(val encryption: Boolean)
|
||||
@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||
@Serializable data class CallCapabilities(val encryption: Boolean)
|
||||
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||
val text: String @Composable get() = when {
|
||||
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
|
||||
stringResource(R.string.call_connection_peer_to_peer)
|
||||
@@ -115,7 +116,7 @@ sealed class WCallResponse {
|
||||
}
|
||||
}
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
|
||||
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
||||
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||
|
||||
@@ -150,7 +151,7 @@ enum class VideoCamera {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ConnectionState(
|
||||
data class ConnectionState(
|
||||
val connectionState: String,
|
||||
val iceConnectionState: String,
|
||||
val iceGatheringState: String,
|
||||
|
||||
@@ -38,6 +38,7 @@ import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun ChatInfoView(
|
||||
@@ -46,18 +47,24 @@ fun ChatInfoView(
|
||||
connStats: ConnectionStats?,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
if (chat != null) {
|
||||
val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) {
|
||||
mutableStateOf(chatModel.contactNetworkStatus(contact))
|
||||
}
|
||||
ChatInfoLayout(
|
||||
chat,
|
||||
contact,
|
||||
connStats,
|
||||
contactNetworkStatus.value,
|
||||
customUserProfile,
|
||||
localAlias,
|
||||
connectionCode,
|
||||
developerTools,
|
||||
onLocalAliasChanged = {
|
||||
setContactAlias(chat.chatInfo.apiId, it, chatModel)
|
||||
@@ -74,6 +81,31 @@ fun ChatInfoView(
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
switchContactAddress = {
|
||||
showSwitchContactAddressAlert(chatModel, contact.contactId)
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
||||
VerifyCodeView(
|
||||
ct.displayName,
|
||||
connectionCode,
|
||||
ct.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyContact(ct.contactId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.updateContact(
|
||||
ct.copy(
|
||||
activeConn = ct.activeConn.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
r
|
||||
}
|
||||
},
|
||||
close,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -121,14 +153,17 @@ fun ChatInfoLayout(
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
connStats: ConnectionStats?,
|
||||
contactNetworkStatus: NetworkStatus,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
developerTools: Boolean,
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
openPreferences: () -> Unit,
|
||||
deleteContact: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
switchContactAddress: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -154,6 +189,10 @@ fun ChatInfoLayout(
|
||||
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(contact.verified, verifyClicked)
|
||||
SectionDivider()
|
||||
}
|
||||
ContactPreferencesButton(openPreferences)
|
||||
}
|
||||
|
||||
@@ -166,9 +205,9 @@ fun ChatInfoLayout(
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.network_status),
|
||||
chat.serverInfo.networkStatus.statusExplanation
|
||||
contactNetworkStatus.statusExplanation
|
||||
)}) {
|
||||
NetworkStatusRow(chat.serverInfo.networkStatus)
|
||||
NetworkStatusRow(contactNetworkStatus)
|
||||
}
|
||||
val rcvServers = connStats.rcvServers
|
||||
if (rcvServers != null && rcvServers.isNotEmpty()) {
|
||||
@@ -208,13 +247,17 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ChatInfoImage(cInfo, size = 192.dp, iconColor = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Text(
|
||||
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Row(Modifier.padding(bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (contact.verified) {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
|
||||
}
|
||||
Text(
|
||||
contact.profile.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.fullName != contact.profile.displayName) {
|
||||
Text(
|
||||
cInfo.fullName, style = MaterialTheme.typography.h2,
|
||||
@@ -276,7 +319,7 @@ fun LocalAliasEditor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -308,14 +351,14 @@ fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ServerImage(networkStatus: Chat.NetworkStatus) {
|
||||
private fun ServerImage(networkStatus: NetworkStatus) {
|
||||
Box(Modifier.size(18.dp)) {
|
||||
when (networkStatus) {
|
||||
is Chat.NetworkStatus.Connected ->
|
||||
is NetworkStatus.Connected ->
|
||||
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_server_status_connected), tint = MaterialTheme.colors.primaryVariant)
|
||||
is Chat.NetworkStatus.Disconnected ->
|
||||
is NetworkStatus.Disconnected ->
|
||||
Icon(Icons.Filled.Pending, stringResource(R.string.icon_descr_server_status_disconnected), tint = HighOrLowlight)
|
||||
is Chat.NetworkStatus.Error ->
|
||||
is NetworkStatus.Error ->
|
||||
Icon(Icons.Filled.Error, stringResource(R.string.icon_descr_server_status_error), tint = HighOrLowlight)
|
||||
else -> Icon(Icons.Outlined.Circle, stringResource(R.string.icon_descr_server_status_pending), tint = HighOrLowlight)
|
||||
}
|
||||
@@ -339,6 +382,16 @@ fun SwitchAddressButton(onClick: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
if (contactVerified) Icons.Outlined.VerifiedUser else Icons.Outlined.Shield,
|
||||
stringResource(if (contactVerified) R.string.view_security_code else R.string.verify_security_code),
|
||||
click = onClick,
|
||||
iconColor = HighOrLowlight,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactPreferencesButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@@ -360,7 +413,7 @@ fun ClearChatButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteContactButton(onClick: () -> Unit) {
|
||||
private fun DeleteContactButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.Delete,
|
||||
stringResource(R.string.button_delete_contact),
|
||||
@@ -398,19 +451,21 @@ fun PreviewChatInfoLayout() {
|
||||
ChatInfoLayout(
|
||||
chat = Chat(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
chatItems = arrayListOf(),
|
||||
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
|
||||
chatItems = arrayListOf()
|
||||
),
|
||||
Contact.sampleData,
|
||||
localAlias = "",
|
||||
connectionCode = "123",
|
||||
developerTools = false,
|
||||
connStats = null,
|
||||
contactNetworkStatus = NetworkStatus.Connected(),
|
||||
onLocalAliasChanged = {},
|
||||
customUserProfile = null,
|
||||
openPreferences = {},
|
||||
deleteContact = {},
|
||||
clearChat = {},
|
||||
switchContactAddress = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.*
|
||||
@@ -12,8 +13,7 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
@@ -56,7 +56,13 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val composeState = rememberSaveable(saver = ComposeState.saver()) {
|
||||
mutableStateOf(ComposeState(useLinkPreviews = useLinkPreviews))
|
||||
mutableStateOf(
|
||||
if (chatModel.draftChatId.value == chatId && chatModel.draft.value != null) {
|
||||
chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
}
|
||||
)
|
||||
}
|
||||
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
|
||||
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
@@ -77,16 +83,23 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
}
|
||||
}
|
||||
launch {
|
||||
// .toList() is important for making observation working
|
||||
snapshotFlow { chatModel.chats.toList() }
|
||||
.distinctUntilChanged()
|
||||
.collect { chats ->
|
||||
chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }.let {
|
||||
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
||||
if (it?.chatInfo != activeChat.value?.chatInfo) {
|
||||
activeChat.value = it
|
||||
}}
|
||||
snapshotFlow {
|
||||
/**
|
||||
* It's possible that in some cases concurrent modification can happen on [ChatModel.chats] list.
|
||||
* In this case only error log will be printed here (no crash).
|
||||
* TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that
|
||||
* */
|
||||
try {
|
||||
chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }
|
||||
} catch (e: ConcurrentModificationException) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
null
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
||||
.filter { it?.chatInfo != activeChat.value?.chatInfo && it != null }
|
||||
.collect { activeChat.value = it }
|
||||
}
|
||||
}
|
||||
val view = LocalView.current
|
||||
@@ -131,16 +144,17 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
withApi {
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
val contact = remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
|
||||
contact.value?.let { ct ->
|
||||
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close)
|
||||
remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct ->
|
||||
ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
}
|
||||
}
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||
var groupLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
GroupChatInfoView(chatModel, close)
|
||||
GroupChatInfoView(chatModel, groupLink, { groupLink = it }, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,8 +163,21 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
member to null
|
||||
}
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
GroupMemberInfoView(groupInfo, member, stats, chatModel, close, close)
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -183,7 +210,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId ->
|
||||
withApi { chatModel.controller.receiveFile(fileId) }
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
withApi { chatModel.controller.receiveFile(user, fileId) }
|
||||
}
|
||||
},
|
||||
joinGroup = { groupId ->
|
||||
withApi { chatModel.controller.apiJoinGroup(groupId) }
|
||||
@@ -205,6 +235,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
},
|
||||
acceptFeature = { contact, feature, param ->
|
||||
withApi {
|
||||
chatModel.controller.allowFeatureToContact(contact, feature, param)
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
@@ -261,6 +296,7 @@ fun ChatLayout(
|
||||
joinGroup: (Long) -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
addMembers: (GroupInfo) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
|
||||
@@ -301,7 +337,7 @@ fun ChatLayout(
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
receiveFile, joinGroup, acceptCall, markRead, setFloatingButton, onComposed,
|
||||
receiveFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -424,10 +460,15 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
|
||||
Modifier.padding(start = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
cInfo.displayName, fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if ((cInfo as? ChatInfo.Direct)?.contact?.verified == true) {
|
||||
ContactVerifiedShield()
|
||||
}
|
||||
Text(
|
||||
cInfo.displayName, fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName && cInfo.localAlias.isEmpty()) {
|
||||
Text(
|
||||
cInfo.fullName,
|
||||
@@ -438,6 +479,11 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContactVerifiedShield() {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight)
|
||||
}
|
||||
|
||||
data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState)
|
||||
|
||||
val CIListStateSaver = run {
|
||||
@@ -466,6 +512,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
receiveFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
onComposed: () -> Unit,
|
||||
@@ -491,7 +538,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
val reversedChatItems by remember { derivedStateOf { chatItems.reversed() } }
|
||||
val reversedChatItems by remember { derivedStateOf { chatItems.reversed().toList() } }
|
||||
val maxHeightRounded = with(LocalDensity.current) { maxHeight.roundToPx() }
|
||||
val scrollToItem: (Long) -> Unit = { itemId: Long ->
|
||||
val index = reversedChatItems.indexOfFirst { it.id == itemId }
|
||||
@@ -530,7 +577,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
scope.launch {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else {
|
||||
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
}
|
||||
@@ -571,11 +618,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
@@ -586,7 +633,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
end = if (sent) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,17 +664,21 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems:
|
||||
// Don't autoscroll next time until it will be needed
|
||||
shouldAutoScroll = false to chatId
|
||||
}
|
||||
val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() }
|
||||
/*
|
||||
* Since we use key with each item in LazyColumn, LazyColumn will not autoscroll to bottom item. We need to do it ourselves.
|
||||
* When the first visible item (from bottom) is fully visible we can autoscroll to 0 item
|
||||
* When the first visible item (from bottom) is visible (even partially) we can autoscroll to 0 item. Or just scrollBy small distance otherwise
|
||||
* */
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { chatItems.lastOrNull()?.id }
|
||||
.distinctUntilChanged()
|
||||
.filter { listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset == 0 }
|
||||
.filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it }
|
||||
.collect {
|
||||
listState.animateScrollToItem(0)
|
||||
if (listState.firstVisibleItemIndex == 0) {
|
||||
listState.animateScrollToItem(0)
|
||||
} else {
|
||||
listState.animateScrollBy(scrollDistance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,6 +1035,7 @@ fun PreviewChatLayout() {
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
@@ -1042,6 +1094,7 @@ fun PreviewGroupChatLayout() {
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
@file:UseSerializers(UriSerializer::class)
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import ComposeVoiceView
|
||||
import ComposeFileView
|
||||
import ComposeVoiceView
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
@@ -32,27 +34,25 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toFile
|
||||
import androidx.core.net.toUri
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.chat.item.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
@Serializable
|
||||
sealed class ComposePreview {
|
||||
@Serializable object NoPreview: ComposePreview()
|
||||
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
|
||||
@Serializable class ImagePreview(val images: List<String>): ComposePreview()
|
||||
@Serializable class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
|
||||
@Serializable class FilePreview(val fileName: String): ComposePreview()
|
||||
@Serializable class ImagePreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
|
||||
@Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
|
||||
@Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -62,16 +62,26 @@ sealed class ComposeContextItem {
|
||||
@Serializable class EditingItem(val chatItem: ChatItem): ComposeContextItem()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LiveMessage(
|
||||
val chatItem: ChatItem,
|
||||
val typedMsg: String,
|
||||
val sentMsg: String,
|
||||
val sent: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ComposeState(
|
||||
val message: String = "",
|
||||
val liveMessage: LiveMessage? = null,
|
||||
val preview: ComposePreview = ComposePreview.NoPreview,
|
||||
val contextItem: ComposeContextItem = ComposeContextItem.NoContextItem,
|
||||
val inProgress: Boolean = false,
|
||||
val useLinkPreviews: Boolean
|
||||
) {
|
||||
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this(
|
||||
constructor(editingItem: ChatItem, liveMessage: LiveMessage? = null, useLinkPreviews: Boolean): this(
|
||||
editingItem.content.text,
|
||||
liveMessage,
|
||||
chatItemPreview(editingItem),
|
||||
ComposeContextItem.EditingItem(editingItem),
|
||||
useLinkPreviews = useLinkPreviews
|
||||
@@ -89,10 +99,13 @@ data class ComposeState(
|
||||
is ComposePreview.ImagePreview -> true
|
||||
is ComposePreview.VoicePreview -> true
|
||||
is ComposePreview.FilePreview -> true
|
||||
else -> message.isNotEmpty()
|
||||
else -> message.isNotEmpty() || liveMessage != null
|
||||
}
|
||||
hasContent && !inProgress
|
||||
}
|
||||
val endLiveDisabled: Boolean
|
||||
get() = liveMessage != null && message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
|
||||
|
||||
val linkPreviewAllowed: Boolean
|
||||
get() =
|
||||
when (preview) {
|
||||
@@ -108,6 +121,19 @@ data class ComposeState(
|
||||
else -> null
|
||||
}
|
||||
|
||||
val attachmentDisabled: Boolean
|
||||
get() {
|
||||
if (editing || liveMessage != null) return true
|
||||
return when (preview) {
|
||||
ComposePreview.NoPreview -> false
|
||||
is ComposePreview.CLinkPreview -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
val empty: Boolean
|
||||
get() = message.isEmpty() && preview is ComposePreview.NoPreview
|
||||
|
||||
companion object {
|
||||
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
|
||||
save = { json.encodeToString(serializer(), it.value) },
|
||||
@@ -118,16 +144,24 @@ data class ComposeState(
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RecordingState {
|
||||
object NotStarted: RecordingState()
|
||||
class Started(val filePath: String, val progressMs: Int = 0): RecordingState()
|
||||
class Finished(val filePath: String, val durationMs: Int): RecordingState()
|
||||
|
||||
val filePathNullable: String?
|
||||
get() = (this as? Started)?.filePath
|
||||
}
|
||||
|
||||
fun chatItemPreview(chatItem: ChatItem): ComposePreview {
|
||||
val fileName = chatItem.file?.fileName ?: ""
|
||||
return when (val mc = chatItem.content.msgContent) {
|
||||
is MsgContent.MCText -> ComposePreview.NoPreview
|
||||
is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview)
|
||||
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image))
|
||||
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = chatItem.file?.fileName ?: "", mc.duration / 1000, true)
|
||||
is MsgContent.MCFile -> {
|
||||
val fileName = chatItem.file?.fileName ?: ""
|
||||
ComposePreview.FilePreview(fileName)
|
||||
}
|
||||
// TODO: include correct type
|
||||
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
|
||||
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true)
|
||||
is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName))
|
||||
is MsgContent.MCUnknown, null -> ComposePreview.NoPreview
|
||||
}
|
||||
}
|
||||
@@ -148,21 +182,12 @@ fun ComposeView(
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
val textStyle = remember { mutableStateOf(smallFont) }
|
||||
// attachments
|
||||
val chosenContent = rememberSaveable { mutableStateOf<List<UploadContent>>(emptyList()) }
|
||||
val audioSaver = Saver<MutableState<Pair<Uri, Int>?>, Pair<String, Int>>(
|
||||
save = { it.value.let { if (it == null) null else it.first.toString() to it.second } },
|
||||
restore = { mutableStateOf(Uri.parse(it.first) to it.second) }
|
||||
)
|
||||
val chosenAudio = rememberSaveable(saver = audioSaver) { mutableStateOf(null) }
|
||||
val chosenFile = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
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)
|
||||
chosenContent.value = listOf(UploadContent.SimpleImage(uri))
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview)))
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
|
||||
}
|
||||
}
|
||||
val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean ->
|
||||
@@ -209,8 +234,7 @@ fun ComposeView(
|
||||
}
|
||||
|
||||
if (imagesPreview.isNotEmpty()) {
|
||||
chosenContent.value = content
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview))
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview, content))
|
||||
}
|
||||
}
|
||||
val processPickedFile = { uri: Uri?, text: String? ->
|
||||
@@ -219,8 +243,7 @@ fun ComposeView(
|
||||
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
|
||||
val fileName = getFileName(SimplexApp.context, uri)
|
||||
if (fileName != null) {
|
||||
chosenFile.value = uri
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName))
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -233,6 +256,7 @@ fun ComposeView(
|
||||
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
|
||||
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
|
||||
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
|
||||
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
|
||||
|
||||
LaunchedEffect(attachmentOption.value) {
|
||||
when (attachmentOption.value) {
|
||||
@@ -310,128 +334,160 @@ fun ComposeView(
|
||||
cancelledLinks.clear()
|
||||
}
|
||||
|
||||
fun checkLinkPreview(): MsgContent {
|
||||
fun clearState(live: Boolean = false) {
|
||||
if (live) {
|
||||
composeState.value = composeState.value.copy(inProgress = false)
|
||||
} else {
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
resetLinkPreview()
|
||||
}
|
||||
recState.value = RecordingState.NotStarted
|
||||
textStyle.value = smallFont
|
||||
chatModel.removeLiveDummy()
|
||||
}
|
||||
|
||||
fun deleteUnusedFiles() {
|
||||
chatModel.filesToDelete.forEach { it.delete() }
|
||||
chatModel.filesToDelete.clear()
|
||||
}
|
||||
|
||||
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
|
||||
val aChatItem = chatModel.controller.apiSendMessage(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
file = file,
|
||||
quotedItemId = quoted,
|
||||
mc = mc,
|
||||
live = live
|
||||
)
|
||||
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
return aChatItem?.chatItem
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val cs = composeState.value
|
||||
return when (val composePreview = cs.preview) {
|
||||
is ComposePreview.CLinkPreview -> {
|
||||
val url = parseMessage(cs.message)
|
||||
val lp = composePreview.linkPreview
|
||||
if (lp != null && url == lp.uri) {
|
||||
MsgContent.MCLink(cs.message, preview = lp)
|
||||
} else {
|
||||
MsgContent.MCText(cs.message)
|
||||
var sent: ChatItem?
|
||||
val msgText = text ?: cs.message
|
||||
|
||||
fun sending() {
|
||||
composeState.value = composeState.value.copy(inProgress = true)
|
||||
}
|
||||
|
||||
fun checkLinkPreview(): MsgContent {
|
||||
return when (val composePreview = cs.preview) {
|
||||
is ComposePreview.CLinkPreview -> {
|
||||
val url = parseMessage(msgText)
|
||||
val lp = composePreview.linkPreview
|
||||
if (lp != null && url == lp.uri) {
|
||||
MsgContent.MCLink(msgText, preview = lp)
|
||||
} else {
|
||||
MsgContent.MCText(msgText)
|
||||
}
|
||||
}
|
||||
else -> MsgContent.MCText(msgText)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMsgContent(msgContent: MsgContent): MsgContent {
|
||||
return when (msgContent) {
|
||||
is MsgContent.MCText -> checkLinkPreview()
|
||||
is MsgContent.MCLink -> checkLinkPreview()
|
||||
is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image)
|
||||
is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration)
|
||||
is MsgContent.MCFile -> MsgContent.MCFile(msgText)
|
||||
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
|
||||
val oldMsgContent = ei.content.msgContent
|
||||
if (oldMsgContent != null) {
|
||||
val updatedItem = chatModel.controller.apiUpdateChatItem(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = ei.meta.itemId,
|
||||
mc = updateMsgContent(oldMsgContent),
|
||||
live = live
|
||||
)
|
||||
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
|
||||
return updatedItem?.chatItem
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
val liveMessage = cs.liveMessage
|
||||
if (!live) {
|
||||
if (liveMessage != null) composeState.value = cs.copy(liveMessage = null)
|
||||
sending()
|
||||
}
|
||||
|
||||
if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
val ei = cs.contextItem.chatItem
|
||||
sent = updateMessage(ei, cInfo, live)
|
||||
} else if (liveMessage != null && liveMessage.sent) {
|
||||
sent = updateMessage(liveMessage.chatItem, cInfo, live)
|
||||
} else {
|
||||
val msgs: ArrayList<MsgContent> = ArrayList()
|
||||
val files: ArrayList<String> = ArrayList()
|
||||
when (val preview = cs.preview) {
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
is ComposePreview.ImagePreview -> {
|
||||
preview.content.forEachIndexed { index, it ->
|
||||
val file = when (it) {
|
||||
is UploadContent.SimpleImage -> saveImage(context, it.uri)
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
files.add(file)
|
||||
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
is ComposePreview.VoicePreview -> {
|
||||
val tmpFile = File(preview.voice)
|
||||
AudioPlayer.stop(tmpFile.absolutePath)
|
||||
val actualFile = File(getAppFilePath(SimplexApp.context, tmpFile.name.replaceAfter(RecorderNative.extension, "")))
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.move(tmpFile.toPath(), actualFile.toPath())
|
||||
}
|
||||
files.add(actualFile.name)
|
||||
deleteUnusedFiles()
|
||||
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000))
|
||||
}
|
||||
is ComposePreview.FilePreview -> {
|
||||
val file = saveFileFromUri(context, preview.uri)
|
||||
if (file != null) {
|
||||
files.add((file))
|
||||
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> MsgContent.MCText(cs.message)
|
||||
val quotedItemId: Long? = when (cs.contextItem) {
|
||||
is ComposeContextItem.QuotedItem -> cs.contextItem.chatItem.id
|
||||
else -> null
|
||||
}
|
||||
sent = null
|
||||
msgs.forEachIndexed { index, content ->
|
||||
if (index > 0) delay(100)
|
||||
sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index),
|
||||
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
|
||||
)
|
||||
}
|
||||
if (sent == null && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview || cs.preview is ComposePreview.VoicePreview)) {
|
||||
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMsgContent(msgContent: MsgContent): MsgContent {
|
||||
val cs = composeState.value
|
||||
return when (msgContent) {
|
||||
is MsgContent.MCText -> checkLinkPreview()
|
||||
is MsgContent.MCLink -> checkLinkPreview()
|
||||
is MsgContent.MCImage -> MsgContent.MCImage(cs.message, image = msgContent.image)
|
||||
is MsgContent.MCVoice -> MsgContent.MCVoice(cs.message, duration = msgContent.duration)
|
||||
is MsgContent.MCFile -> MsgContent.MCFile(cs.message)
|
||||
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = cs.message, json = msgContent.json)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearState() {
|
||||
composeState.value = ComposeState(useLinkPreviews = useLinkPreviews)
|
||||
textStyle.value = smallFont
|
||||
chosenContent.value = emptyList()
|
||||
chosenAudio.value = null
|
||||
chosenFile.value = null
|
||||
linkUrl.value = null
|
||||
prevLinkUrl.value = null
|
||||
pendingLinkUrl.value = null
|
||||
cancelledLinks.clear()
|
||||
clearState(live)
|
||||
return sent
|
||||
}
|
||||
|
||||
fun sendMessage() {
|
||||
composeState.value = composeState.value.copy(inProgress = true)
|
||||
val cInfo = chat.chatInfo
|
||||
val cs = composeState.value
|
||||
when (val contextItem = cs.contextItem) {
|
||||
is ComposeContextItem.EditingItem -> {
|
||||
val ei = contextItem.chatItem
|
||||
val oldMsgContent = ei.content.msgContent
|
||||
if (oldMsgContent != null) {
|
||||
withApi {
|
||||
val updatedItem = chatModel.controller.apiUpdateChatItem(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
itemId = ei.meta.itemId,
|
||||
mc = updateMsgContent(oldMsgContent)
|
||||
)
|
||||
if (updatedItem != null) chatModel.upsertChatItem(cInfo, updatedItem.chatItem)
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val msgs: ArrayList<MsgContent> = ArrayList()
|
||||
val files: ArrayList<String> = ArrayList()
|
||||
when (val preview = cs.preview) {
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(cs.message))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
is ComposePreview.ImagePreview -> {
|
||||
chosenContent.value.forEachIndexed { index, it ->
|
||||
val file = when (it) {
|
||||
is UploadContent.SimpleImage -> saveImage(context, it.uri)
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
files.add(file)
|
||||
msgs.add(MsgContent.MCImage(if (msgs.isEmpty()) cs.message else "", preview.images[index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
is ComposePreview.VoicePreview -> {
|
||||
val chosenAudioVal = chosenAudio.value
|
||||
if (chosenAudioVal != null) {
|
||||
val file = chosenAudioVal.first.toFile().name
|
||||
files.add((file))
|
||||
chatModel.filesToDelete.remove(chosenAudioVal.first.toFile())
|
||||
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) cs.message else "", chosenAudioVal.second / 1000))
|
||||
}
|
||||
}
|
||||
is ComposePreview.FilePreview -> {
|
||||
val chosenFileVal = chosenFile.value
|
||||
if (chosenFileVal != null) {
|
||||
val file = saveFileFromUri(context, chosenFileVal)
|
||||
if (file != null) {
|
||||
files.add((file))
|
||||
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) cs.message else ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val quotedItemId: Long? = when (contextItem) {
|
||||
is ComposeContextItem.QuotedItem -> contextItem.chatItem.id
|
||||
else -> null
|
||||
}
|
||||
if (msgs.isNotEmpty()) {
|
||||
withApi {
|
||||
msgs.forEachIndexed { index, content ->
|
||||
if (index > 0) delay(100)
|
||||
val aChatItem = chatModel.controller.apiSendMessage(
|
||||
type = cInfo.chatType,
|
||||
id = cInfo.apiId,
|
||||
file = files.getOrNull(index),
|
||||
quotedItemId = if (index == 0) quotedItemId else null,
|
||||
mc = content
|
||||
)
|
||||
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
|
||||
}
|
||||
clearState()
|
||||
}
|
||||
} else {
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
withBGApi {
|
||||
sendMessageAsync(null, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,34 +506,17 @@ fun ComposeView(
|
||||
|
||||
fun onAudioAdded(filePath: String, durationMs: Int, finished: Boolean) {
|
||||
val file = File(filePath)
|
||||
chosenAudio.value = file.toUri() to durationMs
|
||||
chatModel.filesToDelete.add(file)
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.VoicePreview(filePath, durationMs, finished))
|
||||
}
|
||||
|
||||
fun allowVoiceToContact() {
|
||||
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
|
||||
val prefs = contact.mergedPreferences.toPreferences().copy(voice = ChatPreference(allow = FeatureAllowed.YES))
|
||||
withApi {
|
||||
val toContact = chatModel.controller.apiSetContactPrefs(contact.contactId, prefs)
|
||||
if (toContact != null) {
|
||||
chatModel.updateContact(toContact)
|
||||
}
|
||||
chatModel.controller.allowFeatureToContact(contact, ChatFeature.Voice)
|
||||
}
|
||||
}
|
||||
|
||||
fun showDisabledVoiceAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.voice_messages_prohibited),
|
||||
text = generalGetString(
|
||||
if (chat.chatInfo is ChatInfo.Direct)
|
||||
R.string.ask_your_contact_to_enable_voice
|
||||
else
|
||||
R.string.only_group_owners_can_enable_voice
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelLinkPreview() {
|
||||
val uri = composeState.value.linkPreview?.uri
|
||||
if (uri != null) {
|
||||
@@ -489,17 +528,70 @@ fun ComposeView(
|
||||
|
||||
fun cancelImages() {
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
|
||||
chosenContent.value = emptyList()
|
||||
}
|
||||
|
||||
fun cancelVoice() {
|
||||
val filePath = recState.value.filePathNullable
|
||||
recState.value = RecordingState.NotStarted
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
|
||||
chosenAudio.value = null
|
||||
withBGApi {
|
||||
RecorderNative.stopRecording?.invoke()
|
||||
AudioPlayer.stop(filePath)
|
||||
filePath?.let { File(it).delete() }
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFile() {
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
|
||||
chosenFile.value = null
|
||||
}
|
||||
|
||||
fun truncateToWords(s: String): String {
|
||||
var acc = ""
|
||||
val word = StringBuilder()
|
||||
for (c in s) {
|
||||
if (c.isLetter() || c.isDigit()) {
|
||||
word.append(c)
|
||||
} else {
|
||||
acc = acc + word.toString() + c
|
||||
word.clear()
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}
|
||||
|
||||
suspend fun sendLiveMessage() {
|
||||
val cs = composeState.value
|
||||
val typedMsg = cs.message
|
||||
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
|
||||
val ci = sendMessageAsync(typedMsg, live = true)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
|
||||
}
|
||||
} else if (cs.liveMessage == null) {
|
||||
val cItem = chatModel.addLiveDummy(chat.chatInfo)
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(cItem, typedMsg = typedMsg, sentMsg = typedMsg, sent = false))
|
||||
}
|
||||
}
|
||||
|
||||
fun liveMessageToSend(lm: LiveMessage, t: String): String? {
|
||||
val s = if (t != lm.typedMsg) truncateToWords(t) else t
|
||||
return if (s != lm.sentMsg) s else null
|
||||
}
|
||||
|
||||
suspend fun updateLiveMessage() {
|
||||
val typedMsg = composeState.value.message
|
||||
val liveMessage = composeState.value.liveMessage
|
||||
if (liveMessage != null) {
|
||||
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
|
||||
if (sentMsg != null) {
|
||||
val ci = sendMessageAsync(sentMsg, live = true)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
|
||||
}
|
||||
} else if (liveMessage.typedMsg != typedMsg) {
|
||||
composeState.value = composeState.value.copy(liveMessage = liveMessage.copy(typedMsg = typedMsg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -564,47 +656,95 @@ fun ComposeView(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
val attachEnabled = !composeState.value.editing && composeState.value.preview !is ComposePreview.VoicePreview
|
||||
IconButton(showChooseAttachment, enabled = attachEnabled) {
|
||||
IconButton(showChooseAttachment, enabled = !composeState.value.attachmentDisabled) {
|
||||
Icon(
|
||||
Icons.Filled.AttachFile,
|
||||
contentDescription = stringResource(R.string.attach),
|
||||
tint = if (attachEnabled) MaterialTheme.colors.primary else HighOrLowlight,
|
||||
tint = if (!composeState.value.attachmentDisabled) MaterialTheme.colors.primary else HighOrLowlight,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.voiceMessageAllowed }
|
||||
val needToAllowVoiceToContact = remember(chat.chatInfo) {
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct -> with(chat.chatInfo.contact.mergedPreferences.voice) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) }
|
||||
LaunchedEffect(allowedVoiceByPrefs) {
|
||||
if (!allowedVoiceByPrefs && chosenAudio.value != null) {
|
||||
if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
// Voice was disabled right when this user records it, just cancel it
|
||||
cancelVoice()
|
||||
}
|
||||
}
|
||||
val needToAllowVoiceToContact = remember(chat.chatInfo) {
|
||||
chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) {
|
||||
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
|
||||
contactPreference.allow == FeatureAllowed.YES
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { recState.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
when(it) {
|
||||
is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false)
|
||||
is RecordingState.Finished -> onAudioAdded(it.filePath, it.durationMs, true)
|
||||
is RecordingState.NotStarted -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCurrentDraft() {
|
||||
if (chatModel.draftChatId.value == chat.id) {
|
||||
chatModel.draft.value = null
|
||||
chatModel.draftChatId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
val activity = LocalContext.current as Activity
|
||||
DisposableEffect(Unit) {
|
||||
val orientation = activity.resources.configuration.orientation
|
||||
onDispose {
|
||||
if (orientation == activity.resources.configuration.orientation) {
|
||||
val cs = composeState.value
|
||||
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
clearCurrentDraft()
|
||||
deleteUnusedFiles()
|
||||
} else if (!composeState.value.empty) {
|
||||
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
|
||||
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
|
||||
}
|
||||
chatModel.draft.value = composeState.value
|
||||
chatModel.draftChatId.value = chat.id
|
||||
} else {
|
||||
clearCurrentDraft()
|
||||
deleteUnusedFiles()
|
||||
}
|
||||
chatModel.removeLiveDummy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SendMsgView(
|
||||
composeState,
|
||||
showVoiceRecordIcon = true,
|
||||
allowedVoiceByPrefs = allowedVoiceByPrefs,
|
||||
needToAllowVoiceToContact = needToAllowVoiceToContact,
|
||||
recState,
|
||||
chat.chatInfo is ChatInfo.Direct,
|
||||
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
|
||||
needToAllowVoiceToContact,
|
||||
allowedVoiceByPrefs,
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
sendMessage = {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
},
|
||||
::onMessageChange,
|
||||
::onAudioAdded,
|
||||
::allowVoiceToContact,
|
||||
::showDisabledVoiceAlert,
|
||||
textStyle
|
||||
sendLiveMessage = ::sendLiveMessage,
|
||||
updateLiveMessage = ::updateLiveMessage,
|
||||
cancelLiveMessage = {
|
||||
composeState.value = composeState.value.copy(liveMessage = null)
|
||||
chatModel.removeLiveDummy()
|
||||
},
|
||||
onMessageChange = ::onMessageChange,
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ fun ComposeVoiceView(
|
||||
val endTime = when {
|
||||
finishedRecording -> duration.value
|
||||
audioPlaying.value -> recordedDurationMs
|
||||
else -> MAX_VOICE_MILLIS_FOR_SENDING.toInt()
|
||||
else -> MAX_VOICE_MILLIS_FOR_SENDING
|
||||
}
|
||||
val to = ((startTime.toDouble() / endTime) * maxWidth.value).dp
|
||||
progressBarWidth.animateTo(to.value, audioProgressBarAnimationSpec())
|
||||
|
||||
@@ -20,6 +20,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.PreferenceToggle
|
||||
|
||||
@Composable
|
||||
fun ContactPreferencesView(
|
||||
@@ -85,6 +86,14 @@ private fun ContactPreferencesLayout(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.contact_preferences))
|
||||
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl ?: 86400))
|
||||
}
|
||||
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowFullDeletion: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) }
|
||||
FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) {
|
||||
applyPrefs(featuresAllowed.copy(fullDelete = it))
|
||||
@@ -112,7 +121,8 @@ private fun FeatureSection(
|
||||
onSelected: (ContactFeatureAllowed) -> Unit
|
||||
) {
|
||||
val enabled = FeatureEnabled.enabled(
|
||||
user = ChatPreference(allow = allowFeature.value.allowed),
|
||||
feature.asymmetric,
|
||||
user = SimpleChatPreference(allow = allowFeature.value.allowed),
|
||||
contact = pref.contactPreference
|
||||
)
|
||||
|
||||
@@ -140,6 +150,50 @@ private fun FeatureSection(
|
||||
SectionTextFooter(feature.enabledDescription(enabled))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimedMessagesFeatureSection(
|
||||
featuresAllowed: ContactFeaturesAllowed,
|
||||
pref: ContactUserPreferenceTimed,
|
||||
allowFeature: State<Boolean>,
|
||||
onTTLUpdated: (Int?) -> Unit,
|
||||
onSelected: (Boolean, Int?) -> Unit
|
||||
) {
|
||||
val enabled = FeatureEnabled.enabled(
|
||||
ChatFeature.TimedMessages.asymmetric,
|
||||
user = TimedMessagesPreference(allow = if (allowFeature.value) FeatureAllowed.YES else FeatureAllowed.NO),
|
||||
contact = pref.contactPreference
|
||||
)
|
||||
|
||||
SectionView(
|
||||
ChatFeature.TimedMessages.text.uppercase(),
|
||||
icon = ChatFeature.TimedMessages.iconFilled,
|
||||
iconTint = if (enabled.forUser) SimplexGreen else if (enabled.forContact) WarningYellow else Color.Red,
|
||||
leadingIcon = true,
|
||||
) {
|
||||
SectionItemView {
|
||||
PreferenceToggle(
|
||||
generalGetString(R.string.chat_preferences_you_allow),
|
||||
checked = allowFeature.value,
|
||||
) { allow ->
|
||||
onSelected(allow, if (allow) featuresAllowed.timedMessagesTTL ?: 86400 else null)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
InfoRow(
|
||||
generalGetString(R.string.chat_preferences_contact_allows),
|
||||
pref.contactPreference.allow.text
|
||||
)
|
||||
SectionDivider()
|
||||
if (featuresAllowed.timedMessagesAllowed) {
|
||||
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
|
||||
TimedMessagesTTLPicker(ttl, onTTLUpdated)
|
||||
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
|
||||
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
SectionView {
|
||||
@@ -153,6 +207,20 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) {
|
||||
val ttlValues = TimedMessagesPreference.ttlValues
|
||||
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
|
||||
SectionItemView {
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.delete_after),
|
||||
values.map { it to TimedMessagesPreference.ttlText(it) },
|
||||
selection,
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(R.string.save_preferences_question),
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCodeScanner
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanCodeLayout(verifyCode, close)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.scan_code), false)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
) {
|
||||
QRCodeScanner { text ->
|
||||
verifyCode(text) {
|
||||
if (it) {
|
||||
close()
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.incorrect_code)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.scan_code_from_contacts_app))
|
||||
}
|
||||
}
|
||||
@@ -6,28 +6,36 @@ import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.text.InputType
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.view.inputmethod.*
|
||||
import android.widget.EditText
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
@@ -35,184 +43,124 @@ import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.widget.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
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.io.*
|
||||
|
||||
@Composable
|
||||
fun SendMsgView(
|
||||
composeState: MutableState<ComposeState>,
|
||||
showVoiceRecordIcon: Boolean,
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
recState: MutableState<RecordingState>,
|
||||
isDirectChat: Boolean,
|
||||
liveMessageAlertShown: SharedPreference<Boolean>,
|
||||
needToAllowVoiceToContact: Boolean,
|
||||
sendMessage: () -> Unit,
|
||||
onMessageChange: (String) -> Unit,
|
||||
onAudioAdded: (String, Int, Boolean) -> Unit,
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
showDisabledVoiceAlert: () -> Unit,
|
||||
sendMessage: () -> Unit,
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
updateLiveMessage: (suspend () -> Unit)? = null,
|
||||
cancelLiveMessage: (() -> Unit)? = null,
|
||||
onMessageChange: (String) -> Unit,
|
||||
textStyle: MutableState<TextStyle>
|
||||
) {
|
||||
Column(Modifier.padding(vertical = 8.dp)) {
|
||||
Box {
|
||||
val cs = composeState.value
|
||||
val attachEnabled = !composeState.value.editing
|
||||
val filePath = rememberSaveable { mutableStateOf(null as String?) }
|
||||
var recordingTimeRange by rememberSaveable(saver = LongRange.saver) { mutableStateOf(0L..0L) } // since..to
|
||||
val showVoiceButton = ((cs.message.isEmpty() || recordingTimeRange.first > 0L) && showVoiceRecordIcon && attachEnabled && cs.preview is ComposePreview.NoPreview) || filePath.value != null
|
||||
Box(if (recordingTimeRange.first == 0L)
|
||||
Modifier
|
||||
else
|
||||
Modifier.clickable(false, onClick = {})
|
||||
) {
|
||||
NativeKeyboard(composeState, textStyle, onMessageChange)
|
||||
Box(Modifier.padding(vertical = 8.dp)) {
|
||||
val cs = composeState.value
|
||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
||||
val 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)
|
||||
// Disable clicks on text field
|
||||
if (cs.preview is ComposePreview.VoicePreview) {
|
||||
Box(Modifier.matchParentSize().clickable(enabled = false, onClick = { }))
|
||||
}
|
||||
if (showDeleteTextButton.value) {
|
||||
DeleteTextButton(composeState)
|
||||
}
|
||||
Box(Modifier.align(Alignment.BottomEnd)) {
|
||||
val sendButtonSize = remember { Animatable(36f) }
|
||||
val sendButtonAlpha = remember { Animatable(1f) }
|
||||
val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO))
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
// Making LiveMessage alive when screen orientation was changed
|
||||
if (cs.liveMessage != null && sendLiveMessage != null && updateLiveMessage != null) {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
}
|
||||
}
|
||||
Box(Modifier.align(Alignment.BottomEnd)) {
|
||||
val icon = if (cs.editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward
|
||||
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
|
||||
if (cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.VoicePreview || cs.preview is ComposePreview.FilePreview)) {
|
||||
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp)
|
||||
} else if (!showVoiceButton) {
|
||||
IconButton(sendMessage, Modifier.size(36.dp), enabled = cs.sendEnabled()) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val permissionsState = rememberMultiplePermissionsState(
|
||||
permissions = listOf(
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
)
|
||||
)
|
||||
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
|
||||
val recordingInProgress: State<Boolean> = remember { rec.recordingInProgress }
|
||||
var now by remember { mutableStateOf(System.currentTimeMillis()) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (isActive) {
|
||||
now = System.currentTimeMillis()
|
||||
if (recordingTimeRange.first != 0L && recordingInProgress.value && composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
filePath.value?.let { onAudioAdded(it, (now - recordingTimeRange.first).toInt(), false) }
|
||||
}
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
val stopRecordingAndAddAudio: () -> Unit = {
|
||||
rec.stop()
|
||||
recordingTimeRange = recordingTimeRange.first..System.currentTimeMillis()
|
||||
filePath.value?.let { onAudioAdded(it, (recordingTimeRange.last - recordingTimeRange.first).toInt(), true) }
|
||||
}
|
||||
val startStopRecording: () -> Unit = {
|
||||
when {
|
||||
showProgress -> ProgressIndicator()
|
||||
showVoiceButton -> {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val stopRecOnNextClick = remember { mutableStateOf(false) }
|
||||
when {
|
||||
needToAllowVoiceToContact -> {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.allow_voice_messages_question),
|
||||
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
|
||||
confirmText = generalGetString(R.string.allow_verb),
|
||||
dismissText = generalGetString(R.string.cancel_verb),
|
||||
onConfirm = allowVoiceToContact,
|
||||
)
|
||||
needToAllowVoiceToContact || !allowedVoiceByPrefs -> {
|
||||
DisallowedVoiceButton {
|
||||
if (needToAllowVoiceToContact) {
|
||||
showNeedToAllowVoiceAlert(allowVoiceToContact)
|
||||
} else {
|
||||
showDisabledVoiceAlert(isDirectChat)
|
||||
}
|
||||
}
|
||||
}
|
||||
!allowedVoiceByPrefs -> showDisabledVoiceAlert()
|
||||
!permissionsState.allPermissionsGranted -> permissionsState.launchMultiplePermissionRequest()
|
||||
recordingInProgress.value -> stopRecordingAndAddAudio()
|
||||
filePath.value == null -> {
|
||||
recordingTimeRange = System.currentTimeMillis()..0L
|
||||
filePath.value = rec.start(stopRecordingAndAddAudio)
|
||||
filePath.value?.let { onAudioAdded(it, (now - recordingTimeRange.first).toInt(), false) }
|
||||
!permissionsState.allPermissionsGranted ->
|
||||
VoiceButtonWithoutPermission { permissionsState.launchMultiplePermissionRequest() }
|
||||
else ->
|
||||
RecordVoiceView(recState, stopRecOnNextClick)
|
||||
}
|
||||
if (sendLiveMessage != null
|
||||
&& updateLiveMessage != null
|
||||
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
|
||||
&& cs.contextItem is ComposeContextItem.NoContextItem) {
|
||||
Spacer(Modifier.width(10.dp))
|
||||
StartLiveMessageButton {
|
||||
if (composeState.value.preview is ComposePreview.NoPreview) {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var stopRecOnNextClick by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(stopRecOnNextClick) {
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
if (stopRecOnNextClick) {
|
||||
// Lock orientation to current orientation because screen rotation will break the recording
|
||||
activity.requestedOrientation = if (activity.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
else
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
// Unlock orientation
|
||||
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
|
||||
}
|
||||
val cleanUp = { remove: Boolean ->
|
||||
rec.stop()
|
||||
AudioPlayer.stop(filePath.value)
|
||||
if (remove) filePath.value?.let { File(it).delete() }
|
||||
filePath.value = null
|
||||
stopRecOnNextClick = false
|
||||
recordingTimeRange = 0L..0L
|
||||
}
|
||||
LaunchedEffect(cs.preview) {
|
||||
if (cs.preview !is ComposePreview.VoicePreview && filePath.value != null) {
|
||||
// Pressed on X icon in preview
|
||||
cleanUp(true)
|
||||
}
|
||||
}
|
||||
val interactionSource = interactionSourceWithTapDetection(
|
||||
// It's just a key for triggering dropping a state in the compose function. Without it
|
||||
// nothing will react on changed params like needToAllowVoiceToContact or allowedVoiceByPrefs
|
||||
needToAllowVoiceToContact.toString() + allowedVoiceByPrefs.toString(),
|
||||
onPress = {
|
||||
if (filePath.value == null) startStopRecording()
|
||||
},
|
||||
onClick = {
|
||||
// Voice not allowed or not granted voice record permission for the app
|
||||
if (!allowedVoiceByPrefs || !permissionsState.allPermissionsGranted) return@interactionSourceWithTapDetection
|
||||
if (!recordingInProgress.value && filePath.value != null) {
|
||||
sendMessage()
|
||||
cleanUp(false)
|
||||
} else if (stopRecOnNextClick) {
|
||||
stopRecordingAndAddAudio()
|
||||
stopRecOnNextClick = false
|
||||
} else {
|
||||
// tapped and didn't hold a finger
|
||||
stopRecOnNextClick = true
|
||||
}
|
||||
},
|
||||
onCancel = startStopRecording,
|
||||
onRelease = startStopRecording
|
||||
)
|
||||
val sendButtonModifier = if (recordingTimeRange.last != 0L)
|
||||
Modifier.clip(CircleShape).background(color)
|
||||
else
|
||||
Modifier
|
||||
IconButton({}, Modifier.size(36.dp), enabled = !cs.inProgress, interactionSource = interactionSource) {
|
||||
Icon(
|
||||
when {
|
||||
recordingTimeRange.last != 0L -> Icons.Outlined.ArrowUpward
|
||||
stopRecOnNextClick -> Icons.Filled.Stop
|
||||
allowedVoiceByPrefs -> Icons.Filled.KeyboardVoice
|
||||
else -> Icons.Outlined.KeyboardVoice
|
||||
},
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = when {
|
||||
recordingTimeRange.last != 0L -> Color.White
|
||||
stopRecOnNextClick -> MaterialTheme.colors.primary
|
||||
allowedVoiceByPrefs -> MaterialTheme.colors.primary
|
||||
else -> HighOrLowlight
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
.then(sendButtonModifier)
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
rec.stop()
|
||||
}
|
||||
cs.liveMessage?.sent == false && cs.message.isEmpty() -> {
|
||||
CancelLiveMessageButton {
|
||||
cancelLiveMessage?.invoke()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val cs = composeState.value
|
||||
val icon = if (cs.editing || cs.liveMessage != null) Icons.Filled.Check else Icons.Outlined.ArrowUpward
|
||||
val disabled = !cs.sendEnabled() ||
|
||||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||
cs.endLiveDisabled
|
||||
if (cs.liveMessage == null &&
|
||||
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
|
||||
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||
sendLiveMessage != null && updateLiveMessage != null
|
||||
) {
|
||||
var showDropdown by rememberSaveable { mutableStateOf(false) }
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showDropdown,
|
||||
onDismissRequest = { showDropdown = false },
|
||||
Modifier.width(220.dp),
|
||||
) {
|
||||
ItemAction(
|
||||
generalGetString(R.string.send_live_message),
|
||||
Icons.Filled.Bolt,
|
||||
onClick = {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,6 +172,7 @@ fun SendMsgView(
|
||||
private fun NativeKeyboard(
|
||||
composeState: MutableState<ComposeState>,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
onMessageChange: (String) -> Unit
|
||||
) {
|
||||
val cs = composeState.value
|
||||
@@ -234,7 +183,6 @@ private fun NativeKeyboard(
|
||||
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
|
||||
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||
|
||||
var showKeyboard by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(cs.contextItem) {
|
||||
if (cs.contextItem is ComposeContextItem.QuotedItem) {
|
||||
@@ -255,6 +203,7 @@ private fun NativeKeyboard(
|
||||
) {
|
||||
super.setOnReceiveContentListener(mimeTypes, listener)
|
||||
}
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
@@ -301,6 +250,7 @@ private fun NativeKeyboard(
|
||||
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
|
||||
showKeyboard = false
|
||||
}
|
||||
showDeleteTextButton.value = it.lineCount >= 4
|
||||
}
|
||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
Text(
|
||||
@@ -312,6 +262,296 @@ private fun NativeKeyboard(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) {
|
||||
IconButton(
|
||||
{ composeState.value = composeState.value.copy(message = "") },
|
||||
Modifier.align(Alignment.TopEnd).size(36.dp)
|
||||
) {
|
||||
Icon(Icons.Filled.Close, null, Modifier.padding(7.dp).size(36.dp), tint = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) {
|
||||
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
|
||||
DisposableEffect(Unit) { onDispose { rec.stop() } }
|
||||
val stopRecordingAndAddAudio: () -> Unit = {
|
||||
recState.value.filePathNullable?.let {
|
||||
recState.value = RecordingState.Finished(it, rec.stop())
|
||||
}
|
||||
}
|
||||
if (stopRecOnNextClick.value) {
|
||||
LaunchedEffect(recState.value) {
|
||||
if (recState.value is RecordingState.NotStarted) {
|
||||
stopRecOnNextClick.value = false
|
||||
}
|
||||
}
|
||||
// Lock orientation to current orientation because screen rotation will break the recording
|
||||
LockToCurrentOrientationUntilDispose()
|
||||
StopRecordButton(stopRecordingAndAddAudio)
|
||||
} else {
|
||||
val startRecording: () -> Unit = {
|
||||
recState.value = RecordingState.Started(
|
||||
filePath = rec.start { progress: Int?, finished: Boolean ->
|
||||
val state = recState.value
|
||||
if (state is RecordingState.Started && progress != null) {
|
||||
recState.value = if (!finished)
|
||||
RecordingState.Started(state.filePath, progress)
|
||||
else
|
||||
RecordingState.Finished(state.filePath, progress)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
val interactionSource = interactionSourceWithTapDetection(
|
||||
onPress = { if (recState.value is RecordingState.NotStarted) startRecording() },
|
||||
onClick = {
|
||||
if (stopRecOnNextClick.value) {
|
||||
stopRecordingAndAddAudio()
|
||||
} else {
|
||||
// tapped and didn't hold a finger
|
||||
stopRecOnNextClick.value = true
|
||||
}
|
||||
},
|
||||
onCancel = stopRecordingAndAddAudio,
|
||||
onRelease = stopRecordingAndAddAudio
|
||||
)
|
||||
RecordVoiceButton(interactionSource)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisallowedVoiceButton(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
Icons.Outlined.KeyboardVoice,
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = HighOrLowlight,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoiceButtonWithoutPermission(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
Icons.Filled.KeyboardVoice,
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockToCurrentOrientationUntilDispose() {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context as Activity
|
||||
val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager
|
||||
val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation
|
||||
activity.requestedOrientation = when (rotation) {
|
||||
android.view.Surface.ROTATION_90 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
android.view.Surface.ROTATION_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||
android.view.Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
// Unlock orientation
|
||||
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StopRecordButton(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
Icons.Filled.Stop,
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
|
||||
IconButton({}, Modifier.size(36.dp), interactionSource = interactionSource) {
|
||||
Icon(
|
||||
Icons.Filled.KeyboardVoice,
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
CircularProgressIndicator(Modifier.size(36.dp).padding(4.dp), color = HighOrLowlight, strokeWidth = 3.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CancelLiveMessageButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
stringResource(R.string.icon_descr_cancel_live_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendMsgButton(
|
||||
icon: ImageVector,
|
||||
sizeDp: Animatable<Float, AnimationVector1D>,
|
||||
alpha: Animatable<Float, AnimationVector1D>,
|
||||
enabled: Boolean,
|
||||
sendMessage: () -> Unit,
|
||||
onLongClick: (() -> Unit)? = null
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.combinedClickable(
|
||||
onClick = sendMessage,
|
||||
onLongClick = onLongClick,
|
||||
enabled = enabled,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(sizeDp.value.dp)
|
||||
.padding(4.dp)
|
||||
.alpha(alpha.value)
|
||||
.clip(CircleShape)
|
||||
.background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight)
|
||||
.padding(3.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StartLiveMessageButton(onClick: () -> Unit) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Box(
|
||||
modifier = Modifier.requiredSize(36.dp)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
enabled = true,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(bounded = false, radius = 24.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Bolt,
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startLiveMessage(
|
||||
scope: CoroutineScope,
|
||||
send: suspend () -> Unit,
|
||||
update: suspend () -> Unit,
|
||||
sendButtonSize: Animatable<Float, AnimationVector1D>,
|
||||
sendButtonAlpha: Animatable<Float, AnimationVector1D>,
|
||||
composeState: MutableState<ComposeState>,
|
||||
liveMessageAlertShown: SharedPreference<Boolean>
|
||||
) {
|
||||
fun run() {
|
||||
scope.launch {
|
||||
while (composeState.value.liveMessage != null) {
|
||||
sendButtonSize.animateTo(if (sendButtonSize.value == 36f) 32f else 36f, tween(700, 50))
|
||||
}
|
||||
sendButtonSize.snapTo(36f)
|
||||
}
|
||||
scope.launch {
|
||||
while (composeState.value.liveMessage != null) {
|
||||
sendButtonAlpha.animateTo(if (sendButtonAlpha.value == 1f) 0.75f else 1f, tween(700, 50))
|
||||
}
|
||||
sendButtonAlpha.snapTo(1f)
|
||||
}
|
||||
scope.launch {
|
||||
delay(3000)
|
||||
while (composeState.value.liveMessage != null) {
|
||||
update()
|
||||
delay(3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun start() = withBGApi {
|
||||
if (composeState.value.liveMessage == null) {
|
||||
send()
|
||||
}
|
||||
run()
|
||||
}
|
||||
|
||||
if (liveMessageAlertShown.state.value) {
|
||||
start()
|
||||
} else {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.live_message),
|
||||
text = generalGetString(R.string.send_live_message_desc),
|
||||
confirmText = generalGetString(R.string.send_verb),
|
||||
onConfirm = {
|
||||
liveMessageAlertShown.set(true)
|
||||
start()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNeedToAllowVoiceAlert(onConfirm: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.allow_voice_messages_question),
|
||||
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
|
||||
confirmText = generalGetString(R.string.allow_verb),
|
||||
dismissText = generalGetString(R.string.cancel_verb),
|
||||
onConfirm = onConfirm,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showDisabledVoiceAlert(isDirectChat: Boolean) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.voice_messages_prohibited),
|
||||
text = generalGetString(
|
||||
if (isDirectChat)
|
||||
R.string.ask_your_contact_to_enable_voice
|
||||
else
|
||||
R.string.only_group_owners_can_enable_voice
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
@@ -326,13 +566,14 @@ fun PreviewSendMsgView() {
|
||||
SendMsgView(
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
showVoiceRecordIcon = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
onAudioAdded = { _, _, _ -> },
|
||||
allowVoiceToContact = {},
|
||||
showDisabledVoiceAlert = {},
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
@@ -353,13 +594,14 @@ fun PreviewSendMsgViewEditing() {
|
||||
SendMsgView(
|
||||
composeState = remember { mutableStateOf(composeStateEditing) },
|
||||
showVoiceRecordIcon = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
onAudioAdded = { _, _, _ -> },
|
||||
allowVoiceToContact = {},
|
||||
showDisabledVoiceAlert = {},
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
@@ -375,18 +617,19 @@ fun PreviewSendMsgViewEditing() {
|
||||
fun PreviewSendMsgViewInProgress() {
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
val textStyle = remember { mutableStateOf(smallFont) }
|
||||
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt"), inProgress = true, useLinkPreviews = true)
|
||||
val composeStateInProgress = ComposeState(preview = ComposePreview.FilePreview("test.txt", getAppFileUri("test.txt")), inProgress = true, useLinkPreviews = true)
|
||||
SimpleXTheme {
|
||||
SendMsgView(
|
||||
composeState = remember { mutableStateOf(composeStateInProgress) },
|
||||
showVoiceRecordIcon = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
allowVoiceToContact = {},
|
||||
sendMessage = {},
|
||||
onMessageChange = { _ -> },
|
||||
onAudioAdded = { _, _, _ -> },
|
||||
allowVoiceToContact = {},
|
||||
showDisabledVoiceAlert = {},
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
|
||||
@Composable
|
||||
fun VerifyCodeView(
|
||||
displayName: String,
|
||||
connectionCode: String?,
|
||||
connectionVerified: Boolean,
|
||||
verify: suspend (String?) -> Pair<Boolean, String>?,
|
||||
close: () -> Unit,
|
||||
) {
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeLayout(
|
||||
displayName,
|
||||
connectionCode,
|
||||
connectionVerified,
|
||||
verifyCode = { newCode, cb ->
|
||||
withBGApi {
|
||||
val res = verify(newCode)
|
||||
if (res != null) {
|
||||
val (verified) = res
|
||||
cb(verified)
|
||||
if (verified) close()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerifyCodeLayout(
|
||||
displayName: String,
|
||||
connectionCode: String,
|
||||
connectionVerified: Boolean,
|
||||
verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.security_code), false)
|
||||
val splitCode = splitToParts(connectionCode, 24)
|
||||
Row(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center) {
|
||||
if (connectionVerified) {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 4.dp).size(22.dp), tint = HighOrLowlight)
|
||||
Text(String.format(stringResource(R.string.is_verified), displayName))
|
||||
} else {
|
||||
Text(String.format(stringResource(R.string.is_not_verified), displayName))
|
||||
}
|
||||
}
|
||||
|
||||
SectionView {
|
||||
QRCode(connectionCode, Modifier.aspectRatio(1f))
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.weight(2f))
|
||||
SelectionContainer(Modifier.padding(vertical = DEFAULT_PADDING_HALF, horizontal = DEFAULT_PADDING_HALF)) {
|
||||
Text(
|
||||
splitCode,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 18.sp,
|
||||
maxLines = 20
|
||||
)
|
||||
}
|
||||
val context = LocalContext.current
|
||||
Box(Modifier.weight(1f)) {
|
||||
IconButton({ shareText(context, connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) {
|
||||
Icon(Icons.Filled.Share, null, tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
|
||||
Text(
|
||||
generalGetString(R.string.to_verify_compare),
|
||||
Modifier.padding(bottom = DEFAULT_PADDING)
|
||||
)
|
||||
|
||||
Row(
|
||||
Modifier.padding(bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
if (connectionVerified) {
|
||||
SimpleButton(generalGetString(R.string.clear_verification), Icons.Outlined.Shield) {
|
||||
verifyCode(null) {}
|
||||
}
|
||||
} else {
|
||||
SimpleButton(generalGetString(R.string.scan_code), Icons.Outlined.QrCode) {
|
||||
ModalManager.shared.showModal {
|
||||
ScanCodeView(verifyCode) { }
|
||||
}
|
||||
}
|
||||
SimpleButton(generalGetString(R.string.mark_code_verified), Icons.Outlined.VerifiedUser) {
|
||||
verifyCode(connectionCode) { verified ->
|
||||
if (!verified) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.incorrect_code)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitToParts(s: String, length: Int): String {
|
||||
if (length >= s.length) return s
|
||||
return (0..(s.length - 1) / length)
|
||||
.map { s.drop(it * length).take(length) }
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
+69
-18
@@ -6,6 +6,7 @@ import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -23,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
@@ -32,7 +34,7 @@ import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, onGroupLinkUpdated: (String?) -> Unit, close: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
@@ -45,6 +47,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
|
||||
.sortedBy { it.displayName.lowercase() },
|
||||
developerTools,
|
||||
groupLink,
|
||||
addMembers = {
|
||||
withApi {
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
@@ -56,8 +59,23 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
showMemberInfo = { member ->
|
||||
withApi {
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
member to null
|
||||
}
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
ModalManager.shared.showModalCloseable(true) { closeCurrent ->
|
||||
GroupMemberInfoView(groupInfo, member, stats, chatModel, closeCurrent) { closeCurrent(); close() }
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) {
|
||||
closeCurrent()
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -78,8 +96,7 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
withApi {
|
||||
val groupLink = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
|
||||
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink) }
|
||||
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, onGroupLinkUpdated) }
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -128,6 +145,7 @@ fun GroupChatInfoLayout(
|
||||
groupInfo: GroupInfo,
|
||||
members: List<GroupMember>,
|
||||
developerTools: Boolean,
|
||||
groupLink: String?,
|
||||
addMembers: () -> Unit,
|
||||
showMemberInfo: (GroupMember) -> Unit,
|
||||
editGroupProfile: () -> Unit,
|
||||
@@ -163,7 +181,13 @@ fun GroupChatInfoLayout(
|
||||
|
||||
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
SectionItemView(manageGroupLink) { GroupLinkButton() }
|
||||
SectionItemView(manageGroupLink) {
|
||||
if (groupLink == null) {
|
||||
CreateGroupLinkButton()
|
||||
} else {
|
||||
GroupLinkButton()
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
|
||||
SectionItemView(onAddMembersClick) {
|
||||
@@ -206,7 +230,7 @@ fun GroupChatInfoLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
||||
private fun GroupChatInfoHeader(cInfo: ChatInfo) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -239,7 +263,7 @@ private fun GroupPreferencesButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
|
||||
private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -255,7 +279,7 @@ fun AddMembersButton(tint: Color = MaterialTheme.colors.primary) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
|
||||
private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) {
|
||||
Column {
|
||||
members.forEachIndexed { index, member ->
|
||||
SectionItemView({ showMemberInfo(member) }, minHeight = 50.dp) {
|
||||
@@ -269,7 +293,7 @@ fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Uni
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
private fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -281,10 +305,15 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
) {
|
||||
ProfileImage(size = 46.dp, member.image)
|
||||
Column {
|
||||
Text(
|
||||
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = if (member.memberIncognito) Indigo else Color.Unspecified
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (member.verified) {
|
||||
MemberVerifiedShield()
|
||||
}
|
||||
Text(
|
||||
member.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = if (member.memberIncognito) Indigo else Color.Unspecified
|
||||
)
|
||||
}
|
||||
val s = member.memberStatus.shortText
|
||||
val statusDescr = if (user) String.format(generalGetString(R.string.group_info_member_you), s) else s
|
||||
Text(
|
||||
@@ -304,7 +333,12 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupLinkButton() {
|
||||
private fun MemberVerifiedShield() {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 3.dp).size(16.dp), tint = HighOrLowlight)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupLinkButton() {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
@@ -320,6 +354,23 @@ fun GroupLinkButton() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateGroupLinkButton() {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.AddLink,
|
||||
stringResource(R.string.create_group_link),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.create_group_link))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditGroupProfileButton() {
|
||||
Row(
|
||||
@@ -338,7 +389,7 @@ fun EditGroupProfileButton() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LeaveGroupButton() {
|
||||
private fun LeaveGroupButton() {
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -354,7 +405,7 @@ fun LeaveGroupButton() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteGroupButton() {
|
||||
private fun DeleteGroupButton() {
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -376,12 +427,12 @@ fun PreviewGroupChatInfoLayout() {
|
||||
GroupChatInfoLayout(
|
||||
chat = Chat(
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
chatItems = arrayListOf(),
|
||||
serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT"))
|
||||
chatItems = arrayListOf()
|
||||
),
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
groupLink = null,
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package chat.simplex.app.views.chat.group
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -15,22 +17,32 @@ 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.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
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?) {
|
||||
var groupLink by remember { mutableStateOf(connReqContact) }
|
||||
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, onGroupLinkUpdated: (String?) -> Unit) {
|
||||
var groupLink by rememberSaveable { mutableStateOf(connReqContact) }
|
||||
var creatingLink by rememberSaveable { mutableStateOf(false) }
|
||||
val cxt = LocalContext.current
|
||||
fun createLink() {
|
||||
creatingLink = true
|
||||
withApi {
|
||||
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
|
||||
onGroupLinkUpdated(groupLink)
|
||||
creatingLink = false
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (groupLink == null && !creatingLink) {
|
||||
createLink()
|
||||
}
|
||||
}
|
||||
GroupLinkLayout(
|
||||
groupLink = groupLink,
|
||||
createLink = {
|
||||
withApi {
|
||||
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
|
||||
}
|
||||
},
|
||||
creatingLink,
|
||||
createLink = ::createLink,
|
||||
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
|
||||
deleteLink = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -42,17 +54,22 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St
|
||||
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
||||
if (r) {
|
||||
groupLink = null
|
||||
onGroupLinkUpdated(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
if (creatingLink) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupLinkLayout(
|
||||
groupLink: String?,
|
||||
creatingLink: Boolean,
|
||||
createLink: () -> Unit,
|
||||
share: () -> Unit,
|
||||
deleteLink: () -> Unit
|
||||
@@ -74,7 +91,7 @@ fun GroupLinkLayout(
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (groupLink == null) {
|
||||
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, click = createLink)
|
||||
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))
|
||||
Row(
|
||||
@@ -99,3 +116,18 @@ fun GroupLinkLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProgressIndicator() {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+71
-26
@@ -5,6 +5,7 @@ import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -21,19 +23,20 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.SimplexServers
|
||||
import chat.simplex.app.views.chat.SwitchAddressButton
|
||||
import chat.simplex.app.views.chatlist.openChat
|
||||
import chat.simplex.app.views.chat.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun GroupMemberInfoView(
|
||||
groupInfo: GroupInfo,
|
||||
member: GroupMember,
|
||||
connStats: ConnectionStats?,
|
||||
connectionCode: String?,
|
||||
chatModel: ChatModel,
|
||||
close: () -> Unit,
|
||||
closeAll: () -> Unit, // Close all open windows up to ChatView
|
||||
@@ -49,6 +52,7 @@ fun GroupMemberInfoView(
|
||||
connStats,
|
||||
newRole,
|
||||
developerTools,
|
||||
connectionCode,
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
knownDirectChat = {
|
||||
withApi {
|
||||
@@ -62,11 +66,9 @@ fun GroupMemberInfoView(
|
||||
withApi {
|
||||
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
|
||||
if (c != null) {
|
||||
// TODO it's not correct to blindly set network status to connected - we should manage network status in model / backend
|
||||
val newChat = c.copy(serverInfo = Chat.ServerInfo(networkStatus = Chat.NetworkStatus.Connected()))
|
||||
chatModel.addChat(newChat)
|
||||
chatModel.addChat(c)
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatId.value = newChat.id
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
}
|
||||
@@ -91,6 +93,32 @@ fun GroupMemberInfoView(
|
||||
},
|
||||
switchMemberAddress = {
|
||||
switchMemberAddress(chatModel, groupInfo, member)
|
||||
},
|
||||
verifyClicked = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem ->
|
||||
VerifyCodeView(
|
||||
mem.displayName,
|
||||
connectionCode,
|
||||
mem.verified,
|
||||
verify = { code ->
|
||||
chatModel.controller.apiVerifyGroupMember(mem.groupId, mem.groupMemberId, code)?.let { r ->
|
||||
val (verified, existingCode) = r
|
||||
chatModel.upsertGroupMember(
|
||||
groupInfo,
|
||||
mem.copy(
|
||||
activeConn = mem.activeConn?.copy(
|
||||
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
|
||||
)
|
||||
)
|
||||
)
|
||||
r
|
||||
}
|
||||
},
|
||||
close,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -120,12 +148,14 @@ fun GroupMemberInfoLayout(
|
||||
connStats: ConnectionStats?,
|
||||
newRole: MutableState<GroupMemberRole>,
|
||||
developerTools: Boolean,
|
||||
connectionCode: String?,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
knownDirectChat: (Chat) -> Unit,
|
||||
newDirectChat: (Long) -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
verifyClicked: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -141,17 +171,25 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
val contactId = member.memberContactId
|
||||
if (contactId != null) {
|
||||
val chat = getContactChat(contactId)
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directContact) {
|
||||
if (member.memberActive) {
|
||||
val contactId = member.memberContactId
|
||||
if (contactId != null) {
|
||||
SectionView {
|
||||
OpenChatButton(onClick = { knownDirectChat(chat) })
|
||||
}
|
||||
SectionSpacer()
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
SectionView {
|
||||
OpenChatButton(onClick = { newDirectChat(contactId) })
|
||||
val chat = getContactChat(contactId)
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
OpenChatButton(onClick = { knownDirectChat(chat) })
|
||||
if (connectionCode != null) {
|
||||
SectionDivider()
|
||||
}
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
OpenChatButton(onClick = { newDirectChat(contactId) })
|
||||
if (connectionCode != null) {
|
||||
SectionDivider()
|
||||
}
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(member.verified, verifyClicked)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
@@ -178,10 +216,10 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
if (connStats != null) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
SwitchAddressButton(switchMemberAddress)
|
||||
SectionDivider()
|
||||
if (connStats != null) {
|
||||
val rcvServers = connStats.rcvServers
|
||||
val sndServers = connStats.sndServers
|
||||
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
|
||||
@@ -196,8 +234,8 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
SectionView {
|
||||
@@ -224,12 +262,17 @@ fun GroupMemberInfoHeader(member: GroupMember) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight)
|
||||
Text(
|
||||
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (member.verified) {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.padding(end = 6.dp, top = 4.dp).size(24.dp), tint = HighOrLowlight)
|
||||
}
|
||||
Text(
|
||||
member.displayName, style = MaterialTheme.typography.h1.copy(fontWeight = FontWeight.Normal),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (member.fullName != "" && member.fullName != member.displayName) {
|
||||
Text(
|
||||
member.fullName, style = MaterialTheme.typography.h2,
|
||||
@@ -319,12 +362,14 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
connStats = null,
|
||||
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
|
||||
developerTools = false,
|
||||
connectionCode = "123",
|
||||
getContactChat = { Chat.sampleData },
|
||||
knownDirectChat = {},
|
||||
newDirectChat = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
verifyClicked = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+42
-11
@@ -18,7 +18,9 @@ import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.TimedMessagesTTLPicker
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
|
||||
|
||||
@Composable
|
||||
fun GroupPreferencesView(m: ChatModel, chatId: String, close: () -> Unit,) {
|
||||
@@ -74,18 +76,30 @@ private fun GroupPreferencesLayout(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.group_preferences))
|
||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.enable) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
applyPrefs(preferences.copy(timedMessages = preferences.timedMessages.copy(ttl = ttl ?: 86400)))
|
||||
}
|
||||
FeatureSection(GroupFeature.TimedMessages, timedMessages, groupInfo, preferences, onTTLUpdated) { enable ->
|
||||
if (enable == GroupFeatureEnabled.ON) {
|
||||
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = preferences.timedMessages.ttl ?: 86400)))
|
||||
} else {
|
||||
applyPrefs(preferences.copy(timedMessages = TimedMessagesGroupPreference(enable = enable, ttl = currentPreferences.timedMessages.ttl)))
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowDirectMessages = remember(preferences) { mutableStateOf(preferences.directMessages.enable) }
|
||||
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo) {
|
||||
FeatureSection(GroupFeature.DirectMessages, allowDirectMessages, groupInfo, preferences, onTTLUpdated) {
|
||||
applyPrefs(preferences.copy(directMessages = GroupPreference(enable = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.enable) }
|
||||
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo) {
|
||||
FeatureSection(GroupFeature.FullDelete, allowFullDeletion, groupInfo, preferences, onTTLUpdated) {
|
||||
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
|
||||
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo) {
|
||||
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
|
||||
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
|
||||
}
|
||||
if (groupInfo.canEdit) {
|
||||
@@ -100,21 +114,34 @@ private fun GroupPreferencesLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureSection(feature: GroupFeature, enableFeature: State<GroupFeatureEnabled>, groupInfo: GroupInfo, onSelected: (GroupFeatureEnabled) -> Unit) {
|
||||
private fun FeatureSection(
|
||||
feature: GroupFeature,
|
||||
enableFeature: State<GroupFeatureEnabled>,
|
||||
groupInfo: GroupInfo,
|
||||
preferences: FullGroupPreferences,
|
||||
onTTLUpdated: (Int?) -> Unit,
|
||||
onSelected: (GroupFeatureEnabled) -> Unit
|
||||
) {
|
||||
SectionView {
|
||||
val on = enableFeature.value == GroupFeatureEnabled.ON
|
||||
val icon = if (on) feature.iconFilled else feature.icon
|
||||
val iconTint = if (on) SimplexGreen else HighOrLowlight
|
||||
val timedOn = feature == GroupFeature.TimedMessages && enableFeature.value == GroupFeatureEnabled.ON
|
||||
if (groupInfo.canEdit) {
|
||||
SectionItemView {
|
||||
ExposedDropDownSettingRow(
|
||||
PreferenceToggleWithIcon(
|
||||
feature.text,
|
||||
GroupFeatureEnabled.values().map { it to it.text },
|
||||
enableFeature,
|
||||
icon = icon,
|
||||
iconTint = iconTint,
|
||||
onSelected = onSelected
|
||||
)
|
||||
icon,
|
||||
iconTint,
|
||||
enableFeature.value == GroupFeatureEnabled.ON,
|
||||
) { checked ->
|
||||
onSelected(if (checked) GroupFeatureEnabled.ON else GroupFeatureEnabled.OFF)
|
||||
}
|
||||
}
|
||||
if (timedOn) {
|
||||
SectionDivider()
|
||||
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
|
||||
TimedMessagesTTLPicker(ttl, onTTLUpdated)
|
||||
}
|
||||
} else {
|
||||
InfoRow(
|
||||
@@ -123,6 +150,10 @@ private fun FeatureSection(feature: GroupFeature, enableFeature: State<GroupFeat
|
||||
icon = icon,
|
||||
iconTint = iconTint,
|
||||
)
|
||||
if (timedOn) {
|
||||
SectionDivider()
|
||||
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(feature.enableDescription(enableFeature.value, groupInfo.canEdit))
|
||||
|
||||
+7
-1
@@ -136,7 +136,13 @@ fun GroupProfileLayout(
|
||||
if (enabled) {
|
||||
Text(
|
||||
stringResource(R.string.save_group_profile),
|
||||
modifier = Modifier.clickable { saveProfile(GroupProfile(displayName.value, fullName.value, profileImage.value)) },
|
||||
modifier = Modifier.clickable {
|
||||
saveProfile(groupProfile.copy(
|
||||
displayName = displayName.value,
|
||||
fullName = fullName.value,
|
||||
image = profileImage.value
|
||||
))
|
||||
},
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
} else {
|
||||
|
||||
+4
-2
@@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.model.*
|
||||
@@ -14,14 +15,15 @@ import chat.simplex.app.model.*
|
||||
fun CIChatFeatureView(
|
||||
chatItem: ChatItem,
|
||||
feature: Feature,
|
||||
iconColor: Color
|
||||
iconColor: Color,
|
||||
icon: ImageVector? = null
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(feature.iconFilled, feature.text, Modifier.size(15.dp), tint = iconColor)
|
||||
Icon(icon ?: feature.iconFilled, feature.text, Modifier.size(18.dp), tint = iconColor)
|
||||
Text(
|
||||
chatEventText(chatItem),
|
||||
Modifier,
|
||||
|
||||
@@ -32,7 +32,7 @@ fun CIEventView(ci: ChatItem) {
|
||||
if (memberDisplayName != null) {
|
||||
chatEventTextView(
|
||||
buildAnnotatedString {
|
||||
withChatEventStyle(this, memberDisplayName)
|
||||
withStyle(chatEventStyle) { append(memberDisplayName) }
|
||||
append(" ")
|
||||
}.plus(chatEventText(ci))
|
||||
)
|
||||
@@ -43,15 +43,11 @@ fun CIEventView(ci: ChatItem) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun withChatEventStyle(builder: AnnotatedString.Builder, text: String) {
|
||||
return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) }
|
||||
}
|
||||
val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)
|
||||
|
||||
fun chatEventText(ci: ChatItem): AnnotatedString =
|
||||
buildAnnotatedString {
|
||||
withChatEventStyle(this, ci.content.text)
|
||||
append(" ")
|
||||
withChatEventStyle(this, ci.timestampText)
|
||||
withStyle(chatEventStyle) { append(ci.content.text + " " + ci.timestampText) }
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
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.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun CIFeaturePreferenceView(
|
||||
chatItem: ChatItem,
|
||||
contact: Contact?,
|
||||
feature: ChatFeature,
|
||||
allowed: FeatureAllowed,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Icon(feature.icon, feature.text, Modifier.size(18.dp), tint = HighOrLowlight)
|
||||
if (contact != null && allowed != FeatureAllowed.NO && contact.allowsFeature(feature) && !contact.userAllowsFeature(feature)) {
|
||||
val acceptStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
|
||||
val setParam = feature == ChatFeature.TimedMessages && contact.mergedPreferences.timedMessages.userPreference.pref.ttl == null
|
||||
val acceptTextId = if (setParam) R.string.accept_feature_set_1_day else R.string.accept_feature
|
||||
val param = if (setParam) 86400 else null
|
||||
val annotatedText = buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(chatItem.content.text + " ") }
|
||||
withAnnotation(tag = "Accept", annotation = "Accept") {
|
||||
withStyle(acceptStyle) { append(generalGetString(acceptTextId) + " ") }
|
||||
}
|
||||
withStyle(chatEventStyle) { append(chatItem.timestampText) }
|
||||
}
|
||||
fun accept(offset: Int): Boolean = annotatedText.getStringAnnotations(tag = "Accept", start = offset, end = offset).isNotEmpty()
|
||||
ClickableText(
|
||||
annotatedText,
|
||||
onClick = { if (accept(it)) { acceptFeature(contact, feature, param) } },
|
||||
shouldConsumeEvent = ::accept
|
||||
)
|
||||
} else {
|
||||
Text(chatItem.content.text + " " + chatItem.timestampText,
|
||||
fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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 chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
|
||||
@Composable
|
||||
fun CIInvalidJSONView(json: String) {
|
||||
Row(Modifier
|
||||
.clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } }
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InvalidJSONView(json: String) {
|
||||
Column {
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
SectionView {
|
||||
val context = LocalContext.current
|
||||
SettingsActionItem(Icons.Outlined.Share, generalGetString(R.string.share_verb), click = {
|
||||
shareText(context, json)
|
||||
})
|
||||
}
|
||||
Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) {
|
||||
Text(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,62 +3,85 @@ package chat.simplex.app.views.chat.item
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.material.icons.outlined.Edit
|
||||
import androidx.compose.material.icons.outlined.Timer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.WarningYellow
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun CIMetaView(chatItem: ChatItem, metaColor: Color = HighOrLowlight) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (!chatItem.isDeletedContent) {
|
||||
if (chatItem.meta.itemEdited) {
|
||||
Icon(
|
||||
Icons.Filled.Edit,
|
||||
modifier = Modifier.height(12.dp).padding(end = 1.dp),
|
||||
contentDescription = stringResource(R.string.icon_descr_edited),
|
||||
tint = metaColor,
|
||||
)
|
||||
}
|
||||
CIStatusView(chatItem.meta.itemStatus, metaColor)
|
||||
fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = HighOrLowlight) {
|
||||
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatItem.isDeletedContent) {
|
||||
Text(
|
||||
chatItem.timestampText,
|
||||
color = metaColor,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
} else {
|
||||
CIMetaText(chatItem.meta, timedMessagesTTL, metaColor)
|
||||
}
|
||||
Text(
|
||||
chatItem.timestampText,
|
||||
color = metaColor,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(start = 3.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
// changing this function requires updating reserveSpaceForMeta
|
||||
private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
|
||||
if (meta.itemEdited) {
|
||||
StatusIconText(Icons.Outlined.Edit, color)
|
||||
Spacer(Modifier.width(3.dp))
|
||||
}
|
||||
if (meta.disappearing) {
|
||||
StatusIconText(Icons.Outlined.Timer, color)
|
||||
val ttl = meta.itemTimed?.ttl
|
||||
if (ttl != chatTTL) {
|
||||
Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color)
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
StatusIconText(icon, statusColor)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
} else if (!meta.disappearing) {
|
||||
StatusIconText(Icons.Filled.Circle, Color.Transparent)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
Text(meta.timestampText, color = color, fontSize = 13.sp)
|
||||
}
|
||||
|
||||
// the conditions in this function should match CIMetaText
|
||||
fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
|
||||
val iconSpace = " "
|
||||
var res = ""
|
||||
if (meta.itemEdited) res += iconSpace
|
||||
if (meta.itemTimed != null) {
|
||||
res += iconSpace
|
||||
val ttl = meta.itemTimed.ttl
|
||||
if (ttl != chatTTL) {
|
||||
res += TimedMessagesPreference.shortTtlText(ttl)
|
||||
}
|
||||
}
|
||||
if (meta.statusIcon(HighOrLowlight) != null || !meta.disappearing) {
|
||||
res += iconSpace
|
||||
}
|
||||
return res + meta.timestampText
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CIStatusView(status: CIStatus, metaColor: Color = HighOrLowlight) {
|
||||
when (status) {
|
||||
is CIStatus.SndSent -> {
|
||||
Icon(Icons.Filled.Check, stringResource(R.string.icon_descr_sent_msg_status_sent), Modifier.height(12.dp), tint = metaColor)
|
||||
}
|
||||
is CIStatus.SndErrorAuth -> {
|
||||
Icon(Icons.Filled.Close, stringResource(R.string.icon_descr_sent_msg_status_unauthorized_send), Modifier.height(12.dp), tint = Color.Red)
|
||||
}
|
||||
is CIStatus.SndError -> {
|
||||
Icon(Icons.Filled.WarningAmber, stringResource(R.string.icon_descr_sent_msg_status_send_failed), Modifier.height(12.dp), tint = WarningYellow)
|
||||
}
|
||||
is CIStatus.RcvNew -> {
|
||||
Icon(Icons.Filled.Circle, stringResource(R.string.icon_descr_received_msg_status_unread), Modifier.height(12.dp), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
private fun StatusIconText(icon: ImageVector, color: Color) {
|
||||
Icon(icon, null, Modifier.height(12.dp), tint = color)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@@ -67,7 +90,8 @@ fun PreviewCIMetaView() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -78,7 +102,8 @@ fun PreviewCIMetaViewUnread() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
status = CIStatus.RcvNew()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -89,7 +114,8 @@ fun PreviewCIMetaViewSendFailed() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
status = CIStatus.SndError("CMD SYNTAX")
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,7 +125,8 @@ fun PreviewCIMetaViewSendNoAuth() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,7 +136,8 @@ fun PreviewCIMetaViewSendSent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,7 +148,8 @@ fun PreviewCIMetaViewEdited() {
|
||||
chatItem = ChatItem.getSampleData(
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,7 +161,8 @@ fun PreviewCIMetaViewEditedUnread() {
|
||||
1, CIDirection.DirectRcv(), Clock.System.now(), "hello",
|
||||
itemEdited = true,
|
||||
status=CIStatus.RcvNew()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,7 +174,8 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello",
|
||||
itemEdited = true,
|
||||
status=CIStatus.SndSent()
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -152,6 +183,7 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
@Composable
|
||||
fun PreviewCIMetaViewDeletedContent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getDeletedContentSampleData()
|
||||
chatItem = ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ fun CIVoiceView(
|
||||
sent: Boolean,
|
||||
hasText: Boolean,
|
||||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
longClick: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
@@ -63,7 +64,7 @@ fun CIVoiceView(
|
||||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, play, pause, longClick)
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick)
|
||||
} else {
|
||||
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
|
||||
val metaReserve = if (edited)
|
||||
@@ -86,6 +87,7 @@ private fun VoiceLayout(
|
||||
brokenAudio: Boolean,
|
||||
sent: Boolean,
|
||||
hasText: Boolean,
|
||||
timedMessagesTTL: Int?,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
longClick: () -> Unit
|
||||
@@ -105,7 +107,7 @@ private fun VoiceLayout(
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +117,7 @@ private fun VoiceLayout(
|
||||
Column {
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
|
||||
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.chat.ComposeContextItem
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
@@ -42,6 +43,7 @@ fun ChatItemView(
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -49,9 +51,10 @@ fun ChatItemView(
|
||||
val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val revealed = remember { mutableStateOf(false) }
|
||||
val fullDeleteAllowed = remember(cInfo) { cInfo.fullDeletionAllowed }
|
||||
val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) }
|
||||
val saveFileLauncher = rememberSaveFileLauncher(cxt = context, ciFile = cItem.file)
|
||||
val onLinkLongClick = { _: String -> showMenu.value = true }
|
||||
val live = composeState.value.liveMessage != null
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -95,7 +98,7 @@ fun ChatItemView(
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
if (!cItem.meta.itemDeleted) {
|
||||
if (!cItem.meta.itemDeleted && !live) {
|
||||
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
@@ -131,7 +134,7 @@ fun ChatItemView(
|
||||
})
|
||||
}
|
||||
}
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice) {
|
||||
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
|
||||
ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = {
|
||||
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
|
||||
showMenu.value = false
|
||||
@@ -147,7 +150,9 @@ fun ChatItemView(
|
||||
}
|
||||
)
|
||||
}
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,14 +179,14 @@ fun ChatItemView(
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted && !revealed.value) {
|
||||
MarkedDeletedItemView(cItem, showMember = showMember)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else if (cItem.quotedItem == null && !cItem.meta.itemDeleted) {
|
||||
} else if (cItem.quotedItem == null && !cItem.meta.itemDeleted && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem)
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL)
|
||||
MsgContentItemDropdownMenu()
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, longClick = { onLinkLongClick("") })
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
|
||||
MsgContentItemDropdownMenu()
|
||||
} else {
|
||||
framedItemView()
|
||||
@@ -194,7 +199,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, showMember = showMember)
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
@@ -215,7 +220,7 @@ fun ChatItemView(
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
@@ -224,10 +229,16 @@ fun ChatItemView(
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
|
||||
is CIContent.RcvChatPreference -> {
|
||||
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
|
||||
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
|
||||
}
|
||||
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, HighOrLowlight, icon = c.feature.icon,)
|
||||
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
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.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,6 +330,7 @@ fun PreviewChatItemView() {
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -338,6 +350,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = ReceivedColorLight,
|
||||
@@ -35,7 +35,7 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,8 @@ fun DeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun PreviewDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getDeletedContentSampleData()
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp)
|
||||
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp)
|
||||
|
||||
@Composable
|
||||
fun EmojiItemView(chatItem: ChatItem) {
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) {
|
||||
Column(
|
||||
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(chatItem.content.text)
|
||||
CIMetaView(chatItem)
|
||||
CIMetaView(chatItem, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -27,6 +28,7 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.min
|
||||
|
||||
val SentColorLight = Color(0x1E45B8FF)
|
||||
val ReceivedColorLight = Color(0x20B1B0B5)
|
||||
@@ -47,6 +49,7 @@ fun FramedItemView(
|
||||
scrollToItem: (Long) -> Unit = {},
|
||||
) {
|
||||
val sent = ci.chatDir.sent
|
||||
val chatTTL = chatInfo.timedMessagesTTL
|
||||
|
||||
fun membership(): GroupMember? {
|
||||
return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null
|
||||
@@ -67,7 +70,7 @@ fun FramedItemView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciDeletedView() {
|
||||
fun FramedItemHeader(caption: String, italic: Boolean, icon: ImageVector? = null) {
|
||||
Row(
|
||||
Modifier
|
||||
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight)
|
||||
@@ -78,15 +81,19 @@ fun FramedItemView(
|
||||
.padding(bottom = if (ci.quotedItem == null) 6.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Delete,
|
||||
stringResource(R.string.marked_deleted_description),
|
||||
Modifier.size(18.dp),
|
||||
tint = if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
icon,
|
||||
caption,
|
||||
Modifier.size(18.dp),
|
||||
tint = if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
}
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = HighOrLowlight)) { append(generalGetString(R.string.marked_deleted_description)) }
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = HighOrLowlight)) {
|
||||
append(caption)
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
)
|
||||
@@ -138,12 +145,12 @@ fun FramedItemView(
|
||||
@Composable
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
|
||||
if (text != "") {
|
||||
CIMarkdownText(ci, showMember, linkMode = linkMode, uriHandler)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
|
||||
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && ci.content.text.isEmpty() && ci.quotedItem == null
|
||||
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
|
||||
|
||||
Box(Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
@@ -158,9 +165,13 @@ fun FramedItemView(
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
Column(Modifier.width(IntrinsicSize.Max)) {
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
if (ci.meta.itemDeleted) { ciDeletedView() }
|
||||
if (ci.meta.itemDeleted) {
|
||||
FramedItemHeader(stringResource(R.string.marked_deleted_description), true, Icons.Outlined.Delete)
|
||||
} else if (ci.meta.isLive) {
|
||||
FramedItemHeader(stringResource(R.string.live), false)
|
||||
}
|
||||
ci.quotedItem?.let { ciQuoteView(it) }
|
||||
if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) {
|
||||
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -176,36 +187,36 @@ fun FramedItemView(
|
||||
when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
|
||||
if (mc.text == "") {
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, longClick = { onLinkLongClick("") })
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> ciFileView(ci, mc.text)
|
||||
is MsgContent.MCUnknown ->
|
||||
if (ci.file == null) {
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
} else {
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview)
|
||||
CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
}
|
||||
else -> CIMarkdownText(ci, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
|
||||
CIMetaView(ci, metaColor)
|
||||
CIMetaView(ci, chatTTL, metaColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,21 +225,24 @@ fun FramedItemView(
|
||||
@Composable
|
||||
fun CIMarkdownText(
|
||||
ci: ChatItem,
|
||||
chatTTL: Int?,
|
||||
showMember: Boolean,
|
||||
linkMode: SimplexLinkMode,
|
||||
uriHandler: UriHandler?,
|
||||
onLinkLongClick: (link: String) -> Unit = {}
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
MarkdownText(
|
||||
ci.content.text, ci.formattedText, if (showMember) ci.memberDisplayName else null,
|
||||
metaText = ci.timestampText, edited = ci.meta.itemEdited, linkMode = linkMode,
|
||||
text, if (text.isEmpty()) emptyList() else ci.formattedText, if (showMember) ci.memberDisplayName else null,
|
||||
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
const val MAX_SAFE_WIDTH_HEIGHT = 100_000
|
||||
|
||||
@Composable
|
||||
fun PriorityLayout(
|
||||
@@ -247,9 +261,15 @@ fun PriorityLayout(
|
||||
if (it.layoutId == priorityLayoutId)
|
||||
imagePlaceable!!
|
||||
else
|
||||
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: constraints.maxWidth)) }
|
||||
// Limit width for every other element to width of important element and height for a sum of all elements
|
||||
layout(imagePlaceable?.measuredWidth ?: placeables.maxOf { it.width }, placeables.sumOf { it.height }) {
|
||||
it.measure(constraints.copy(maxWidth = imagePlaceable?.width ?: min(MAX_SAFE_WIDTH_HEIGHT, constraints.maxWidth))) }
|
||||
/**
|
||||
* Limit width for every other element to width of important element and height for a sum of all elements.
|
||||
*
|
||||
* min(MAX_SAFE_WIDTH_HEIGHT, ...) is here because of exception (related to width of long text):
|
||||
* java.lang.IllegalArgumentException: Can't represent a size of 324314 in Constraints
|
||||
* at androidx.compose.ui.unit.Constraints$Companion.bitsNeedForSize(Constraints.kt:403)
|
||||
* */
|
||||
layout(imagePlaceable?.measuredWidth ?: min(MAX_SAFE_WIDTH_HEIGHT, placeables.maxOf { it.width }), min(MAX_SAFE_WIDTH_HEIGHT, placeables.sumOf { it.height })) {
|
||||
var y = 0
|
||||
placeables.forEach {
|
||||
it.place(0, y)
|
||||
|
||||
+4
-3
@@ -22,7 +22,7 @@ import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
Modifier.clickable(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -45,7 +45,7 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,8 @@ fun IntegrityErrorItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun IntegrityErrorItemViewPreview() {
|
||||
SimpleXTheme {
|
||||
IntegrityErrorItemView(
|
||||
ChatItem.getDeletedContentSampleData()
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -20,7 +20,7 @@ import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (ci.chatDir.sent) SentColorLight else ReceivedColorLight,
|
||||
@@ -37,7 +37,7 @@ fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci)
|
||||
CIMetaView(ci, timedMessagesTTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ fun MarkedDeletedItemView(ci: ChatItem, showMember: Boolean = false) {
|
||||
fun PreviewMarkedDeletedItemView() {
|
||||
SimpleXTheme {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = true)
|
||||
ChatItem.getSampleData(itemDeleted = true),
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.util.Log
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
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.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -18,7 +22,9 @@ import androidx.compose.ui.unit.*
|
||||
import androidx.core.text.BidiFormatter
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.helpers.detectGesture
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
val reserveTimestampStyle = SpanStyle(color = Color.Transparent)
|
||||
val boldFont = SpanStyle(fontWeight = FontWeight.Medium)
|
||||
@@ -40,13 +46,31 @@ fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolea
|
||||
}
|
||||
}
|
||||
|
||||
private val noTyping: AnnotatedString = AnnotatedString(" ")
|
||||
|
||||
private val typingIndicators: List<AnnotatedString> = listOf(
|
||||
typing(FontWeight.Black) + typing() + typing(),
|
||||
typing(FontWeight.Bold) + typing(FontWeight.Black) + typing(),
|
||||
typing() + typing(FontWeight.Bold) + typing(FontWeight.Black),
|
||||
typing() + typing() + typing(FontWeight.Bold)
|
||||
)
|
||||
|
||||
|
||||
private fun typingIndicator(recent: Boolean, @IntRange (from = 0, to = 4) typingIdx: Int): AnnotatedString = buildAnnotatedString {
|
||||
pushStyle(SpanStyle(color = HighOrLowlight, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp))
|
||||
append(if (recent) typingIndicators[typingIdx] else noTyping)
|
||||
}
|
||||
|
||||
private fun typing(w: FontWeight = FontWeight.Light): AnnotatedString =
|
||||
AnnotatedString(".", SpanStyle(fontWeight = w))
|
||||
|
||||
@Composable
|
||||
fun MarkdownText (
|
||||
text: String,
|
||||
text: CharSequence,
|
||||
formattedText: List<FormattedText>? = null,
|
||||
sender: String? = null,
|
||||
metaText: String? = null,
|
||||
edited: Boolean = false,
|
||||
meta: CIMeta? = null,
|
||||
chatTTL: Int? = null,
|
||||
style: TextStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onSurface, lineHeight = 22.sp),
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
@@ -54,29 +78,69 @@ fun MarkdownText (
|
||||
senderBold: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
linkMode: SimplexLinkMode,
|
||||
inlineContent: Map<String, InlineTextContent>? = null,
|
||||
onLinkLongClick: (link: String) -> Unit = {}
|
||||
) {
|
||||
val textLayoutDirection = remember (text) {
|
||||
if (BidiFormatter.getInstance().isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
}
|
||||
val reserve = when {
|
||||
textLayoutDirection != LocalLayoutDirection.current && metaText != null -> "\n"
|
||||
edited -> " "
|
||||
else -> " "
|
||||
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
|
||||
"\n"
|
||||
} else if (meta != null) {
|
||||
reserveSpaceForMeta(meta, chatTTL)
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides if (textLayoutDirection != LocalLayoutDirection.current)
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Ltr) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
else
|
||||
LocalLayoutDirection.current
|
||||
) {
|
||||
var timer: Job? by remember { mutableStateOf(null) }
|
||||
var typingIdx by rememberSaveable { mutableStateOf(0) }
|
||||
fun stopTyping() {
|
||||
timer?.cancel()
|
||||
timer = null
|
||||
}
|
||||
fun switchTyping() {
|
||||
if (meta != null && meta.isLive && meta.recent) {
|
||||
timer = timer ?: scope.launch {
|
||||
while (isActive) {
|
||||
typingIdx = (typingIdx + 1) % typingIndicators.size
|
||||
delay(250)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopTyping()
|
||||
}
|
||||
}
|
||||
if (meta?.isLive == true) {
|
||||
val activity = LocalContext.current as Activity
|
||||
LaunchedEffect(meta.recent, meta.isLive) {
|
||||
switchTyping()
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
val orientation = activity.resources.configuration.orientation
|
||||
onDispose {
|
||||
if (orientation == activity.resources.configuration.orientation) {
|
||||
stopTyping()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (formattedText == null) {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
appendSender(this, sender, senderBold)
|
||||
append(text)
|
||||
if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
|
||||
if (text is String) append(text)
|
||||
else if (text is AnnotatedString) append(text)
|
||||
if (meta?.isLive == true) {
|
||||
append(typingIndicator(meta.recent, typingIdx))
|
||||
}
|
||||
if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
|
||||
}
|
||||
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow)
|
||||
Text(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, inlineContent = inlineContent ?: mapOf())
|
||||
} else {
|
||||
var hasLinks = false
|
||||
val annotatedText = buildAnnotatedString {
|
||||
@@ -100,10 +164,13 @@ fun MarkdownText (
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meta?.isLive == true) {
|
||||
append(typingIndicator(meta.recent, typingIdx))
|
||||
}
|
||||
// With RTL language set globally links looks bad sometimes, better to add a new line to bo sure everything looks good
|
||||
/*if (metaText != null && hasLinks && LocalLayoutDirection.current == LayoutDirection.Rtl)
|
||||
withStyle(reserveTimestampStyle) { append("\n" + metaText) }
|
||||
else */if (metaText != null) withStyle(reserveTimestampStyle) { append(reserve + metaText) }
|
||||
else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) }
|
||||
}
|
||||
if (hasLinks && uriHandler != null) {
|
||||
ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow,
|
||||
|
||||
@@ -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.usersettings.MarkdownHelpView
|
||||
import chat.simplex.app.views.usersettings.simplexTeamUri
|
||||
|
||||
val bold = SpanStyle(fontWeight = FontWeight.Bold)
|
||||
@@ -76,6 +77,15 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
|
||||
Text(annotatedStringResource(R.string.desktop_scan_QR_code_from_app_via_scan_QR_code), lineHeight = 22.sp)
|
||||
Text(annotatedStringResource(R.string.mobile_tap_open_in_mobile_app_then_tap_connect_in_app), lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(vertical = 24.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.markdown_in_messages), style = MaterialTheme.typography.h2)
|
||||
MarkdownHelpView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-8
@@ -11,15 +11,20 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
import chat.simplex.app.views.chat.group.deleteGroupDialog
|
||||
import chat.simplex.app.views.chat.group.leaveGroupDialog
|
||||
import chat.simplex.app.views.chat.item.InvalidJSONView
|
||||
import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.ContactConnectionInfoView
|
||||
@@ -39,17 +44,19 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
delay(500L)
|
||||
}
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
is ChatInfo.Direct -> {
|
||||
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) },
|
||||
click = { directChatAction(chat.chatInfo, chatModel) },
|
||||
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
stopped
|
||||
)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, stopped, linkMode) },
|
||||
chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) },
|
||||
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
||||
dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) },
|
||||
showMenu,
|
||||
@@ -75,6 +82,18 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
showMenu,
|
||||
stopped
|
||||
)
|
||||
is ChatInfo.InvalidJSON ->
|
||||
ChatListNavLinkLayout(
|
||||
chatLinkPreview = {
|
||||
InvalidDataView()
|
||||
},
|
||||
click = {
|
||||
ModalManager.shared.showModal(true) { InvalidJSONView(chat.chatInfo.json) }
|
||||
},
|
||||
dropdownMenuItems = null,
|
||||
showMenu,
|
||||
stopped
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +301,7 @@ fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatMo
|
||||
if (chatModel.incognito.value) Icons.Filled.TheaterComedy else Icons.Outlined.Check,
|
||||
color = if (chatModel.incognito.value) Indigo else MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
acceptContactRequest(chatInfo, chatModel)
|
||||
acceptContactRequest(chatInfo.apiId, chatInfo, true, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
)
|
||||
@@ -320,6 +339,29 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InvalidDataView() {
|
||||
Row {
|
||||
ProfileImage(72.dp, null, Icons.Filled.AccountCircle, HighOrLowlight)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp)
|
||||
.weight(1F)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.invalid_data),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h3,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Red
|
||||
)
|
||||
val height = with(LocalDensity.current) { 46.sp.toDp() }
|
||||
Spacer(Modifier.height(height))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
var chat = c
|
||||
withApi {
|
||||
@@ -367,16 +409,16 @@ fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel
|
||||
title = generalGetString(R.string.accept_connection_request__question),
|
||||
text = generalGetString(R.string.if_you_choose_to_reject_the_sender_will_not_be_notified),
|
||||
confirmText = if (chatModel.incognito.value) generalGetString(R.string.accept_contact_incognito_button) else generalGetString(R.string.accept_contact_button),
|
||||
onConfirm = { acceptContactRequest(contactRequest, chatModel) },
|
||||
onConfirm = { acceptContactRequest(contactRequest.apiId, contactRequest, true, chatModel) },
|
||||
dismissText = generalGetString(R.string.reject_contact_button),
|
||||
onDismiss = { rejectContactRequest(contactRequest, chatModel) }
|
||||
)
|
||||
}
|
||||
|
||||
fun acceptContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
|
||||
fun acceptContactRequest(apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) {
|
||||
withApi {
|
||||
val contact = chatModel.controller.apiAcceptContactRequest(contactRequest.apiId)
|
||||
if (contact != null) {
|
||||
val contact = chatModel.controller.apiAcceptContactRequest(apiId)
|
||||
if (contact != null && isCurrentUser && contactRequest != null) {
|
||||
val chat = Chat(ChatInfo.Direct(contact), listOf())
|
||||
chatModel.replaceChat(contactRequest.id, chat)
|
||||
}
|
||||
@@ -585,8 +627,11 @@ fun PreviewChatListNavLinkDirect() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
stopped = false,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION
|
||||
)
|
||||
@@ -623,8 +668,11 @@ fun PreviewChatListNavLinkGroup() {
|
||||
),
|
||||
chatStats = Chat.ChatStats()
|
||||
),
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
stopped = false,
|
||||
linkMode = SimplexLinkMode.DESCRIPTION
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -13,41 +14,60 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.NewChatSheet
|
||||
import chat.simplex.app.views.onboarding.WhatsNewView
|
||||
import chat.simplex.app.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.app.views.usersettings.SettingsView
|
||||
import chat.simplex.app.views.usersettings.simplexTeamUri
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
val newChatSheetState by rememberSaveable(stateSaver = NewChatSheetState.saver()) { mutableStateOf(MutableStateFlow(NewChatSheetState.GONE)) }
|
||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
newChatSheetState.value = NewChatSheetState.VISIBLE
|
||||
newChatSheetState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
val hideNewChatSheet: (animated: Boolean) -> Unit = { animated ->
|
||||
if (animated) newChatSheetState.value = NewChatSheetState.HIDING
|
||||
else newChatSheetState.value = NewChatSheetState.GONE
|
||||
if (animated) newChatSheetState.value = AnimatedViewState.HIDING
|
||||
else newChatSheetState.value = AnimatedViewState.GONE
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (shouldShowWhatsNew(chatModel)) {
|
||||
delay(1000L)
|
||||
ModalManager.shared.showCustomModal { close -> WhatsNewView(close = close) }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(chatModel.clearOverlays.value) {
|
||||
if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false)
|
||||
}
|
||||
LaunchedEffect(chatModel.appOpenUrl.value) {
|
||||
val url = chatModel.appOpenUrl.value
|
||||
if (url != null) {
|
||||
chatModel.appOpenUrl.value = null
|
||||
connectIfOpenedViaUri(url, chatModel)
|
||||
}
|
||||
}
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val switchingUsers = rememberSaveable { mutableStateOf(false) }
|
||||
Scaffold(
|
||||
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, stopped) { searchInList = it.trim() } },
|
||||
topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } },
|
||||
scaffoldState = scaffoldState,
|
||||
drawerContent = { SettingsView(chatModel, setPerformLA) },
|
||||
floatingActionButton = {
|
||||
@@ -80,7 +100,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
) {
|
||||
if (chatModel.chats.isNotEmpty()) {
|
||||
ChatList(chatModel, search = searchInList)
|
||||
} else {
|
||||
} else if (!switchingUsers.value) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
if (!stopped && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
OnboardingButtons(showNewChatSheet)
|
||||
@@ -94,6 +114,17 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped:
|
||||
if (searchInList.isEmpty()) {
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
UserPicker(chatModel, userPickerState, switchingUsers) {
|
||||
scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() }
|
||||
}
|
||||
if (switchingUsers.value) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().clickable(enabled = false, onClick = {}),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -139,7 +170,7 @@ private fun ConnectButton(text: String, onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
|
||||
if (showSearch) {
|
||||
@@ -172,10 +203,23 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
|
||||
val scope = rememberCoroutineScope()
|
||||
DefaultTopAppBar(
|
||||
navigationButton = {
|
||||
if (showSearch)
|
||||
if (showSearch) {
|
||||
NavigationButtonBack(hideSearchOnBack)
|
||||
else
|
||||
} else if (chatModel.users.isEmpty()) {
|
||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.toList() } }
|
||||
val allRead = users
|
||||
.filter { !it.user.activeUser }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
if (users.size == 1) {
|
||||
scope.launch { drawerState.open() }
|
||||
} else {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -202,6 +246,47 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
Box {
|
||||
ProfileImage(
|
||||
image = image,
|
||||
size = 37.dp
|
||||
)
|
||||
if (!allRead) {
|
||||
unreadBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.unreadBadge(text: String? = "") {
|
||||
Text(
|
||||
text ?: "",
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 6.sp,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.badgeLayout()
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
.size(30.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 2.5.dp
|
||||
)
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -4,17 +4,20 @@ import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.foundation.text.appendInlineContent
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material.icons.filled.NotificationsOff
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -22,11 +25,22 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.ComposePreview
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
import chat.simplex.app.views.chat.item.MarkdownText
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, stopped: Boolean, linkMode: SimplexLinkMode) {
|
||||
fun ChatPreviewView(
|
||||
chat: Chat,
|
||||
chatModelDraft: ComposeState?,
|
||||
chatModelDraftChatId: ChatId?,
|
||||
chatModelIncognito: Boolean,
|
||||
currentUserProfileDisplayName: String?,
|
||||
contactNetworkStatus: NetworkStatus?,
|
||||
stopped: Boolean,
|
||||
linkMode: SimplexLinkMode
|
||||
) {
|
||||
val cInfo = chat.chatInfo
|
||||
|
||||
@Composable
|
||||
@@ -63,11 +77,58 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerifiedIcon() {
|
||||
Icon(Icons.Outlined.VerifiedUser, null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = HighOrLowlight)
|
||||
}
|
||||
|
||||
fun messageDraft(draft: ComposeState): Pair<AnnotatedString, Map<String, InlineTextContent>> {
|
||||
fun attachment(): Pair<ImageVector, String?>? =
|
||||
when (draft.preview) {
|
||||
is ComposePreview.FilePreview -> Icons.Filled.InsertDriveFile to draft.preview.fileName
|
||||
is ComposePreview.ImagePreview -> Icons.Outlined.Image to null
|
||||
is ComposePreview.VoicePreview -> Icons.Filled.PlayArrow to durationText(draft.preview.durationMs / 1000)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val attachment = attachment()
|
||||
val text = buildAnnotatedString {
|
||||
appendInlineContent(id = "editIcon")
|
||||
append(" ")
|
||||
if (attachment != null) {
|
||||
appendInlineContent(id = "attachmentIcon")
|
||||
if (attachment.second != null) {
|
||||
append(attachment.second as String)
|
||||
}
|
||||
append(" ")
|
||||
}
|
||||
append(draft.message)
|
||||
}
|
||||
val inlineContent: Map<String, InlineTextContent> = mapOf(
|
||||
"editIcon" to InlineTextContent(
|
||||
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Icon(Icons.Outlined.EditNote, null, tint = MaterialTheme.colors.primary)
|
||||
},
|
||||
"attachmentIcon" to InlineTextContent(
|
||||
Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter)
|
||||
) {
|
||||
Icon(attachment?.first ?: Icons.Outlined.EditNote, null, tint = HighOrLowlight)
|
||||
}
|
||||
)
|
||||
return text to inlineContent
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun chatPreviewTitle() {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (cInfo.contact.verified) {
|
||||
VerifiedIcon()
|
||||
}
|
||||
chatPreviewTitleText(if (cInfo.ready) Color.Unspecified else HighOrLowlight)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
when (cInfo.groupInfo.membership.memberStatus) {
|
||||
GroupMemberStatus.MemInvited -> chatPreviewTitleText(if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.primary)
|
||||
@@ -82,16 +143,30 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
fun chatPreviewText(chatModelIncognito: Boolean) {
|
||||
val ci = chat.chatItems.lastOrNull()
|
||||
if (ci != null) {
|
||||
val (text: CharSequence, inlineTextContent) = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) }
|
||||
!ci.meta.itemDeleted -> ci.text to null
|
||||
else -> generalGetString(R.string.marked_deleted_description) to null
|
||||
}
|
||||
val formattedText = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
!ci.meta.itemDeleted -> ci.formattedText
|
||||
else -> null
|
||||
}
|
||||
MarkdownText(
|
||||
if (!ci.meta.itemDeleted) ci.text else generalGetString(R.string.marked_deleted_description),
|
||||
if (!ci.meta.itemDeleted) ci.formattedText else null,
|
||||
sender = if (cInfo is ChatInfo.Group && !ci.chatDir.sent) ci.memberDisplayName else null,
|
||||
text,
|
||||
formattedText,
|
||||
sender = when {
|
||||
chatModelDraftChatId == chat.id && chatModelDraft != null -> null
|
||||
cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName
|
||||
else -> null
|
||||
},
|
||||
linkMode = linkMode,
|
||||
senderBold = true,
|
||||
metaText = null,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp),
|
||||
inlineContent = inlineTextContent,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
@@ -179,7 +254,7 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
Modifier.padding(top = 52.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ChatStatusImage(chat)
|
||||
ChatStatusImage(contactNetworkStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,10 +277,9 @@ fun unreadCountStr(n: Int): String {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatStatusImage(chat: Chat) {
|
||||
val s = chat.serverInfo.networkStatus
|
||||
val descr = s.statusString
|
||||
if (s is Chat.NetworkStatus.Error) {
|
||||
fun ChatStatusImage(s: NetworkStatus?) {
|
||||
val descr = s?.statusString
|
||||
if (s is NetworkStatus.Error) {
|
||||
Icon(
|
||||
Icons.Outlined.ErrorOutline,
|
||||
contentDescription = descr,
|
||||
@@ -213,7 +287,7 @@ fun ChatStatusImage(chat: Chat) {
|
||||
modifier = Modifier
|
||||
.size(19.dp)
|
||||
)
|
||||
} else if (s !is Chat.NetworkStatus.Connected) {
|
||||
} else if (s !is NetworkStatus.Connected) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.padding(horizontal = 2.dp)
|
||||
@@ -233,6 +307,6 @@ fun ChatStatusImage(chat: Chat) {
|
||||
@Composable
|
||||
fun PreviewChatPreviewView() {
|
||||
SimpleXTheme {
|
||||
ChatPreviewView(Chat.sampleData, false, "", stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
|
||||
ChatPreviewView(Chat.sampleData, null, null, false, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) },
|
||||
stopped
|
||||
)
|
||||
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection -> {}
|
||||
is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import SectionItemViewSpaceBetween
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, openSettings: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
val users by remember { derivedStateOf { chatModel.users.sortedByDescending { it.user.activeUser } } }
|
||||
val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
userPickerState.collect {
|
||||
newChat = it
|
||||
launch {
|
||||
animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec())
|
||||
if (newChat.isHiding()) userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { newChat.isVisible() }
|
||||
.distinctUntilChanged()
|
||||
.filter { it }
|
||||
.collect {
|
||||
try {
|
||||
val updatedUsers = chatModel.controller.listUsers().sortedByDescending { it.user.activeUser }
|
||||
var same = users.size == updatedUsers.size
|
||||
if (same) {
|
||||
for (i in 0 until minOf(users.size, updatedUsers.size)) {
|
||||
val prev = updatedUsers[i].user
|
||||
val next = users[i].user
|
||||
if (prev.userId != next.userId || prev.activeUser != next.activeUser || prev.chatViewName != next.chatViewName || prev.image != next.image) {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!same) {
|
||||
chatModel.users.clear()
|
||||
chatModel.users.addAll(updatedUsers)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error updating users ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() }
|
||||
val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density }
|
||||
Box(Modifier
|
||||
.fillMaxSize()
|
||||
.offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else xOffset, 0) }
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING })
|
||||
.padding(bottom = 10.dp, top = 10.dp)
|
||||
.graphicsLayer {
|
||||
alpha = animatedFloat.value
|
||||
translationY = (animatedFloat.value - 1) * xOffset
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.widthIn(min = 220.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
.height(IntrinsicSize.Min)
|
||||
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
|
||||
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
users.forEach { u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
if (!u.user.activeUser) {
|
||||
chatModel.chats.clear()
|
||||
scope.launch {
|
||||
val job = launch {
|
||||
delay(500)
|
||||
switchingUsers.value = true
|
||||
}
|
||||
chatModel.controller.changeActiveUser(u.user.userId)
|
||||
job.cancel()
|
||||
switchingUsers.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider(Modifier.requiredHeight(1.dp))
|
||||
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
|
||||
}
|
||||
}
|
||||
SettingsPickerItem {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = 46.dp)
|
||||
.combinedClickable(
|
||||
onClick = if (u.activeUser) openSettings else onClick,
|
||||
onLongClick = onLongClick,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (!u.activeUser) LocalIndication.current else null
|
||||
)
|
||||
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
|
||||
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
|
||||
)
|
||||
}
|
||||
if (u.activeUser) {
|
||||
Icon(Icons.Filled.Done, null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
} else if (unreadCount > 0) {
|
||||
Row {
|
||||
Text(
|
||||
unreadCountStr(unreadCount),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
.background(MaterialTheme.colors.primary, shape = CircleShape)
|
||||
.sizeIn(minWidth = 20.dp, minHeight = 20.dp)
|
||||
.padding(horizontal = 3.dp)
|
||||
.padding(vertical = 1.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
)
|
||||
Spacer(Modifier.width(2.dp))
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
|
||||
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
)
|
||||
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
@@ -87,6 +89,8 @@ fun DatabaseView(
|
||||
m.controller.appPrefs.privacyFullBackup,
|
||||
appFilesCountAndSize,
|
||||
chatItemTTL,
|
||||
m.currentUser.value,
|
||||
m.users,
|
||||
startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) },
|
||||
stopChatAlert = { stopChatAlert(m, runChat, context) },
|
||||
exportArchive = { exportArchive(context, m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) },
|
||||
@@ -136,6 +140,8 @@ fun DatabaseLayout(
|
||||
privacyFullBackup: SharedPreference<Boolean>,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
chatItemTTL: MutableState<ChatItemTTL>,
|
||||
currentUser: User?,
|
||||
users: List<UserInfo>,
|
||||
startChat: () -> Unit,
|
||||
stopChatAlert: () -> Unit,
|
||||
exportArchive: () -> Unit,
|
||||
@@ -148,10 +154,27 @@ fun DatabaseLayout(
|
||||
val operationsDisabled = !stopped || progressIndicator
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(bottom = 48.dp),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_chat_database))
|
||||
|
||||
SectionView(stringResource(R.string.messages_section_title).uppercase()) {
|
||||
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
|
||||
}
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(R.string.messages_section_description) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.run_chat_section)) {
|
||||
RunChatSetting(runChat, stopped, chatDbDeleted, startChat, stopChatAlert)
|
||||
}
|
||||
@@ -224,16 +247,14 @@ fun DatabaseLayout(
|
||||
)
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.data_section)) {
|
||||
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
|
||||
SectionDivider()
|
||||
SectionView(stringResource(R.string.files_and_media_section).uppercase()) {
|
||||
val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0
|
||||
SectionItemView(
|
||||
deleteAppFilesAndMedia,
|
||||
disabled = deleteFilesDisabled
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.delete_files_and_media),
|
||||
stringResource(if (users.size > 1) R.string.delete_files_and_media_for_all_users else R.string.delete_files_and_media_all),
|
||||
color = if (deleteFilesDisabled) HighOrLowlight else Color.Red
|
||||
)
|
||||
}
|
||||
@@ -696,6 +717,8 @@ fun PreviewDatabaseLayout() {
|
||||
privacyFullBackup = SharedPreference({ true }, {}),
|
||||
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
|
||||
chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) },
|
||||
currentUser = User.sampleData,
|
||||
users = listOf(UserInfo.sampleData),
|
||||
startChat = {},
|
||||
stopChatAlert = {},
|
||||
exportArchive = {},
|
||||
|
||||
@@ -8,11 +8,13 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
class AlertManager {
|
||||
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
|
||||
@@ -44,15 +46,25 @@ class AlertManager {
|
||||
|
||||
fun showAlertDialogButtonsColumn(
|
||||
title: String,
|
||||
text: String? = null,
|
||||
text: AnnotatedString? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
showAlert {
|
||||
Dialog(onDismissRequest = this::hideAlert) {
|
||||
Column(Modifier.background(MaterialTheme.colors.background)) {
|
||||
Text(title, Modifier.padding(DEFAULT_PADDING), fontSize = 18.sp)
|
||||
Column(Modifier.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)) {
|
||||
Text(title,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = if (text == null) DEFAULT_PADDING else DEFAULT_PADDING_HALF),
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
if (text != null) {
|
||||
Text(text)
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
buttons()
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: () -> Unit) {
|
||||
fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -20,9 +20,15 @@ fun CloseSheetBar(close: () -> Unit) {
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.width(TitleInsetWithIcon - AppBarHorizontalPadding)
|
||||
.padding(top = 4.dp), // Like in DefaultAppBar
|
||||
content = { NavigationButtonBack(close) }
|
||||
content = {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
NavigationButtonBack(close)
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +36,7 @@ fun CloseSheetBar(close: () -> Unit) {
|
||||
@Composable
|
||||
fun AppBarTitle(title: String, withPadding: Boolean = true) {
|
||||
val padding = if (withPadding)
|
||||
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING )
|
||||
PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
|
||||
else
|
||||
PaddingValues(bottom = DEFAULT_PADDING)
|
||||
Text(
|
||||
|
||||
@@ -53,6 +53,15 @@ fun NavigationButtonBack(onButtonClicked: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareButton(onButtonClicked: () -> Unit) {
|
||||
IconButton(onButtonClicked) {
|
||||
Icon(
|
||||
Icons.Outlined.Share, stringResource(R.string.share_verb), tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationButtonMenu(onButtonClicked: () -> Unit) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
@file:UseSerializers(UriSerializer::class)
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
sealed class SharedContent {
|
||||
data class Text(val text: String): SharedContent()
|
||||
@@ -11,7 +15,7 @@ sealed class SharedContent {
|
||||
data class File(val text: String, val uri: Uri): SharedContent()
|
||||
}
|
||||
|
||||
enum class NewChatSheetState {
|
||||
enum class AnimatedViewState {
|
||||
VISIBLE, HIDING, GONE;
|
||||
fun isVisible(): Boolean {
|
||||
return this == VISIBLE
|
||||
@@ -23,7 +27,7 @@ enum class NewChatSheetState {
|
||||
return this == GONE
|
||||
}
|
||||
companion object {
|
||||
fun saver(): Saver<MutableStateFlow<NewChatSheetState>, *> = Saver(
|
||||
fun saver(): Saver<MutableStateFlow<AnimatedViewState>, *> = Saver(
|
||||
save = { it.value.toString() },
|
||||
restore = {
|
||||
MutableStateFlow(valueOf(it))
|
||||
@@ -32,7 +36,16 @@ enum class NewChatSheetState {
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UploadContent {
|
||||
data class SimpleImage(val uri: Uri): UploadContent()
|
||||
data class AnimatedImage(val uri: Uri): UploadContent()
|
||||
|
||||
@Serializer(forClass = Uri::class)
|
||||
object UriSerializer : KSerializer<Uri> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
|
||||
override fun serialize(encoder: Encoder, value: Uri) = encoder.encodeString(value.toString())
|
||||
override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString())
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class UploadContent {
|
||||
@Serializable data class SimpleImage(val uri: Uri): UploadContent()
|
||||
@Serializable data class AnimatedImage(val uri: Uri): UploadContent()
|
||||
}
|
||||
|
||||
@@ -214,8 +214,8 @@ fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun interactionSourceWithTapDetection(key: Any, onPress: () -> Unit, onClick: () -> Unit, onCancel: () -> Unit, onRelease: ()-> Unit): MutableInteractionSource {
|
||||
val interactionSource = remember(key) { MutableInteractionSource() }
|
||||
fun interactionSourceWithTapDetection(onPress: () -> Unit, onClick: () -> Unit, onCancel: () -> Unit, onRelease: ()-> Unit): MutableInteractionSource {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
LaunchedEffect(interactionSource) {
|
||||
var firstTapTime = 0L
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
|
||||
@@ -20,12 +20,13 @@ fun ModalView(
|
||||
close: () -> Unit,
|
||||
background: Color = MaterialTheme.colors.background,
|
||||
modifier: Modifier = Modifier,
|
||||
endButtons: @Composable RowScope.() -> Unit = {},
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
Column(Modifier.background(background)) {
|
||||
CloseSheetBar(close)
|
||||
CloseSheetBar(close, endButtons)
|
||||
Box(modifier) { content() }
|
||||
}
|
||||
}
|
||||
@@ -37,9 +38,9 @@ class ModalManager {
|
||||
private val toRemove = mutableSetOf<Int>()
|
||||
private var oldViewChanging = AtomicBoolean(false)
|
||||
|
||||
fun showModal(settings: Boolean = false, content: @Composable () -> Unit) {
|
||||
fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
|
||||
showCustomModal { close ->
|
||||
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, content = content)
|
||||
ModalView(close, if (!settings || isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight, endButtons = endButtons, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.media.*
|
||||
import android.media.AudioManager.AudioPlaybackCallback
|
||||
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED
|
||||
import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
@@ -10,25 +12,25 @@ import androidx.compose.runtime.*
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.views.helpers.AudioPlayer.duration
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
interface Recorder {
|
||||
val recordingInProgress: MutableState<Boolean>
|
||||
fun start(onStop: () -> Unit): String
|
||||
fun stop()
|
||||
fun cancel(filePath: String, recordingInProgress: MutableState<Boolean>)
|
||||
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
|
||||
fun stop(): Int
|
||||
}
|
||||
|
||||
class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
companion object {
|
||||
// Allows to stop the recorder from outside without having the recorder in a variable
|
||||
var stopRecording: (() -> Unit)? = null
|
||||
const val extension = "m4a"
|
||||
}
|
||||
override val recordingInProgress = mutableStateOf(false)
|
||||
private var recorder: MediaRecorder? = null
|
||||
private var progressJob: Job? = null
|
||||
private var filePath: String? = null
|
||||
private var recStartedAt: Long? = null
|
||||
private fun initRecorder() =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
MediaRecorder(SimplexApp.context)
|
||||
@@ -36,9 +38,8 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
MediaRecorder()
|
||||
}
|
||||
|
||||
override fun start(onStop: () -> Unit): String {
|
||||
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
|
||||
AudioPlayer.stop()
|
||||
recordingInProgress.value = true
|
||||
val rec: MediaRecorder
|
||||
recorder = initRecorder().also { rec = it }
|
||||
rec.setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||
@@ -47,28 +48,39 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
rec.setAudioChannels(1)
|
||||
rec.setAudioSamplingRate(16000)
|
||||
rec.setAudioEncodingBitRate(16000)
|
||||
rec.setMaxDuration(-1) // TODO set limit
|
||||
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
|
||||
rec.setMaxFileSize(recordedBytesLimit)
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val filePath = getAppFilePath(SimplexApp.context, uniqueCombine(SimplexApp.context, getAppFilePath(SimplexApp.context, "voice_${timestamp}.m4a")))
|
||||
rec.setOutputFile(filePath)
|
||||
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
|
||||
val fileToSave = File.createTempFile(generateNewFileName(SimplexApp.context, "voice", "${extension}_"), ".tmp", tmpDir)
|
||||
fileToSave.deleteOnExit()
|
||||
val path = fileToSave.absolutePath
|
||||
filePath = path
|
||||
rec.setOutputFile(path)
|
||||
rec.prepare()
|
||||
rec.start()
|
||||
rec.setOnInfoListener { mr, what, extra ->
|
||||
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
|
||||
stop()
|
||||
onStop()
|
||||
recStartedAt = System.currentTimeMillis()
|
||||
progressJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
while(isActive) {
|
||||
onProgressUpdate(progress(), false)
|
||||
delay(50)
|
||||
}
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
onProgressUpdate(realDuration(path), true)
|
||||
}
|
||||
}
|
||||
stopRecording = { stop(); onStop() }
|
||||
return filePath
|
||||
rec.setOnInfoListener { _, what, _ ->
|
||||
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED || what == MEDIA_RECORDER_INFO_MAX_DURATION_REACHED) {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
stopRecording = { stop() }
|
||||
return path
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
if (!recordingInProgress.value) return
|
||||
override fun stop(): Int {
|
||||
val path = filePath ?: return 0
|
||||
stopRecording = null
|
||||
recordingInProgress.value = false
|
||||
recorder?.metrics?.
|
||||
runCatching {
|
||||
recorder?.stop()
|
||||
}
|
||||
@@ -76,16 +88,25 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
recorder?.reset()
|
||||
}
|
||||
runCatching {
|
||||
// release all resources
|
||||
recorder?.release()
|
||||
}
|
||||
// Await coroutine finishes in order to send real duration to it's listener
|
||||
runBlocking {
|
||||
progressJob?.cancelAndJoin()
|
||||
}
|
||||
progressJob = null
|
||||
filePath = null
|
||||
recorder = null
|
||||
return (realDuration(path) ?: 0).also { recStartedAt = null }
|
||||
}
|
||||
|
||||
override fun cancel(filePath: String, recordingInProgress: MutableState<Boolean>) {
|
||||
stop()
|
||||
runCatching { File(filePath).delete() }.getOrElse { Log.d(TAG, "Unable to delete a file: ${it.stackTraceToString()}") }
|
||||
}
|
||||
private fun progress(): Int? = recStartedAt?.let { (System.currentTimeMillis() - it).toInt() }
|
||||
|
||||
/**
|
||||
* Return real duration from [AudioPlayer] if it's possible (should always be possible).
|
||||
* As a fallback, return internally counted duration
|
||||
* */
|
||||
private fun realDuration(path: String): Int? = duration(path) ?: progress()
|
||||
}
|
||||
|
||||
object AudioPlayer {
|
||||
@@ -251,8 +272,8 @@ object AudioPlayer {
|
||||
audioPlaying.value = false
|
||||
}
|
||||
|
||||
fun duration(filePath: String): Int {
|
||||
var res = 0
|
||||
fun duration(filePath: String): Int? {
|
||||
var res: Int? = null
|
||||
kotlin.runCatching {
|
||||
helperPlayer.setDataSource(filePath)
|
||||
helperPlayer.prepare()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
@@ -11,6 +12,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
@@ -99,6 +101,7 @@ fun SectionItemView(
|
||||
@Composable
|
||||
fun SectionItemViewSpaceBetween(
|
||||
click: (() -> Unit)? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
minHeight: Dp = 46.dp,
|
||||
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
|
||||
disabled: Boolean = false,
|
||||
@@ -108,7 +111,7 @@ fun SectionItemViewSpaceBetween(
|
||||
.fillMaxWidth()
|
||||
.sizeIn(minHeight = minHeight)
|
||||
Row(
|
||||
if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding),
|
||||
if (click == null || disabled) modifier.padding(padding) else modifier.combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -157,6 +160,11 @@ fun <T> SectionItemWithValue(
|
||||
|
||||
@Composable
|
||||
fun SectionTextFooter(text: String) {
|
||||
SectionTextFooter(AnnotatedString(text))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SectionTextFooter(text: AnnotatedString) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING_HALF).fillMaxWidth(0.9F),
|
||||
|
||||
@@ -28,6 +28,22 @@ fun SimpleButton(text: String, icon: ImageVector,
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButton(
|
||||
text: String, icon: ImageVector,
|
||||
color: Color = MaterialTheme.colors.primary,
|
||||
disabled: Boolean,
|
||||
click: () -> Unit
|
||||
) {
|
||||
SimpleButtonFrame(click, disabled = disabled) {
|
||||
Icon(
|
||||
icon, text, tint = if (disabled) HighOrLowlight else color,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(text, style = MaterialTheme.typography.caption, color = if (disabled) HighOrLowlight else color)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButtonIconEnded(
|
||||
text: String,
|
||||
|
||||
@@ -228,7 +228,7 @@ const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
|
||||
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
|
||||
|
||||
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
|
||||
const val MAX_VOICE_MILLIS_FOR_SENDING: Long = 43_000 // approximately is ok
|
||||
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
|
||||
|
||||
const val MAX_FILE_SIZE: Long = 8000000
|
||||
|
||||
@@ -244,6 +244,11 @@ fun getAppFilePath(context: Context, fileName: String): String {
|
||||
return "${getAppFilesDirectory(context)}/$fileName"
|
||||
}
|
||||
|
||||
fun getAppFileUri(fileName: String): Uri {
|
||||
return Uri.parse("${getAppFilesDirectory(SimplexApp.context)}/$fileName")
|
||||
}
|
||||
|
||||
|
||||
fun getLoadedFilePath(context: Context, file: CIFile?): String? {
|
||||
return if (file?.filePath != null && file.loaded) {
|
||||
val filePath = getAppFilePath(context, file.filePath)
|
||||
@@ -331,8 +336,7 @@ fun saveImage(context: Context, image: Bitmap): String? {
|
||||
return try {
|
||||
val ext = if (image.hasAlpha()) "png" else "jpg"
|
||||
val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE)
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
|
||||
val fileToSave = generateNewFileName(context, "IMG", ext)
|
||||
val file = File(getAppFilePath(context, fileToSave))
|
||||
val output = FileOutputStream(file)
|
||||
dataResized.writeTo(output)
|
||||
@@ -355,8 +359,7 @@ fun saveAnimImage(context: Context, uri: Uri): String? {
|
||||
}
|
||||
// Just in case the image has a strange extension
|
||||
if (ext.length < 3 || ext.length > 4) ext = "gif"
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
|
||||
val fileToSave = generateNewFileName(context, "IMG", ext)
|
||||
val file = File(getAppFilePath(context, fileToSave))
|
||||
val output = FileOutputStream(file)
|
||||
context.contentResolver.openInputStream(uri)!!.use { input ->
|
||||
@@ -390,15 +393,23 @@ fun saveFileFromUri(context: Context, uri: Uri): String? {
|
||||
}
|
||||
}
|
||||
|
||||
fun generateNewFileName(context: Context, prefix: String, ext: String): String {
|
||||
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US)
|
||||
sdf.timeZone = TimeZone.getTimeZone("GMT")
|
||||
val timestamp = sdf.format(Date())
|
||||
return uniqueCombine(context, "${prefix}_$timestamp.$ext")
|
||||
}
|
||||
|
||||
fun uniqueCombine(context: Context, fileName: String): String {
|
||||
fun tryCombine(fileName: String, n: Int): String {
|
||||
val name = File(fileName).nameWithoutExtension
|
||||
val ext = File(fileName).extension
|
||||
val orig = File(fileName)
|
||||
val name = orig.nameWithoutExtension
|
||||
val ext = orig.extension
|
||||
fun tryCombine(n: Int): String {
|
||||
val suffix = if (n == 0) "" else "_$n"
|
||||
val f = "$name$suffix.$ext"
|
||||
return if (File(getAppFilePath(context, f)).exists()) tryCombine(fileName, n + 1) else f
|
||||
return if (File(getAppFilePath(context, f)).exists()) tryCombine(n + 1) else f
|
||||
}
|
||||
return tryCombine(fileName, 0)
|
||||
return tryCombine(0)
|
||||
}
|
||||
|
||||
fun formatBytes(bytes: Long): String {
|
||||
|
||||
@@ -134,7 +134,13 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
if (enabled) {
|
||||
CreateGroupButton(MaterialTheme.colors.primary, Modifier
|
||||
.clickable { createGroup(GroupProfile(displayName.value, fullName.value, profileImage.value)) }
|
||||
.clickable {
|
||||
createGroup(GroupProfile(
|
||||
displayName = displayName.value,
|
||||
fullName = fullName.value,
|
||||
image = profileImage.value
|
||||
))
|
||||
}
|
||||
.padding(8.dp))
|
||||
} else {
|
||||
CreateGroupButton(HighOrLowlight, Modifier.padding(8.dp))
|
||||
|
||||
@@ -37,7 +37,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<NewChatSheetState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedViewState>, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) {
|
||||
if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) }
|
||||
NewChatSheetLayout(
|
||||
newChatSheetState,
|
||||
@@ -63,7 +63,7 @@ private val icons = listOf(Icons.Outlined.AddLink, Icons.Outlined.QrCode, Icons.
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
newChatSheetState: StateFlow<NewChatSheetState>,
|
||||
newChatSheetState: StateFlow<AnimatedViewState>,
|
||||
stopped: Boolean,
|
||||
addContact: () -> Unit,
|
||||
connectViaLink: () -> Unit,
|
||||
@@ -216,7 +216,7 @@ fun ActionButton(
|
||||
private fun PreviewNewChatSheet() {
|
||||
SimpleXTheme {
|
||||
NewChatSheetLayout(
|
||||
MutableStateFlow(NewChatSheetState.VISIBLE),
|
||||
MutableStateFlow(AnimatedViewState.VISIBLE),
|
||||
stopped = false,
|
||||
addContact = {},
|
||||
connectViaLink = {},
|
||||
|
||||
@@ -16,11 +16,12 @@ import kotlinx.coroutines.launch
|
||||
enum class OnboardingStage {
|
||||
Step1_SimpleXInfo,
|
||||
Step2_CreateProfile,
|
||||
Step3_SetNotificationsMode,
|
||||
OnboardingComplete
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateProfile(chatModel: ChatModel) {
|
||||
fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
@@ -33,7 +34,10 @@ fun CreateProfile(chatModel: ChatModel) {
|
||||
.background(color = MaterialTheme.colors.background)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
CreateProfilePanel(chatModel)
|
||||
CreateProfilePanel(chatModel, close)
|
||||
LaunchedEffect(Unit) {
|
||||
setLastVersionDefault(chatModel)
|
||||
}
|
||||
if (savedKeyboardState != keyboardState) {
|
||||
LaunchedEffect(keyboardState) {
|
||||
scope.launch {
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package chat.simplex.app.views.onboarding
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import chat.simplex.app.views.usersettings.changeNotificationsMode
|
||||
|
||||
@Composable
|
||||
fun SetNotificationsMode(m: ChatModel) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.onboarding_notifications_mode_title), false)
|
||||
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
|
||||
Text(stringResource(R.string.onboarding_notifications_mode_subtitle))
|
||||
Spacer(Modifier.padding(DEFAULT_PADDING_HALF))
|
||||
NotificationButton(currentMode, NotificationsMode.OFF, R.string.onboarding_notifications_mode_off, R.string.onboarding_notifications_mode_off_desc)
|
||||
NotificationButton(currentMode, NotificationsMode.PERIODIC, R.string.onboarding_notifications_mode_periodic, R.string.onboarding_notifications_mode_periodic_desc)
|
||||
NotificationButton(currentMode, NotificationsMode.SERVICE, R.string.onboarding_notifications_mode_service, R.string.onboarding_notifications_mode_service_desc)
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage) {
|
||||
changeNotificationsMode(currentMode.value, m)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mode: NotificationsMode, @StringRes title: Int, @StringRes description: Int) {
|
||||
TextButton(
|
||||
onClick = { currentMode.value = mode },
|
||||
border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight.copy(alpha = 0.5f)),
|
||||
shape = RoundedCornerShape(15.dp),
|
||||
) {
|
||||
Column(Modifier.padding(bottom = 6.dp).padding(horizontal = 8.dp)) {
|
||||
Text(
|
||||
stringResource(title),
|
||||
style = MaterialTheme.typography.h2,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Text(annotatedStringResource(description), color = MaterialTheme.colors.onBackground, lineHeight = 24.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ModalManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
|
||||
@@ -105,14 +104,14 @@ private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: I
|
||||
@Composable
|
||||
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
|
||||
if (user == null) {
|
||||
ActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
|
||||
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
|
||||
} else {
|
||||
ActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
|
||||
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(
|
||||
fun OnboardingActionButton(
|
||||
@StringRes labelId: Int,
|
||||
onboarding: OnboardingStage?,
|
||||
onboardingStage: MutableState<OnboardingStage?>,
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package chat.simplex.app.views.onboarding
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) }
|
||||
|
||||
@Composable
|
||||
fun featureDescription(icon: ImageVector, titleId: Int, descrId: Int, link: String?) {
|
||||
@Composable
|
||||
fun linkButton(link: String) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Icon(
|
||||
Icons.Outlined.OpenInNew, stringResource(titleId), tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.clickable { uriHandler.openUri(link) }
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(icon, stringResource(titleId), tint = HighOrLowlight)
|
||||
Text(
|
||||
generalGetString(titleId),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h3,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (link != null) {
|
||||
linkButton(link)
|
||||
}
|
||||
}
|
||||
Text(generalGetString(descrId))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun pagination() {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(bottom = 16.dp)
|
||||
) {
|
||||
if (currentVersion.value > 0) {
|
||||
val prev = currentVersion.value - 1
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.clickable { currentVersion.value = prev }
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Icon(Icons.Outlined.ArrowBackIosNew, "previous", tint = MaterialTheme.colors.primary)
|
||||
Text(versionDescriptions[prev].version, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
if (currentVersion.value < versionDescriptions.lastIndex) {
|
||||
val next = currentVersion.value + 1
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.clickable { currentVersion.value = next }
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Text(versionDescriptions[next].version, color = MaterialTheme.colors.primary)
|
||||
Icon(Icons.Outlined.ArrowForwardIos, "next", tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val v = versionDescriptions[currentVersion.value]
|
||||
|
||||
ModalView(close = close) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
String.format(generalGetString(R.string.new_in_version), v.version),
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(DEFAULT_PADDING),
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h1,
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
|
||||
v.features.forEach { feature ->
|
||||
featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link)
|
||||
}
|
||||
|
||||
if (!viaSettings) {
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(
|
||||
Modifier.fillMaxWidth(), contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
generalGetString(R.string.ok),
|
||||
modifier = Modifier.clickable(onClick = close),
|
||||
style = MaterialTheme.typography.h3,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
|
||||
pagination()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class FeatureDescription(
|
||||
val icon: ImageVector,
|
||||
val titleId: Int,
|
||||
val descrId: Int,
|
||||
val link: String? = null
|
||||
)
|
||||
|
||||
private data class VersionDescription(
|
||||
val version: String,
|
||||
val features: List<FeatureDescription>
|
||||
)
|
||||
|
||||
private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
VersionDescription(
|
||||
version = "v4.2",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.VerifiedUser,
|
||||
titleId = R.string.v4_2_security_assessment,
|
||||
descrId = R.string.v4_2_security_assessment_desc,
|
||||
link = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Group,
|
||||
titleId = R.string.v4_2_group_links,
|
||||
descrId = R.string.v4_2_group_links_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Check,
|
||||
titleId = R.string.v4_2_auto_accept_contact_requests,
|
||||
descrId = R.string.v4_2_auto_accept_contact_requests_desc
|
||||
),
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.3",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Mic,
|
||||
titleId = R.string.v4_3_voice_messages,
|
||||
descrId = R.string.v4_3_voice_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.DeleteForever,
|
||||
titleId = R.string.v4_3_irreversible_message_deletion,
|
||||
descrId = R.string.v4_3_irreversible_message_deletion_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.WifiTethering,
|
||||
titleId = R.string.v4_3_improved_server_configuration,
|
||||
descrId = R.string.v4_3_improved_server_configuration_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.VisibilityOff,
|
||||
titleId = R.string.v4_3_improved_privacy_and_security,
|
||||
descrId = R.string.v4_3_improved_privacy_and_security_desc
|
||||
),
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.4",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Timer,
|
||||
titleId = R.string.v4_4_disappearing_messages,
|
||||
descrId = R.string.v4_4_disappearing_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Pending,
|
||||
titleId = R.string.v4_4_live_messages,
|
||||
descrId = R.string.v4_4_live_messages_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.VerifiedUser,
|
||||
titleId = R.string.v4_4_verify_connection_security,
|
||||
descrId = R.string.v4_4_verify_connection_security_desc
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v4_4_french_interface,
|
||||
descrId = R.string.v4_4_french_interface_descr
|
||||
)
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v4.5",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.ManageAccounts,
|
||||
titleId = R.string.v4_5_multiple_chat_profiles,
|
||||
descrId = R.string.v4_5_multiple_chat_profiles_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.EditNote,
|
||||
titleId = R.string.v4_5_message_draft,
|
||||
descrId = R.string.v4_5_message_draft_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.SafetyDivider,
|
||||
titleId = R.string.v4_5_transport_isolation,
|
||||
descrId = R.string.v4_5_transport_isolation_descr,
|
||||
link = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation"
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Task,
|
||||
titleId = R.string.v4_5_private_filenames,
|
||||
descrId = R.string.v4_5_private_filenames_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Battery2Bar,
|
||||
titleId = R.string.v4_5_reduced_battery_usage,
|
||||
descrId = R.string.v4_5_reduced_battery_usage_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v4_5_italian_interface,
|
||||
descrId = R.string.v4_5_italian_interface_descr,
|
||||
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
|
||||
fun setLastVersionDefault(m: ChatModel) {
|
||||
m.controller.appPrefs.whatsNewVersion.set(lastVersion)
|
||||
}
|
||||
|
||||
fun shouldShowWhatsNew(m: ChatModel): Boolean {
|
||||
val v = m.controller.appPrefs.whatsNewVersion.get()
|
||||
setLastVersionDefault(m)
|
||||
return v != lastVersion
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun PreviewWhatsNewView() {
|
||||
SimpleXTheme {
|
||||
WhatsNewView(
|
||||
viaSettings = true,
|
||||
close = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
+21
-9
@@ -33,6 +33,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
|
||||
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
|
||||
val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) }
|
||||
val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) }
|
||||
val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) }
|
||||
val networkTCPKeepIdle: MutableState<Int>
|
||||
val networkTCPKeepIntvl: MutableState<Int>
|
||||
@@ -48,10 +49,6 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
fun buildCfg(): NetCfg {
|
||||
val socksProxy = currentCfg.value.socksProxy
|
||||
val tcpConnectTimeout = networkTCPConnectTimeout.value
|
||||
val tcpTimeout = networkTCPTimeout.value
|
||||
val smpPingInterval = networkSMPPingInterval.value
|
||||
val enableKeepAlive = networkEnableKeepAlive.value
|
||||
val tcpKeepAlive = if (enableKeepAlive) {
|
||||
val keepIdle = networkTCPKeepIdle.value
|
||||
@@ -62,11 +59,15 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
null
|
||||
}
|
||||
return NetCfg(
|
||||
socksProxy = socksProxy,
|
||||
tcpConnectTimeout = tcpConnectTimeout,
|
||||
tcpTimeout = tcpTimeout,
|
||||
socksProxy = currentCfg.value.socksProxy,
|
||||
hostMode = currentCfg.value.hostMode,
|
||||
requiredHostMode = currentCfg.value.requiredHostMode,
|
||||
sessionMode = currentCfg.value.sessionMode,
|
||||
tcpConnectTimeout = networkTCPConnectTimeout.value,
|
||||
tcpTimeout = networkTCPTimeout.value,
|
||||
tcpKeepAlive = tcpKeepAlive,
|
||||
smpPingInterval = smpPingInterval
|
||||
smpPingInterval = networkSMPPingInterval.value,
|
||||
smpPingCount = networkSMPPingCount.value
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,6 +75,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
networkTCPConnectTimeout.value = cfg.tcpConnectTimeout
|
||||
networkTCPTimeout.value = cfg.tcpTimeout
|
||||
networkSMPPingInterval.value = cfg.smpPingInterval
|
||||
networkSMPPingCount.value = cfg.smpPingCount
|
||||
networkEnableKeepAlive.value = cfg.enableKeepAlive
|
||||
if (cfg.tcpKeepAlive != null) {
|
||||
networkTCPKeepIdle.value = cfg.tcpKeepAlive.keepIdle
|
||||
@@ -113,6 +115,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
networkTCPConnectTimeout,
|
||||
networkTCPTimeout,
|
||||
networkSMPPingInterval,
|
||||
networkSMPPingCount,
|
||||
networkEnableKeepAlive,
|
||||
networkTCPKeepIdle,
|
||||
networkTCPKeepIntvl,
|
||||
@@ -129,6 +132,7 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
networkTCPConnectTimeout: MutableState<Long>,
|
||||
networkTCPTimeout: MutableState<Long>,
|
||||
networkSMPPingInterval: MutableState<Long>,
|
||||
networkSMPPingCount: MutableState<Int>,
|
||||
networkEnableKeepAlive: MutableState<Boolean>,
|
||||
networkTCPKeepIdle: MutableState<Int>,
|
||||
networkTCPKeepIntvl: MutableState<Int>,
|
||||
@@ -170,7 +174,14 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
SectionItemView {
|
||||
TimeoutSettingRow(
|
||||
stringResource(R.string.network_option_ping_interval), networkSMPPingInterval,
|
||||
listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000), secondsLabel
|
||||
listOf(120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000), secondsLabel
|
||||
)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
IntSettingRow(
|
||||
stringResource(R.string.network_option_ping_count), networkSMPPingCount,
|
||||
listOf(1, 2, 3, 5, 8), ""
|
||||
)
|
||||
}
|
||||
SectionDivider()
|
||||
@@ -412,6 +423,7 @@ fun PreviewAdvancedNetworkSettingsLayout() {
|
||||
networkTCPConnectTimeout = remember { mutableStateOf(10_000000) },
|
||||
networkTCPTimeout = remember { mutableStateOf(10_000000) },
|
||||
networkSMPPingInterval = remember { mutableStateOf(10_000000) },
|
||||
networkSMPPingCount = remember { mutableStateOf(3) },
|
||||
networkEnableKeepAlive = remember { mutableStateOf(true) },
|
||||
networkTCPKeepIdle = remember { mutableStateOf(10) },
|
||||
networkTCPKeepIntvl = remember { mutableStateOf(10) },
|
||||
|
||||
@@ -24,8 +24,6 @@ import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -65,11 +63,6 @@ fun AppearanceView() {
|
||||
AppearanceLayout(
|
||||
appIcon,
|
||||
changeIcon = ::setAppIcon,
|
||||
showThemeSelector = {
|
||||
ModalManager.shared.showModal(true) {
|
||||
ThemeSelectorView()
|
||||
}
|
||||
},
|
||||
editPrimaryColor = { primary ->
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ColorEditor(primary, close)
|
||||
@@ -81,7 +74,6 @@ fun AppearanceView() {
|
||||
@Composable fun AppearanceLayout(
|
||||
icon: MutableState<AppIcon>,
|
||||
changeIcon: (AppIcon) -> Unit,
|
||||
showThemeSelector: () -> Unit,
|
||||
editPrimaryColor: (Color) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
@@ -115,8 +107,12 @@ fun AppearanceView() {
|
||||
SectionSpacer()
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
SectionView(stringResource(R.string.settings_section_title_themes)) {
|
||||
SectionItemViewSpaceBetween(showThemeSelector) {
|
||||
Text(generalGetString(R.string.theme))
|
||||
SectionItemViewSpaceBetween {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val state = remember { derivedStateOf { currentTheme.second } }
|
||||
ThemeSelector(state) {
|
||||
ThemeManager.applyTheme(it.name, darkTheme)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemViewSpaceBetween({ editPrimaryColor(currentTheme.first.primary) }) {
|
||||
@@ -183,6 +179,21 @@ fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeSelector(state: State<DefaultTheme>, onSelected: (DefaultTheme) -> Unit) {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second to it.third }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.theme),
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon ->
|
||||
SimplexApp.context.packageManager.getComponentEnabledSetting(
|
||||
ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}")
|
||||
@@ -196,7 +207,6 @@ fun PreviewAppearanceSettings() {
|
||||
AppearanceLayout(
|
||||
icon = remember { mutableStateOf(AppIcon.DARK_BLUE) },
|
||||
changeIcon = {},
|
||||
showThemeSelector = {},
|
||||
editPrimaryColor = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -113,7 +114,7 @@ fun SharedPreferenceToggleWithIcon(
|
||||
) {
|
||||
val prefState = preferenceState ?: remember { mutableStateOf(preference.get()) }
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text, Modifier.padding(end = 4.dp))
|
||||
Text(text, Modifier.padding(end = 4.dp), color = if (stopped) HighOrLowlight else Color.Unspecified)
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
|
||||
@@ -17,18 +17,13 @@ import chat.simplex.app.model.Format
|
||||
import chat.simplex.app.model.FormatColor
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun MarkdownHelpView() {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.how_to_use_markdown), false)
|
||||
Text(stringResource(R.string.you_can_use_markdown_to_format_messages__prompt))
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
val bold = stringResource(R.string.bold)
|
||||
|
||||
+76
-7
@@ -31,6 +31,7 @@ fun NetworkAndServersView(
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
val onionHosts = remember { mutableStateOf(netCfg.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(netCfg.sessionMode) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
chatModel.userSMPServersUnsaved.value = null
|
||||
@@ -40,6 +41,7 @@ fun NetworkAndServersView(
|
||||
developerTools = developerTools,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
showModal = showModal,
|
||||
showSettingsModal = showSettingsModal,
|
||||
toggleSocksProxy = { enable ->
|
||||
@@ -82,9 +84,13 @@ fun NetworkAndServersView(
|
||||
OnionHosts.PREFER -> generalGetString(R.string.network_use_onion_hosts_prefer_desc_in_alert)
|
||||
OnionHosts.REQUIRED -> generalGetString(R.string.network_use_onion_hosts_required_desc_in_alert)
|
||||
}
|
||||
updateOnionHostsDialog(startsWith, onDismiss = {
|
||||
onionHosts.value = prevValue
|
||||
}) {
|
||||
updateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_onion_hosts_settings_question),
|
||||
startsWith,
|
||||
onDismiss = {
|
||||
onionHosts.value = prevValue
|
||||
}
|
||||
) {
|
||||
withApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
@@ -96,6 +102,31 @@ fun NetworkAndServersView(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateSessionMode = {
|
||||
if (sessionMode.value == it) return@NetworkAndServersLayout
|
||||
val prevValue = sessionMode.value
|
||||
sessionMode.value = it
|
||||
val startsWith = when (it) {
|
||||
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
|
||||
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_network_session_mode_question),
|
||||
startsWith,
|
||||
onDismiss = { sessionMode.value = prevValue }
|
||||
) {
|
||||
withApi {
|
||||
val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it)
|
||||
val res = chatModel.controller.apiSetNetworkConfig(newCfg)
|
||||
if (res) {
|
||||
chatModel.controller.setNetCfg(newCfg)
|
||||
sessionMode.value = it
|
||||
} else {
|
||||
sessionMode.value = prevValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -104,10 +135,12 @@ fun NetworkAndServersView(
|
||||
developerTools: Boolean,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -123,10 +156,12 @@ fun NetworkAndServersView(
|
||||
}
|
||||
SectionDivider()
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||
SectionDivider()
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showSettingsModal, updateSessionMode)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
}
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SectionView(generalGetString(R.string.settings_section_title_calls)) {
|
||||
@@ -185,7 +220,6 @@ private fun UseOnionHosts(
|
||||
}
|
||||
}
|
||||
val onSelected = showModal {
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
@@ -205,14 +239,47 @@ private fun UseOnionHosts(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateOnionHostsDialog(
|
||||
@Composable
|
||||
private fun SessionModePicker(
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val values = remember {
|
||||
TransportSessionMode.values().map {
|
||||
when (it) {
|
||||
TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(R.string.network_session_mode_user), generalGetString(R.string.network_session_mode_user_description))
|
||||
TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(R.string.network_session_mode_entity), generalGetString(R.string.network_session_mode_entity_description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionItemWithValue(
|
||||
generalGetString(R.string.network_session_mode_transport_isolation),
|
||||
sessionMode,
|
||||
values,
|
||||
icon = Icons.Outlined.SafetyDivider,
|
||||
onSelected = showModal {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.network_session_mode_transport_isolation))
|
||||
SectionViewSelectable(null, sessionMode, values, updateSessionMode)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.update_onion_hosts_settings_question),
|
||||
title = title,
|
||||
text = startsWith + "\n\n" + message,
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onDismiss = onDismiss,
|
||||
@@ -232,7 +299,9 @@ fun PreviewNetworkAndServersLayout() {
|
||||
showSettingsModal = { {} },
|
||||
toggleSocksProxy = {},
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
useOnion = {},
|
||||
updateSessionMode = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+22
-21
@@ -46,25 +46,6 @@ enum class NotificationPreviewMode {
|
||||
fun NotificationsSettingsView(
|
||||
chatModel: ChatModel,
|
||||
) {
|
||||
val onNotificationsModeSelected = { mode: NotificationsMode ->
|
||||
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
|
||||
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
|
||||
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
|
||||
}
|
||||
chatModel.notificationsMode.value = mode
|
||||
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (mode == NotificationsMode.SERVICE)
|
||||
SimplexService.start(SimplexApp.context)
|
||||
else
|
||||
SimplexService.safeStopService(SimplexApp.context)
|
||||
}
|
||||
|
||||
if (mode != NotificationsMode.PERIODIC) {
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
}
|
||||
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
|
||||
}
|
||||
val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode ->
|
||||
chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name)
|
||||
chatModel.notificationPreviewMode.value = mode
|
||||
@@ -76,7 +57,7 @@ fun NotificationsSettingsView(
|
||||
showPage = { page ->
|
||||
ModalManager.shared.showModalCloseable(true) {
|
||||
when (page) {
|
||||
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode, onNotificationsModeSelected)
|
||||
CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode) { changeNotificationsMode(it, chatModel) }
|
||||
CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected)
|
||||
}
|
||||
}
|
||||
@@ -159,7 +140,7 @@ fun NotificationPreviewView(
|
||||
}
|
||||
|
||||
// mode, name, description
|
||||
fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
|
||||
private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> {
|
||||
val res = ArrayList<ValueTitleDesc<NotificationsMode>>()
|
||||
res.add(
|
||||
ValueTitleDesc(
|
||||
@@ -211,3 +192,23 @@ fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> {
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) {
|
||||
chatModel.controller.appPrefs.notificationsMode.set(mode.name)
|
||||
if (mode.requiresIgnoringBattery && !chatModel.controller.isIgnoringBatteryOptimizations(chatModel.controller.appContext)) {
|
||||
chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false)
|
||||
}
|
||||
chatModel.notificationsMode.value = mode
|
||||
SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
if (mode == NotificationsMode.SERVICE)
|
||||
SimplexService.start(SimplexApp.context)
|
||||
else
|
||||
SimplexService.safeStopService(SimplexApp.context)
|
||||
}
|
||||
|
||||
if (mode != NotificationsMode.PERIODIC) {
|
||||
MessagesFetcherWorker.cancelAll()
|
||||
}
|
||||
chatModel.controller.showBackgroundServiceNoticeIfNeeded()
|
||||
}
|
||||
|
||||
@@ -29,12 +29,8 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
|
||||
val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences())
|
||||
val updatedProfile = m.controller.apiUpdateProfile(newProfile)
|
||||
if (updatedProfile != null) {
|
||||
val updatedUser = user.copy(
|
||||
profile = updatedProfile.toLocalProfile(user.profile.profileId),
|
||||
fullPreferences = preferences
|
||||
)
|
||||
m.updateCurrentUser(updatedProfile, preferences)
|
||||
currentPreferences = preferences
|
||||
m.currentUser.value = updatedUser
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
@@ -69,14 +65,19 @@ private fun PreferencesLayout(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_preferences))
|
||||
val timedMessages = remember(preferences) { mutableStateOf(preferences.timedMessages.allow) }
|
||||
TimedMessagesFeatureSection(timedMessages) {
|
||||
applyPrefs(preferences.copy(timedMessages = TimedMessagesPreference(allow = if (it) FeatureAllowed.YES else FeatureAllowed.NO)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowFullDeletion = remember(preferences) { mutableStateOf(preferences.fullDelete.allow) }
|
||||
FeatureSection(ChatFeature.FullDelete, allowFullDeletion) {
|
||||
applyPrefs(preferences.copy(fullDelete = ChatPreference(allow = it)))
|
||||
applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) }
|
||||
FeatureSection(ChatFeature.Voice, allowVoice) {
|
||||
applyPrefs(preferences.copy(voice = ChatPreference(allow = it)))
|
||||
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
ResetSaveButtons(
|
||||
@@ -103,6 +104,22 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllo
|
||||
SectionTextFooter(feature.allowDescription(allowFeature.value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimedMessagesFeatureSection(allowFeature: State<FeatureAllowed>, onSelected: (Boolean) -> Unit) {
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
PreferenceToggleWithIcon(
|
||||
ChatFeature.TimedMessages.text,
|
||||
ChatFeature.TimedMessages.icon,
|
||||
HighOrLowlight,
|
||||
allowFeature.value == FeatureAllowed.ALWAYS || allowFeature.value == FeatureAllowed.YES,
|
||||
onSelected
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(ChatFeature.TimedMessages.allowDescription(allowFeature.value))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
SectionView {
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.views.usersettings
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -15,6 +16,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -76,6 +79,7 @@ fun SMPServersView(m: ChatModel) {
|
||||
serversUnchanged = serversUnchanged.value,
|
||||
saveDisabled = saveDisabled.value,
|
||||
allServersDisabled = allServersDisabled.value,
|
||||
m.currentUser.value,
|
||||
addServer = {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(R.string.smp_servers_add),
|
||||
@@ -156,6 +160,7 @@ private fun SMPServersLayout(
|
||||
serversUnchanged: Boolean,
|
||||
saveDisabled: Boolean,
|
||||
allServersDisabled: Boolean,
|
||||
currentUser: User?,
|
||||
addServer: () -> Unit,
|
||||
testServers: () -> Unit,
|
||||
resetServers: () -> Unit,
|
||||
@@ -186,6 +191,17 @@ private fun SMPServersLayout(
|
||||
iconColor = if (testing) HighOrLowlight else MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(R.string.smp_servers_per_user) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser?.displayName ?: "")
|
||||
}
|
||||
append(".")
|
||||
}
|
||||
}
|
||||
)
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
SectionItemView(resetServers, disabled = serversUnchanged) {
|
||||
|
||||
+73
-30
@@ -4,6 +4,7 @@ import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -16,14 +17,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -34,6 +35,7 @@ import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.CreateLinkTab
|
||||
import chat.simplex.app.views.newchat.CreateLinkView
|
||||
import chat.simplex.app.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.app.views.onboarding.WhatsNewView
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
@@ -43,6 +45,8 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
MaintainIncognitoState(chatModel)
|
||||
|
||||
if (user != null) {
|
||||
val requireAuth = remember { chatModel.controller.appPrefs.performLA.state }
|
||||
val context = LocalContext.current
|
||||
SettingsLayout(
|
||||
profile = user.profile,
|
||||
stopped,
|
||||
@@ -55,8 +59,43 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } },
|
||||
showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } },
|
||||
showTerminal = { ModalManager.shared.showCustomModal { close -> TerminalView(chatModel, close) } },
|
||||
// showVideoChatPrototype = { ModalManager.shared.showCustomModal { close -> CallViewDebug(close) } },
|
||||
showVersion = {
|
||||
withApi {
|
||||
val info = chatModel.controller.apiGetVersion()
|
||||
if (info != null) {
|
||||
ModalManager.shared.showModal { VersionInfoView(info) }
|
||||
}
|
||||
}
|
||||
},
|
||||
withAuth = { block ->
|
||||
if (!requireAuth.value) {
|
||||
block()
|
||||
} else {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
val onFinishAuth = { success: Boolean ->
|
||||
if (success) {
|
||||
close()
|
||||
block()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
runAuth(context, onFinishAuth)
|
||||
}
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
runAuth(context, onFinishAuth)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -64,16 +103,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
val simplexTeamUri =
|
||||
"simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
|
||||
|
||||
// TODO pass close
|
||||
//fun showSectionedModal(chatModel: ChatModel, modalView: (@Composable (ChatModel) -> Unit)) {
|
||||
// ModalManager.shared.showCustomModal { close ->
|
||||
// ModalView(close = close, modifier = Modifier,
|
||||
// background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight) {
|
||||
// modalView(chatModel)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@Composable
|
||||
fun SettingsLayout(
|
||||
profile: LocalProfile,
|
||||
@@ -87,8 +116,8 @@ fun SettingsLayout(
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
showTerminal: () -> Unit,
|
||||
// showVideoChatPrototype: () -> Unit
|
||||
showVersion: () -> Unit,
|
||||
withAuth: (block: () -> Unit) -> Unit
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Surface(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {
|
||||
@@ -112,11 +141,13 @@ fun SettingsLayout(
|
||||
ProfilePreview(profile, stopped = stopped)
|
||||
}
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.ManageAccounts, stringResource(R.string.your_chat_profiles), { withAuth { showSettingsModal { UserProfilesView(it) }() } }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { showModal { IncognitoView() }() }
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.QrCode, stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
ChatPreferencesItem(showCustomModal)
|
||||
ChatPreferencesItem(showCustomModal, stopped = stopped)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
@@ -138,9 +169,9 @@ fun SettingsLayout(
|
||||
SectionView(stringResource(R.string.settings_section_title_help)) {
|
||||
SettingsActionItem(Icons.Outlined.HelpOutline, stringResource(R.string.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Info, stringResource(R.string.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) })
|
||||
SettingsActionItem(Icons.Outlined.Add, stringResource(R.string.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.TextFormat, stringResource(R.string.markdown_in_messages), showModal { MarkdownHelpView() })
|
||||
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)
|
||||
SectionDivider()
|
||||
@@ -162,14 +193,14 @@ fun SettingsLayout(
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
|
||||
SectionDivider()
|
||||
if (devTools.value) {
|
||||
ChatConsoleItem(showTerminal)
|
||||
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
}
|
||||
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) })
|
||||
// SectionDivider()
|
||||
AppVersionItem()
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,17 +270,18 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit))) {
|
||||
@Composable fun ChatPreferencesItem(showCustomModal: ((@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit)), stopped: Boolean) {
|
||||
SettingsActionItem(
|
||||
Icons.Outlined.ToggleOn,
|
||||
stringResource(R.string.chat_preferences),
|
||||
click = {
|
||||
click = if (stopped) null else ({
|
||||
withApi {
|
||||
showCustomModal { m, close ->
|
||||
PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}),
|
||||
disabled = stopped
|
||||
)
|
||||
}
|
||||
|
||||
@@ -344,8 +376,8 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun AppVersionItem() {
|
||||
SectionItemView() {
|
||||
@Composable private fun AppVersionItem(showVersion: () -> Unit) {
|
||||
SectionItemView(showVersion) {
|
||||
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
}
|
||||
}
|
||||
@@ -407,7 +439,7 @@ fun SettingsPreferenceItemWithInfo(
|
||||
pref: SharedPreference<Boolean>,
|
||||
prefState: MutableState<Boolean>? = null
|
||||
) {
|
||||
SectionItemView(onClickInfo) {
|
||||
SectionItemView(if (stopped) null else onClickInfo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(icon, text, tint = if (stopped) HighOrLowlight else iconTint)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
@@ -468,6 +500,17 @@ fun PreferenceToggleWithIcon(
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_open_chat_console),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
@@ -488,9 +531,9 @@ fun PreviewSettingsLayout() {
|
||||
setPerformLA = {},
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showCustomModal = { {}},
|
||||
showTerminal = {},
|
||||
// showVideoChatPrototype = {}
|
||||
showCustomModal = { {} },
|
||||
showVersion = {},
|
||||
withAuth = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionViewSelectable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun ThemeSelectorView() {
|
||||
val darkTheme = isSystemInDarkTheme()
|
||||
val allThemes by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { ValueTitleDesc(it.second, it.third, "") }) }
|
||||
|
||||
ThemeSelectorLayout(
|
||||
allThemes,
|
||||
onSelectTheme = {
|
||||
ThemeManager.applyTheme(it.name, darkTheme)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeSelectorLayout(
|
||||
allThemes: List<ValueTitleDesc<DefaultTheme>>,
|
||||
onSelectTheme: (DefaultTheme) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.settings_section_title_themes).lowercase().capitalize(Locale.current))
|
||||
val currentTheme by CurrentColors.collectAsState()
|
||||
val state = remember { derivedStateOf { currentTheme.second } }
|
||||
SectionViewSelectable(null, state, allThemes, onSelectTheme)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -44,12 +44,9 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
close,
|
||||
saveProfile = { displayName, fullName, image ->
|
||||
withApi {
|
||||
val p = Profile(displayName, fullName, image)
|
||||
val newProfile = chatModel.controller.apiUpdateProfile(p)
|
||||
val newProfile = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image))
|
||||
if (newProfile != null) {
|
||||
chatModel.currentUser.value?.profile?.profileId?.let {
|
||||
chatModel.updateUserProfile(newProfile.toLocalProfile(it))
|
||||
}
|
||||
chatModel.updateCurrentUser(newProfile)
|
||||
profile = newProfile
|
||||
}
|
||||
editProfile.value = false
|
||||
@@ -97,7 +94,7 @@ fun UserProfileLayout(
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_chat_profile), false)
|
||||
AppBarTitle(stringResource(R.string.your_current_profile), false)
|
||||
Text(
|
||||
stringResource(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it),
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun UserProfilesView(m: ChatModel) {
|
||||
val users by remember { derivedStateOf { m.users.map { it.user } } }
|
||||
UserProfilesView(
|
||||
users = users,
|
||||
addUser = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
CreateProfile(m, close)
|
||||
}
|
||||
},
|
||||
activateUser = { user ->
|
||||
withBGApi {
|
||||
m.controller.changeActiveUser(user.userId)
|
||||
}
|
||||
},
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfilesView(
|
||||
users: List<User>,
|
||||
addUser: () -> Unit,
|
||||
activateUser: (User) -> Unit,
|
||||
removeUser: (User) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_chat_profiles))
|
||||
|
||||
SectionView {
|
||||
for (user in users) {
|
||||
UserView(user, users, activateUser, removeUser)
|
||||
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)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(R.string.your_chat_profiles_stored_locally))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserView(user: User, users: List<User>, activateUser: (User) -> Unit, removeUser: (User) -> Unit) {
|
||||
var showDropdownMenu by remember { mutableStateOf(false) }
|
||||
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
|
||||
activateUser(user)
|
||||
}
|
||||
Box(Modifier.padding(horizontal = 16.dp)) {
|
||||
DropdownMenu(
|
||||
expanded = showDropdownMenu,
|
||||
onDismissRequest = { showDropdownMenu = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
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) {
|
||||
if (users.size < 2) return
|
||||
|
||||
withBGApi {
|
||||
try {
|
||||
if (user.activeUser) {
|
||||
val newActive = users.first { !it.activeUser }
|
||||
m.controller.changeActiveUser_(newActive.userId)
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.BuildConfig
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CoreVersionInfo
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
|
||||
@Composable
|
||||
fun VersionInfoView(info: CoreVersionInfo) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.app_version_title), false)
|
||||
Text(String.format(stringResource(R.string.app_version_name), BuildConfig.VERSION_NAME))
|
||||
Text(String.format(stringResource(R.string.app_version_code), BuildConfig.VERSION_CODE))
|
||||
Text(String.format(stringResource(R.string.core_version), info.version))
|
||||
Text(String.format(stringResource(R.string.core_build_timestamp), info.buildTimestamp))
|
||||
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, info.simplexmqCommit.substring(startIndex = 0, endIndex = 7)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
<?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="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>
|
||||
<string name="network_settings">Pokročilá nastavení sítě</string>
|
||||
<string name="accept">Přijmout</string>
|
||||
<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="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>
|
||||
<string name="allow_verb">Povolit</string>
|
||||
<string name="allow_voice_messages_question">Povolit hlasové zprávy\?</string>
|
||||
<string name="about_simplex">O Simplex</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="accept_call_on_lock_screen">Přijmout</string>
|
||||
<string name="chat_item_ttl_day">1 den</string>
|
||||
<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="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íc</string>
|
||||
<string name="chat_item_ttl_week">1 týden</string>
|
||||
<string name="callstatus_accepted">přijatý hovor</string>
|
||||
<string name="accept_contact_incognito_button">Přijmout inkognito</string>
|
||||
<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="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>
|
||||
<string name="button_create_group_link">Vytvořit odkaz</string>
|
||||
<string name="delete_link_question">Smazat odkaz\?</string>
|
||||
<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="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říjem prostřednictvím</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_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_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>
|
||||
<string name="incognito">Inkognito</string>
|
||||
<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">Akcent</string>
|
||||
<string name="chat_preferences_you_allow">Povolíte</string>
|
||||
<string name="chat_preferences_default">výchozí (%s)</string>
|
||||
<string name="chat_preferences_yes">ano</string>
|
||||
<string name="chat_preferences_no">ne</string>
|
||||
<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">Zmizení zpráv</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="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="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="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_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 pozvánku\?</string>
|
||||
<string name="connect_via_group_link">Připojit se přes odkaz 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>
|
||||
<string name="server_connecting">připojení</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.</string>
|
||||
<string name="deleted_description">Smazáno</string>
|
||||
<string name="invalid_chat">neplatný chat</string>
|
||||
<string name="invalid_data">neplatné údaje</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_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="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>
|
||||
<string name="simplex_link_mode">Odkazy na SimpleX</string>
|
||||
<string name="simplex_link_mode_description">Popis</string>
|
||||
<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="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í prutu(ů)</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">Error accepting contact request</string>
|
||||
<string name="error_changing_address">Error changing address</string>
|
||||
<string name="settings_notifications_mode_title">Služba oznamování</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="la_notice_title_simplex_lock">SimpleX Lock</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="reply_verb">Odpovězte na</string>
|
||||
<string name="share_verb">Sdílet</string>
|
||||
<string name="copy_verb">Kopírovat</string>
|
||||
<string name="icon_descr_received_msg_status_unread">nepřečteno</string>
|
||||
<string name="personal_welcome">Vítejte <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="welcome">Vítejte!</string>
|
||||
<string name="this_text_is_available_in_settings">Tento text je k dispozici v nastavení</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">odeslání se nezdařilo</string>
|
||||
<string name="share_file">Sdílet soubor…</string>
|
||||
<string name="attach">Připojit</string>
|
||||
<string name="icon_descr_context">Kontextová ikona</string>
|
||||
<string name="image_decoding_exception_desc">Obrázek nelze dekódovat. Zkuste prosím použít jiný obrázek nebo kontaktujte vývojáře.</string>
|
||||
<string name="image_descr">Obrázek</string>
|
||||
<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="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="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>
|
||||
<string name="icon_descr_server_status_disconnected">Odpojeno</string>
|
||||
<string name="icon_descr_server_status_error">Chyba</string>
|
||||
<string name="switch_receiving_address_desc">Tato funkce je experimentální! Bude fungovat pouze v případě, že druhý klient má nainstalovanou verzi 4.2. Po dokončení změny adresy by se měla v konverzaci zobrazit zpráva - zkontrolujte, zda můžete od tohoto kontaktu (nebo člena skupiny) stále přijímat zprávy.</string>
|
||||
<string name="switch_receiving_address_question">Přepnout přijímací adresu\?</string>
|
||||
<string name="send_verb">Send</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Abyste mohli odesílat hlasové zprávy, musíte je povolit svému kontaktu.</string>
|
||||
<string name="icon_descr_cancel_live_message">Cancel live message</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="cancel_verb">Cancel</string>
|
||||
<string name="reset_verb">Reset</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">no details</string>
|
||||
<string name="add_contact">One-time invitation link</string>
|
||||
<string name="copied">Copied to clipboard</string>
|
||||
<string name="add_contact_or_create_group">Start new chat</string>
|
||||
<string name="create_group">Create secret group</string>
|
||||
<string name="to_share_with_your_contact">(to share with your contact)</string>
|
||||
<string name="only_stored_on_members_devices">(only stored by group members)</string>
|
||||
<string name="toast_permission_denied">Permission Denied!</string>
|
||||
<string name="use_camera_button">Use Camera</string>
|
||||
<string name="from_gallery_button">From Gallery</string>
|
||||
<string name="choose_file">Choose file</string>
|
||||
<string name="to_start_a_new_chat_help_header">To start a new chat</string>
|
||||
<string name="chat_help_tap_button">Tap button</string>
|
||||
<string name="above_then_preposition_continuation">above, then:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Add new contact</b>: to create your one-time QR Code for your contact.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scan QR code</b>: to connect to your contact who shows QR code to you.</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="clear_chat_question">Clear chat\?</string>
|
||||
<string name="clear_verb">Clear</string>
|
||||
<string name="mark_read">Označit přečtení</string>
|
||||
<string name="mark_unread">Označit jako nepřečtené</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="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>
|
||||
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> Tým</string>
|
||||
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> Adresa</string>
|
||||
<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="paste_connection_link_below_to_connect">Paste the link you received into the box below to connect with your contact.</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">Create one-time invitation link</string>
|
||||
<string name="one_time_link">One-time invitation link</string>
|
||||
<string name="security_code">Bezpečnostní kód</string>
|
||||
<string name="is_verified">\"%s je ověřeno</string>
|
||||
<string name="chat_console">Konzola pro chat</string>
|
||||
<string name="smp_servers">SMP servery</string>
|
||||
<string name="smp_servers_preset_address">Přednastavená adresa serveru</string>
|
||||
<string name="smp_servers_test_failed">Test serveru se nezdařil!</string>
|
||||
<string name="smp_servers_test_some_failed">Některé servery neprošly testem:</string>
|
||||
<string name="smp_servers_scan_qr">Naskenujte QR kód serveru</string>
|
||||
<string name="smp_servers_enter_manually">Zadejte server ručně</string>
|
||||
<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 na</string>
|
||||
<string name="how_to">Jak na to</string>
|
||||
<string name="how_to_use_your_servers">Jak používat servery</string>
|
||||
<string name="your_ICE_servers">Vaše servery ICE</string>
|
||||
<string name="configure_ICE_servers">Konfigurace serverů ICE</string>
|
||||
<string name="network_settings_title">Nastavení sítě</string>
|
||||
<string name="network_enable_socks">Použít proxy server SOCKS\?</string>
|
||||
<string name="network_disable_socks">Použít přímé připojení k internetu\?</string>
|
||||
<string name="network_use_onion_hosts_no">Ne</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Cibuloví hostitelé nebudou použiti.</string>
|
||||
<string name="network_session_mode_user">Profil chatu</string>
|
||||
<string name="network_session_mode_entity">Connection</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="create_address">Create address</string>
|
||||
<string name="accept_automatically">Automatically</string>
|
||||
<string name="section_title_welcome_message">WELCOME MESSAGE</string>
|
||||
<string name="save_and_notify_group_members">Uložit a upozornit členy skupiny</string>
|
||||
<string name="exit_without_saving">Ukončit bez uložení</string>
|
||||
<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ření profilu</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 bílé znaky.</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">Only client devices store user profiles, contacts, groups, and messages sent with <b>2-layer end-to-end encryption</b>.</string>
|
||||
<string name="onboarding_notifications_mode_title">Private notifications</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periodic</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_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>
|
||||
<string name="open_simplex_chat_to_accept_call">Otevřete <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro přijetí hovoru</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Povolte volání ze zamčené obrazovky prostřednictvím Nastavení.</string>
|
||||
<string name="open_verb">Otevřete stránku</string>
|
||||
<string name="status_e2e_encrypted">e2e encrypted</string>
|
||||
<string name="icon_descr_audio_on">Zvuk zapnut</string>
|
||||
<string name="icon_descr_speaker_off">Reproduktor vypnut</string>
|
||||
<string name="icon_descr_speaker_on">Zapnutý reproduktor</string>
|
||||
<string name="icon_descr_call_progress">Probíhající hovor</string>
|
||||
<string name="auto_accept_images">Automatické přijímání obrázků</string>
|
||||
<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_experimental_features">Experimentální funkce</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_icon">IKONA APLIKACE</string>
|
||||
<string name="settings_section_title_themes">TÉMATA</string>
|
||||
<string name="settings_section_title_messages">ZPRÁVY</string>
|
||||
<string name="settings_section_title_calls">VOLÁNÍ</string>
|
||||
<string name="export_database">Export databáze</string>
|
||||
<string name="import_database">Import databáze</string>
|
||||
<string name="delete_database">Odstranění databáze</string>
|
||||
<string name="error_exporting_chat_database">Chyba při exportu databáze chatu</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 databázi chatu.</string>
|
||||
<string name="delete_chat_profile_question">Smazat profil chatu\?</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ý profil chatu.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Nejnovější verzi databáze chatu 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>
|
||||
<string name="stop_chat_to_enable_database_actions">Zastavte chat a povolte akce s databází.</string>
|
||||
<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">Odstranění zpráv</string>
|
||||
<string name="remove_passphrase_from_keychain">Remove passphrase from Keystore\?</string>
|
||||
<string name="notifications_will_be_hidden">Notifications will be delivered only until the app stops!</string>
|
||||
<string name="remove_passphrase">Remove</string>
|
||||
<string name="update_database">Update</string>
|
||||
<string name="current_passphrase">Current passphrase…</string>
|
||||
<string name="update_database_passphrase">Update database passphrase</string>
|
||||
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
|
||||
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Please note</b>: you will NOT be able to recover or change passphrase if you lose it.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">Databáze bude zaš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="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="chat_is_stopped_indication">Chat je zastaven</string>
|
||||
<string name="chat_archive_header">Chat se archivuje</string>
|
||||
<string name="delete_chat_archive_question">Smazat archiv chatu\?</string>
|
||||
<string name="join_group_question">Připojit se ke skupině\?</string>
|
||||
<string name="join_group_button">Připojte se na</string>
|
||||
<string name="leave_group_button">Opustit</string>
|
||||
<string name="icon_descr_add_members">Pozvat členy</string>
|
||||
<string name="alert_title_no_group">Skupina nebyla nalezena!</string>
|
||||
<string name="alert_title_cant_invite_contacts">Nelze pozvat kontakty!</string>
|
||||
<string name="snd_group_event_changed_member_role">změnili jste roli %s na %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">změnili jste svou roli na %s</string>
|
||||
<string name="snd_group_event_member_deleted">odstranili jste <xliff:g id="profil člena" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_user_left">odešli jste</string>
|
||||
<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 zobrazovacím názvem. Zvolte prosím jiné jméno.</string>
|
||||
<string name="smp_server_test_create_queue">Create queue</string>
|
||||
<string name="smp_server_test_secure_queue">Secure queue</string>
|
||||
<string name="service_notifications">Instant notifications!</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>It can be disabled via settings</b> – notifications will still be shown while the app is running.</string>
|
||||
<string name="turn_off_battery_optimization">Chcete-li ji používat, <b>vypněte optimalizaci baterie</b> pro <xliff:g id="appName">SimpleX</xliff:g> v dalším dialogu. V opačném případě budou oznámení vypnuta.</string>
|
||||
<string name="periodic_notifications_desc">Aplikace pravidelně načítá nové zprávy - denně spotřebuje několik procent baterie. Aplikace nepoužívá push oznámení - data ze zařízení nejsou odesílána na servery.</string>
|
||||
<string name="enter_passphrase_notification_title">Je vyžadována přístupová fráze</string>
|
||||
<string name="enter_passphrase_notification_desc">Chcete-li dostávat oznámení, zadejte přístupovou frázi do databáze.</string>
|
||||
<string name="database_initialization_error_title">Nelze inicializovat databázi</string>
|
||||
<string name="hide_notification">Skrýt</string>
|
||||
<string name="ntf_channel_calls">Volání SimpleX Chat</string>
|
||||
<string name="notification_preview_new_message">nová zpráva</string>
|
||||
<string name="notification_new_contact_request">Žádost o nový kontakt</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Zpráva bude smazána - nelze to vzít zpět!</string>
|
||||
<string name="confirm_verb">Confirm</string>
|
||||
<string name="share_invitation_link">Share invitation link</string>
|
||||
<string name="send_us_an_email">Pošlete nám e-mail</string>
|
||||
<string name="chat_lock">Zámek SimpleX</string>
|
||||
<string name="install_simplex_chat_for_terminal">Instalace <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pro terminál</string>
|
||||
<string name="star_on_github">Hvězda na GitHubu</string>
|
||||
<string name="rate_the_app">Ohodnoťte aplikaci</string>
|
||||
<string name="your_SMP_servers">Vaše servery SMP</string>
|
||||
<string name="network_disable_socks_info">Pokud potvrdíte, budou servery pro zasílání zpráv vidět vaši IP adresu a váš poskytovatel - ke kterým serverům se připojujete.</string>
|
||||
<string name="colored">barevné</string>
|
||||
<string name="secret">secret</string>
|
||||
<string name="callstatus_calling">volání…</string>
|
||||
<string name="callstate_connected">připojeno</string>
|
||||
<string name="callstate_ended">ukončeno</string>
|
||||
<string name="next_generation_of_private_messaging">Nová generace soukromých zpráv</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte.</string>
|
||||
<string name="integrity_msg_bad_hash">špatný hash zprávy</string>
|
||||
<string name="chat_database_imported">Importovaná databáze chatu</string>
|
||||
<string name="new_passphrase">New passphrase…</string>
|
||||
<string name="save_passphrase_and_open_chat">Uložte heslo a otevřete chat</string>
|
||||
<string name="chat_archive_section">ARCHIV CHATU</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>
|
||||
<string name="info_row_group">Skupina</string>
|
||||
<string name="network_options_revert">Vrátit</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Aktualizací nastavení se klient znovu připojí ke všem serverům.</string>
|
||||
<string name="accept_feature_set_1_day">Nastavit 1 den</string>
|
||||
<string name="connection_error_auth">Chyba spojení (AUTH)</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string>
|
||||
<string name="error_smp_test_server_auth">Server requires authorization to create queues, check password</string>
|
||||
<string name="smp_server_test_delete_queue">Delete queue</string>
|
||||
<string name="delete_group_menu_action">Smazat</string>
|
||||
<string name="delete_pending_connection__question">Smazat čekající připojení\?</string>
|
||||
<string name="icon_descr_settings">Nastavení</string>
|
||||
<string name="image_descr_qr_code">QR kód</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Váš kontakt může z aplikace naskenovat QR kód.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">If you can\'t meet in person, <b>show QR code in the video call</b>, or share the link.</string>
|
||||
<string name="scan_code">Scan code</string>
|
||||
<string name="incorrect_code">Nesprávný bezpečnostní kód!</string>
|
||||
<string name="scan_code_from_contacts_app">Naskenujte bezpečnostní kód z aplikace vašeho kontaktu.</string>
|
||||
<string name="mark_code_verified">Označit jako ověřený</string>
|
||||
<string name="clear_verification">Zrušte 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 <xliff:g id="appName">Adresa kontaktu SimpleX</xliff:g></string>
|
||||
<string name="database_passphrase_and_export">Databázová hesla a export</string>
|
||||
<string name="your_chat_profiles">Vaše profily v chatu</string>
|
||||
<string name="chat_with_the_founder">Zasílání otázek a nápadů</string>
|
||||
<string name="smp_servers_test_server">Testovací server</string>
|
||||
<string name="enter_one_ICE_server_per_line">Servery ICE (jeden na řádek)</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Pro připojení budou vyžadováni cibuloví hostitelé.</string>
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode\?</string>
|
||||
<string name="app_version_code">Sestavení aplikace: %s</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you. You won\'t lose your contacts if you later delete it.</string>
|
||||
<string name="share_link">Share link</string>
|
||||
<string name="delete_address">Delete address</string>
|
||||
<string name="full_name__field">Full name:</string>
|
||||
<string name="your_current_profile">Váš současný profil</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">To preserve your privacy, instead of push notifications the app has a <b><xliff:g id="appName">SimpleX</xliff:g> background service</b> – it uses a few percent of the battery per day.</string>
|
||||
<string name="periodic_notifications">Pravidelná oznámení</string>
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> služba</string>
|
||||
<string name="simplex_service_notification_text">Příjem zpráv…</string>
|
||||
<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_periodic">Spouští se pravidelně</string>
|
||||
<string name="notifications_mode_service">Vždy zapnuto</string>
|
||||
<string name="notifications_mode_periodic_desc">Kontroluje nové zprávy každých 10 minut po dobu až 1 minuty</string>
|
||||
<string name="notification_preview_mode_contact_desc">Zobrazit 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_unlock">Odemknutí stránky</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. Vypnutí zámku SimpleX Lock.</string>
|
||||
<string name="edit_verb">Upravit</string>
|
||||
<string name="delete_verb">Smazat</string>
|
||||
<string name="for_everybody">Pro všechny</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">odesláno</string>
|
||||
<string name="contact_connection_pending">připojení…</string>
|
||||
<string name="images_limit_desc">Současně lze odeslat pouze 10 obrázků</string>
|
||||
<string name="image_decoding_exception_title">Chyba dekódování</string>
|
||||
<string name="image_saved">Obrázek uložen do galerie</string>
|
||||
<string name="icon_descr_file">Soubor</string>
|
||||
<string name="large_file">Velký soubor!</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Soubor bude přijat, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string>
|
||||
<string name="file_saved">Soubor uložen</string>
|
||||
<string name="file_not_found">Soubor nebyl nalezen</string>
|
||||
<string name="voice_message_with_duration">Hlasová zpráva (<xliff:g id="duration">%1$s</xliff:g>)</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kontakt a všechny zprávy budou smazány - nelze to vzít zpět!</string>
|
||||
<string name="button_delete_contact">Smazat kontakt</string>
|
||||
<string name="text_field_set_contact_placeholder">Nastavení jména kontaktu…</string>
|
||||
<string name="view_security_code">Zobrazení bezpečnostního kódu</string>
|
||||
<string name="icon_descr_record_voice_message">Nahrát hlasovou zprávu</string>
|
||||
<string name="voice_messages_prohibited">Hlasové zprávy jsou zakázány!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Please ask your contact to enable sending voice messages.</string>
|
||||
<string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
|
||||
<string name="share_one_time_link">Create one-time invitation link</string>
|
||||
<string name="scan_QR_code">Scan QR code</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scan or paste from clipboard)</string>
|
||||
<string name="edit_image">Upravit obrázek</string>
|
||||
<string name="delete_image">Smazat obrázek</string>
|
||||
<string name="callstatus_error">chyba volání</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protokol a kód s otevřeným zdrojovým kódem - servery může provozovat kdokoli.</string>
|
||||
<string name="create_your_profile">Vytvořte si svůj profil</string>
|
||||
<string name="make_private_connection">Vytvořte si soukromé připojení</string>
|
||||
<string name="encrypted_video_call">Videohovor šifrovaný e2e</string>
|
||||
<string name="encrypted_audio_call">e2e šifrovaný audio hovor</string>
|
||||
<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 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>
|
||||
<string name="icon_descr_audio_off">Zvuk vypnutý</string>
|
||||
<string name="integrity_msg_bad_id">špatné ID zprávy</string>
|
||||
<string name="integrity_msg_duplicate">duplicitní zpráva</string>
|
||||
<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="transfer_images_faster">Rychlejší přenos obrázků</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">Confirm new passphrase…</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">left</string>
|
||||
<string name="clear_contacts_selection_button">Vymazat</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>
|
||||
<string name="error_removing_member">Chyba při odebrání člena</string>
|
||||
<string name="error_saving_group_profile">Chyba při ukládání profilu skupiny</string>
|
||||
<string name="network_option_seconds_label">sec</string>
|
||||
<string name="incognito_info_allows">Umožňuje mít v jednom profilu chatu mnoho anonymních spojení bez jakýchkoli sdílených údajů mezi nimi.</string>
|
||||
<string name="incognito_info_share">Pokud s někým sdílíte inkognito profil, bude tento profil použit pro skupiny, do kterých vás pozve.</string>
|
||||
<string name="theme_system">Systém</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>
|
||||
<string name="ttl_mth">\"%dmth</string>
|
||||
<string name="ttl_hours">\"%d hodin</string>
|
||||
<string name="ttl_h">\"%dh</string>
|
||||
<string name="ttl_d">\"%dd</string>
|
||||
<string name="v4_2_security_assessment">Posouzení bezpečnosti</string>
|
||||
<string name="v4_2_security_assessment_desc">Bezpečnost SimpleX Chat byla prověřena společností Trail of Bits.</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Nevratné mazání zpráv</string>
|
||||
<string name="v4_3_improved_server_configuration">Vylepšená konfigurace serveru</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Vylepšená ochrana soukromí a zabezpečení</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Skrytí obrazovky aplikace v posledních aplikacích.</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Odeslané zprávy se po uplynutí nastavené doby odstraní.</string>
|
||||
<string name="v4_4_live_messages">Živé zprávy</string>
|
||||
<string name="v4_4_live_messages_desc">Příjemci uvidí aktualizace během jejich psaní.</string>
|
||||
<string name="v4_4_verify_connection_security">Ověření zabezpečení připojení</string>
|
||||
<string name="v4_4_french_interface">Francouzské rozhraní</string>
|
||||
<string name="v4_4_french_interface_descr">Díky uživatelům - přispívejte prostřednictvím Weblate!</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Více chatovacích profilů</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Různá jména, avatary a dopravní izolace.</string>
|
||||
<string name="v4_5_message_draft">Návrh zprávy</string>
|
||||
<string name="v4_5_message_draft_descr">Zachování posledního návrhu zprávy s přílohami.</string>
|
||||
<string name="v4_5_transport_isolation">Izolace transportu</string>
|
||||
<string name="v4_5_transport_isolation_descr">Podle profilu chatu (výchozí) nebo podle připojení (BETA).</string>
|
||||
<string name="you_will_join_group">Připojíte se ke skupině, na kterou tento odkaz odkazuje, a spojíte se s jejími členy.</string>
|
||||
<string name="connect_via_link_verb">Připojení</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Jste připojeni k serveru, který se používá k přijímání zpráv od tohoto kontaktu.</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Pokoušíte se připojit k serveru používanému pro příjem zpráv od tohoto kontaktu (chyba: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="marked_deleted_description">označeno jako smazáno</string>
|
||||
<string name="sending_files_not_yet_supported">Odesílání souborů zatím není podporováno</string>
|
||||
<string name="receiving_files_not_yet_supported">přijímání souborů zatím není podporováno</string>
|
||||
<string name="sender_you_pronoun">you</string>
|
||||
<string name="unknown_message_format">neznámý formát zprávy</string>
|
||||
<string name="invalid_message_format">neplatný formát zprávy</string>
|
||||
<string name="live">LIVE</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_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="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ři příjmu souboru</string>
|
||||
<string name="error_creating_address">Chyba při vytváření adresy</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný.</string>
|
||||
<string name="connection_error_auth_desc">Pokud váš kontakt spojení nesmazal nebo tento odkaz již byl použit, může se jednat o chybu - nahlaste ji prosím. Chcete-li se připojit, požádejte prosím svůj kontakt o vytvoření jiného odkazu pro připojení a zkontrolujte, zda máte stabilní připojení k síti.</string>
|
||||
<string name="error_deleting_contact">Error deleting contact</string>
|
||||
<string name="error_deleting_group">Error deleting group</string>
|
||||
<string name="error_deleting_contact_request">Error deleting contact request</string>
|
||||
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
|
||||
<string name="error_smp_test_failed_at_step">Test failed at step %s.</string>
|
||||
<string name="error_smp_test_certificate">Possibly, certificate fingerprint in server address is incorrect</string>
|
||||
<string name="smp_server_test_connect">Connect</string>
|
||||
<string name="smp_server_test_disconnect">Disconnect</string>
|
||||
<string name="error_deleting_user">Error deleting user profile</string>
|
||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||
<string name="service_notifications_disabled">Instant notifications are disabled!</string>
|
||||
<string name="turning_off_service_and_periodic">Je aktivní optimalizace baterie, která vypíná službu na pozadí a pravidelné požadavky na nové zprávy. Můžete je znovu povolit prostřednictvím nastavení.</string>
|
||||
<string name="periodic_notifications_disabled">Pravidelná oznámení jsou vypnuta!</string>
|
||||
<string name="database_initialization_error_desc">Databáze nefunguje správně. Klepnutím na 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 spuštění, žádná služba na pozadí se nespustí</string>
|
||||
<string name="notification_display_mode_hidden_desc">Skrýt kontakt a 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. Př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_disable_simplex_lock">Vypnutí zámku SimpleX</string>
|
||||
<string name="auth_confirm_credential">Potvrďte své pověření</string>
|
||||
<string name="auth_unavailable">Ověřování není k dispozici</string>
|
||||
<string name="auth_stop_chat">Zastavení chatu</string>
|
||||
<string name="auth_open_chat_console">Otevřete konzolu chatu</string>
|
||||
<string name="message_delivery_error_title">Chyba doručení zprávy</string>
|
||||
<string name="message_delivery_error_desc">Tento kontakt s největší pravděpodobností smazal spojení s vámi.</string>
|
||||
<string name="save_verb">Uložit</string>
|
||||
<string name="reveal_verb">Odhalit</string>
|
||||
<string name="hide_verb">Skrýt</string>
|
||||
<string name="delete_message__question">Smazat zprávu\?</string>
|
||||
<string name="delete_message_mark_deleted_warning">Zpráva bude označena ke smazání. Příjemce (příjemci) bude moci tuto zprávu odhalit.</string>
|
||||
<string name="for_me_only">Smazat pro mě</string>
|
||||
<string name="icon_descr_edited">upraveno</string>
|
||||
<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="tap_to_start_new_chat">Klepnutím na zahájíte nový chat</string>
|
||||
<string name="chat_with_developers">Chatujte s vývojáři</string>
|
||||
<string name="you_have_no_chats">Nemáte žádné chaty</string>
|
||||
<string name="icon_descr_cancel_image_preview">Zrušit náhled obrázku</string>
|
||||
<string name="share_message">Sdílet zprávu…</string>
|
||||
<string name="share_image">Sdílet obrázek…</string>
|
||||
<string name="icon_descr_cancel_file_preview">Zrušit náhled souboru</string>
|
||||
<string name="images_limit_title">Příliš mnoho obrázků!</string>
|
||||
<string name="waiting_for_file">Čekání na soubor</string>
|
||||
<string name="notifications">Oznámení</string>
|
||||
<string name="delete_contact_question">Smazat kontakt\?</string>
|
||||
<string name="icon_descr_server_status_pending">Čeká na vyřízení</string>
|
||||
<string name="verify_security_code">Ověření bezpečnostního kódu</string>
|
||||
<string name="icon_descr_send_message">Odeslat zprávu</string>
|
||||
<string name="only_group_owners_can_enable_voice">Only group owners can enable voice messages.</string>
|
||||
<string name="send_live_message">Send live message</string>
|
||||
<string name="live_message">Live message!</string>
|
||||
<string name="connect_via_link_or_qr">Connect via link / QR code</string>
|
||||
<string name="thank_you_for_installing_simplex">Thank you for installing <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">You can <font color="#0088ff">connect to <xliff:g id="appNameFull">SimpleX Chat</xliff:g> developers to ask any questions and to receive updates</font>.</string>
|
||||
<string name="to_connect_via_link_title">To connect via link</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">If you received <xliff:g id="appName">SimpleX Chat</xliff:g> invitation link, you can open it in your browser:</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">If you choose to reject sender will NOT be notified.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobile: tap <b>Open in mobile app</b>, then tap <b>Connect</b> in the app.</string>
|
||||
<string name="reject_contact_button">Reject</string>
|
||||
<string name="clear_chat_button">Clear chat</string>
|
||||
<string name="clear_chat_menu_action">Clear</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. Toto 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ástupce 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="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>
|
||||
<string name="icon_descr_more_button">Více na</string>
|
||||
<string name="show_QR_code">Zobrazit QR kód</string>
|
||||
<string name="invalid_QR_code">Neplatný QR kód</string>
|
||||
<string name="this_QR_code_is_not_a_link">Tento QR kód není odkaz!</string>
|
||||
<string name="invalid_contact_link">Neplatný odkaz!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Tento odkaz není platným odkazem pro připojení!</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">If you cannot meet in person, you can <b>scan QR code in the video call</b>, or your contact can share an invitation link.</string>
|
||||
<string name="connect_via_link">Connect via link</string>
|
||||
<string name="connect_button">Connect</string>
|
||||
<string name="paste_button">Paste</string>
|
||||
<string name="this_string_is_not_a_connection_link">This string is not a connection link!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">You can also connect by clicking the link. If it opens in the browser, click <b>Open in mobile app</b> button.</string>
|
||||
<string name="is_not_verified">\"%s není ověřeno</string>
|
||||
<string name="how_to_use_simplex_chat">Jak ji používat</string>
|
||||
<string name="markdown_help">Nápověda k markdown</string>
|
||||
<string name="smp_servers_save">Uložit servery</string>
|
||||
<string name="markdown_in_messages">Markdown ve zprávách</string>
|
||||
<string name="smp_servers_test_servers">Testovací servery</string>
|
||||
<string name="smp_servers_preset_server">Přednastavený server</string>
|
||||
<string name="smp_servers_your_server">Váš server</string>
|
||||
<string name="smp_servers_your_server_address">Adresa vašeho serveru</string>
|
||||
<string name="smp_servers_use_server">Použít server</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Použít pro nová připojení</string>
|
||||
<string name="smp_servers_per_user">Servery pro nová připojení vašeho aktuálního profilu chatu.</string>
|
||||
<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="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>
|
||||
<string name="network_socks_toggle">Použít proxy server SOCKS (port 9050)</string>
|
||||
<string name="update_onion_hosts_settings_question">Aktualizovat nastavení hostitelů .onion\?</string>
|
||||
<string name="network_use_onion_hosts">Použít hostitele .onion</string>
|
||||
<string name="network_use_onion_hosts_prefer">Když je k dispozici</string>
|
||||
<string name="network_use_onion_hosts_required">Povinné</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Onion hosts se použijí, pokud jsou k dispozici.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Onion hostitelé nebudou použiti.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Cibuloví hostitelé budou použiti, pokud budou k dispozici.</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Pro připojení budou vyžadováni cibuloví hostitelé.</string>
|
||||
<string name="network_session_mode_transport_isolation">Izolace přenosu</string>
|
||||
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
|
||||
<string name="network_session_mode_entity_description">A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.
|
||||
\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
<string name="app_version_title">App version</string>
|
||||
<string name="app_version_name">App version: v%s</string>
|
||||
<string name="core_version">Core version: v%s</string>
|
||||
<string name="core_build_timestamp">Core built at: %s</string>
|
||||
<string name="delete_address__question">Delete address\?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">All your contacts will remain connected.</string>
|
||||
<string name="contact_requests">Contact requests</string>
|
||||
<string name="display_name__field">Display name:</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty. <xliff:g id="appName">SimpleX</xliff:g> servery váš profil nevidí.</string>
|
||||
<string name="save_preferences_question">Uložit předvolby\?</string>
|
||||
<string name="save_and_notify_contact">Uložit a upozornit kontakt</string>
|
||||
<string name="save_and_notify_contacts">Uložit a upozornit kontakty</string>
|
||||
<string name="you_control_your_chat">Chat máte pod kontrolou!</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Na serverech neukládáme žádné vaše kontakty ani zprávy (po doručení).</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</string>
|
||||
<string name="display_name">Zobrazované jméno</string>
|
||||
<string name="full_name_optional__prompt">Celé jméno (volitelné)</string>
|
||||
<string name="create_profile_button">Vytvořit</string>
|
||||
<string name="how_to_use_markdown">Jak používat markdown</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">K formátování zpráv můžete použít markdown:</string>
|
||||
<string name="italic">kurzíva</string>
|
||||
<string name="strikethrough">přeškrtnout</string>
|
||||
<string name="callstatus_missed">zmeškané volání</string>
|
||||
<string name="callstatus_rejected">odmítnutý hovor</string>
|
||||
<string name="callstatus_connecting">spojovací hovor…</string>
|
||||
<string name="callstatus_ended">hovor ukončen <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="callstate_starting">začíná…</string>
|
||||
<string name="callstate_waiting_for_answer">čeká na odpověď…</string>
|
||||
<string name="callstate_waiting_for_confirmation">čekání na potvrzení…</string>
|
||||
<string name="callstate_received_answer">obdržel odpověď…</string>
|
||||
<string name="callstate_received_confirmation">obdržel potvrzení…</string>
|
||||
<string name="callstate_connecting">připojení…</string>
|
||||
<string name="privacy_redefined">Nové vymezení soukromí</string>
|
||||
<string name="first_platform_without_user_ids">1. Platforma bez identifikátorů uživatelů - soukromá už od záměru.</string>
|
||||
<string name="immune_to_spam_and_abuse">Odolná vůči spamu a zneužití</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">To protect privacy, instead of user IDs used by all other platforms, <xliff:g id="appName">SimpleX</xliff:g> has identifiers for message queues, separate for each of your contacts.</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Mnoho lidí se ptalo: <i>if <xliff:g id="appName">SimpleX</xliff:g> has no user identifiers, how can it deliver messages\?</i></string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">You control through which server(s) <b>to receive</b> the messages, your contacts – the servers you use to message them.</string>
|
||||
<string name="read_more_in_github">Read more in our GitHub repository.</string>
|
||||
<string name="read_more_in_github_with_link">Read more in our <font color="#0088ff">GitHub repository</font>.</string>
|
||||
<string name="use_chat">Use chat</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">It can be changed later via settings.</string>
|
||||
<string name="onboarding_notifications_mode_off">When app is running</string>
|
||||
<string name="onboarding_notifications_mode_service">Instant</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Best for battery</b>. You will receive notifications only when the app is running, background service will NOT be used.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Good for battery</b>. Background service checks for new messages every 10 minutes. You may miss calls and urgent messages.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Využívá více baterie</b>! Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
|
||||
<string name="paste_the_link_you_received">Vložení přijatého odkazu</string>
|
||||
<string name="incoming_video_call">Příchozí videohovor</string>
|
||||
<string name="incoming_audio_call">Příchozí zvukový hovor</string>
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> se s vámi chce spojit prostřednictvím</string>
|
||||
<string name="video_call_no_encryption">videohovoru (nešifrovaného e2e).</string>
|
||||
<string name="audio_call_no_encryption">zvukový hovor (nešifrovaný 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 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="status_no_e2e_encryption">bez šifrování e2e</string>
|
||||
<string name="icon_descr_flip_camera">Flipová kamera</string>
|
||||
<string name="icon_descr_call_pending_sent">Probíhají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">Spojení hovoru</string>
|
||||
<string name="icon_descr_call_ended">Ukonč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ž: 1. Zprávy na serveru vyprší, pokud nebyly přijaty po dobu 30 dnů, 2. Server, který používáte pro příjem zpráv od tohoto kontaktu, byl aktualizován a restartován. 3. Spojení je narušeno. Připojte se k vývojářům prostřednictvím Nastavení, abyste mohli dostávat aktualizace o serverech. Budeme 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">PODPORA SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_develop">DEVELOP</string>
|
||||
<string name="settings_developer_tools">Nástroje pro vývojáře</string>
|
||||
<string name="settings_section_title_incognito">Režim inkognito</string>
|
||||
<string name="your_chat_database">Vaše chatovací 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="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="stop_chat_question">Zastavit chat\?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Zastavení chatu pro export, import nebo smazání databáze chatu. Během zastavení chatu nebudete moci přijímat a 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_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="import_database_question">Importovat databázi chatu\?</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Vaše aktuální databáze chatu bude Smazána a Nahrazena importovanou databází. Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</string>
|
||||
<string name="error_deleting_database">Chyba při mazání databáze chatu</string>
|
||||
<string name="error_importing_database">Chyba při importu databáze chatu</string>
|
||||
<string name="chat_database_deleted">Databáze chatu odstraněna</string>
|
||||
<string name="delete_files_and_media_for_all_users">Odstranění souborů pro všechny profily chatu</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>
|
||||
<string name="total_files_count_and_size">%d souborů s celkovou velikostí %s</string>
|
||||
<string name="chat_item_ttl_none">nikdy</string>
|
||||
<string name="chat_item_ttl_seconds">\"%s vteřin(y)</string>
|
||||
<string name="messages_section_title">Zprávy</string>
|
||||
<string name="messages_section_description">Toto nastavení se vztahuje na zprávy ve vašem aktuálním profilu chatu.</string>
|
||||
<string name="delete_messages_after">Smazat zprávy po</string>
|
||||
<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">Error changing setting</string>
|
||||
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
|
||||
<string name="database_encrypted">Database encrypted!</string>
|
||||
<string name="error_encrypting_database">Error encrypting database</string>
|
||||
<string name="encrypt_database">Encrypt</string>
|
||||
<string name="encrypted_with_random_passphrase">Database is encrypted using a random passphrase, you can change it.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">You have to enter passphrase every time the app starts - it is not stored on the device.</string>
|
||||
<string name="encrypt_database_question">Encrypt database\?</string>
|
||||
<string name="change_database_passphrase_question">Change database passphrase\?</string>
|
||||
<string name="database_will_be_encrypted">Databáze bude zašifrována.</string>
|
||||
<string name="database_encryption_will_be_updated">Šifrovací heslová fráze databáze bude aktualizována a uložena do úložiště klíčů.</string>
|
||||
<string name="database_passphrase_will_be_updated">Šifrovací heslová fráze databáze bude aktualizována.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Uložte prosím bezpečně přístupovou frázi, pokud ji ztratíte, NEBUDE možné přistupovat k chatu.</string>
|
||||
<string name="wrong_passphrase">Špatná přístupová fráze k databázi</string>
|
||||
<string name="encrypted_database">Zašifrovaná databáze</string>
|
||||
<string name="database_error">Chyba databáze</string>
|
||||
<string name="keychain_error">Chyba klíčenky</string>
|
||||
<string name="passphrase_is_different">Databázová heslová fráze se liší od uložené v klíčence.</string>
|
||||
<string name="cannot_access_keychain">Nelze získat přístup k úložišti klíčů pro uložení hesla 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>
|
||||
<string name="enter_passphrase">Zadejte přístupovou frázi…</string>
|
||||
<string name="database_backup_can_be_restored">Pokus o změnu přístupové fráze databáze nebyl dokončen.</string>
|
||||
<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="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="join_group_incognito_button">Připojte se inkognito</string>
|
||||
<string name="joining_group">Připojení 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>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Přestanete dostávat zprávy z této skupiny. Historie chatu bude zachována.</string>
|
||||
<string name="alert_title_group_invitation_expired">Platnost pozvánky vypršela!</string>
|
||||
<string name="alert_message_group_invitation_expired">Pozvánka do skupiny již není platná, byla odstraněna odesílatelem.</string>
|
||||
<string name="alert_message_no_group">Tato skupina již neexistuje.</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Pro tuto skupinu používáte inkognito profil - abyste zabránili sdílení svého hlavního profilu, není pozvání kontaktů povoleno.</string>
|
||||
<string name="you_sent_group_invitation">Odeslali jste pozvánku do skupiny</string>
|
||||
<string name="you_are_invited_to_group">Jste pozváni do skupiny</string>
|
||||
<string name="group_invitation_tap_to_join">Klepnutím se připojíte</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Klepnutím se připojíte inkognito</string>
|
||||
<string name="you_joined_this_group">Připojili jste se k této skupině</string>
|
||||
<string name="you_rejected_group_invitation">Odmítli jste pozvánku do skupiny</string>
|
||||
<string name="group_invitation_expired">Platnost pozvánky do skupiny vypršela</string>
|
||||
<string name="rcv_group_event_member_added">pozvánka <xliff:g id="profil člena" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_connected">připojeno</string>
|
||||
<string name="rcv_group_event_changed_member_role">změnil roli %s na %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">změnil svou roli na %s</string>
|
||||
<string name="rcv_group_event_member_deleted">odstraněno <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_user_deleted">odstranil vás</string>
|
||||
<string name="rcv_group_event_group_deleted">odstraněna skupina</string>
|
||||
<string name="rcv_group_event_updated_group_profile">aktualizoval profil skupiny</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">pozváni prostřednictvím odkazu na vaši skupinu</string>
|
||||
<string name="snd_group_event_group_profile_updated">profil skupiny aktualizován</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">změna adresy…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">jste změnili adresu pro %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">změna adresy pro %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">změnili jste adresu</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">změna adresy…</string>
|
||||
<string name="group_member_role_member">člen</string>
|
||||
<string name="group_member_role_owner">vlastník</string>
|
||||
<string name="group_member_status_removed">odstraněno</string>
|
||||
<string name="group_member_status_left">opustil</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_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_connected">connected</string>
|
||||
<string name="group_member_status_complete">kompletní</string>
|
||||
<string name="group_member_status_creator">tvůrce</string>
|
||||
<string name="group_member_status_connecting">connecting</string>
|
||||
<string name="no_contacts_to_add">Žádné kontakty k přidání</string>
|
||||
<string name="new_member_role">Nová role člena</string>
|
||||
<string name="invite_to_group_button">Pozvat do skupiny</string>
|
||||
<string name="skip_inviting_button">Přeskočit pozvání členů</string>
|
||||
<string name="select_contacts">Vybrat kontakty</string>
|
||||
<string name="icon_descr_contact_checked">Zkontrolované kontakty</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> kontakt(y) vybrán(y)</string>
|
||||
<string name="button_add_members">Pozvat členy</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBERS</string>
|
||||
<string name="group_info_member_you">vy: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<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="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="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 při vytváření odkazu skupiny</string>
|
||||
<string name="error_deleting_link_for_group">Chyba při odstraňování odkazu skupiny</string>
|
||||
<string name="only_group_owners_can_change_prefs">Předvolby skupiny mohou měnit pouze vlastníci skupiny.</string>
|
||||
<string name="section_title_for_console">PRO KONSOLE</string>
|
||||
<string name="info_row_local_name">Místní název</string>
|
||||
<string name="info_row_database_id">ID databáze</string>
|
||||
<string name="button_remove_member">Odstranit člena</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Člen bude odstraněn ze skupiny - toto nelze vzít zpět!</string>
|
||||
<string name="remove_member_confirmation">Odstranit</string>
|
||||
<string name="role_in_group">Role</string>
|
||||
<string name="change_role">Změnit roli</string>
|
||||
<string name="change_verb">Změnit</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na \"%s\". Člen obdrží novou pozvánku.</string>
|
||||
<string name="error_changing_role">Chyba při změně role</string>
|
||||
<string name="conn_level_desc_direct">přímo</string>
|
||||
<string name="sending_via">Odesílání přes</string>
|
||||
<string name="network_status">Stav sítě</string>
|
||||
<string name="switch_receiving_address">Přepínač přijímací adresy</string>
|
||||
<string name="group_is_decentralized">Skupina je plně decentralizovaná - je viditelná pouze pro členy.</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Zde není podporován režim inkognito - členům skupiny bude zaslán váš hlavní profil.</string>
|
||||
<string name="save_group_profile">Uložení profilu skupiny</string>
|
||||
<string name="network_options_reset_to_defaults">Obnovení výchozího nastavení</string>
|
||||
<string name="network_option_tcp_connection_timeout">Časový limit připojení TCP</string>
|
||||
<string name="network_option_protocol_timeout">Časový limit protokolu</string>
|
||||
<string name="network_option_ping_interval">Interval PING</string>
|
||||
<string name="network_option_ping_count">Počet PING</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Povolit TCP keep-alive</string>
|
||||
<string name="update_network_settings_confirmation">Aktualizovat</string>
|
||||
<string name="users_delete_question">Smazat profil chatu\?</string>
|
||||
<string name="users_delete_profile_for">Smazat profil chatu pro</string>
|
||||
<string name="users_delete_with_connections">Profil a připojení k serveru</string>
|
||||
<string name="users_delete_data_only">Pouze lokální profilová data</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ětlo</string>
|
||||
<string name="theme_dark">Tmavý</string>
|
||||
<string name="theme">Téma</string>
|
||||
<string name="chat_preferences_contact_allows">Kontakt povoluje</string>
|
||||
<string name="chat_preferences_on">na</string>
|
||||
<string name="chat_preferences_off">vypnuto</string>
|
||||
<string name="chat_preferences">Předvolby chatu</string>
|
||||
<string name="contact_preferences">Předvolby kontaktů</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="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">Mizící zprávy můžete odesílat pouze 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">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="ttl_month">\"%d měsíc</string>
|
||||
<string name="ttl_months">\"%d měsíců</string>
|
||||
<string name="ttl_day">\"%d den</string>
|
||||
<string name="ttl_days">\"%d dnů</string>
|
||||
<string name="ttl_week">\"%d týden</string>
|
||||
<string name="ttl_weeks">\"%d týdnů</string>
|
||||
<string name="ttl_w">\"%dw</string>
|
||||
<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="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_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>
|
||||
<string name="v4_5_reduced_battery_usage">Snížení spotřeby baterie</string>
|
||||
<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řispívejte 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="your_chat_profile_will_be_sent_to_your_contact">Váš profil v chatu bude odeslán vašemu kontaktu</string>
|
||||
<string name="your_chat_profiles_stored_locally">Vaše profily v chatu jsou uloženy lokálně, pouze ve vašem zařízení.</string>
|
||||
<string name="your_chats">Vaše konverzace</string>
|
||||
</resources>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -0,0 +1,956 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="connect_via_contact_link">Se connecter via le lien du contact \?</string>
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Votre profil va être envoyé au contact qui vous a envoyé ce lien.</string>
|
||||
<string name="you_will_join_group">Vous allez rejoindre le groupe correspondant à ce lien et être mis en relation avec les autres membres du groupe.</string>
|
||||
<string name="connect_via_link_verb">Se connecter</string>
|
||||
<string name="connect_via_group_link">Se connecter via le lien du groupe \?</string>
|
||||
<string name="connect_via_invitation_link">Se connecter via un lien d\'invitation \?</string>
|
||||
<string name="server_error">erreur</string>
|
||||
<string name="server_connecting">connexion</string>
|
||||
<string name="server_connected">connecté</string>
|
||||
<string name="display_name_connection_established">connexion établie</string>
|
||||
<string name="display_name_invited_to_connect">invité à se connecter</string>
|
||||
<string name="simplex_link_invitation">Invitation unique SimpleX</string>
|
||||
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
<string name="simplex_link_mode">Liens SimpleX</string>
|
||||
<string name="simplex_link_mode_description">Description</string>
|
||||
<string name="error_deleting_contact">Erreur lors de la suppression du contact</string>
|
||||
<string name="error_joining_group">Erreur lors de la liaison avec le groupe</string>
|
||||
<string name="sender_cancelled_file_transfer">L\'expéditeur a annulé le transfert de fichiers.</string>
|
||||
<string name="deleted_description">supprimé</string>
|
||||
<string name="marked_deleted_description">supprimé</string>
|
||||
<string name="unknown_message_format">format de message inconnu</string>
|
||||
<string name="display_name_connecting">connexion…</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">vous avez partagé un lien unique en incognito</string>
|
||||
<string name="description_via_group_link">via le lien de groupe</string>
|
||||
<string name="description_via_contact_address_link">via le lien d\'adresse du contact</string>
|
||||
<string name="description_via_contact_address_link_incognito">mode incognito via le lien d\'adresse du contact</string>
|
||||
<string name="simplex_link_group">Lien de groupe SimpleX</string>
|
||||
<string name="simplex_link_mode_browser">Via navigateur</string>
|
||||
<string name="simplex_link_mode_browser_warning">Ouvrir le lien dans le navigateur peut réduire la confidentialité et la sécurité de la connexion. Les liens SimpleX non fiables seront en rouge.</string>
|
||||
<string name="network_error_desc">Vérifiez votre connexion réseau avec <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> et réessayez.</string>
|
||||
<string name="error_receiving_file">Erreur lors de la réception du fichier</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">L\'expéditeur a peut-être supprimé la demande de connexion.</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Vous êtes connecté·e au serveur utilisé pour recevoir les messages de ce contact.</string>
|
||||
<string name="sending_files_not_yet_supported">l\'envoi de fichiers n\'est pas encore supporté</string>
|
||||
<string name="sender_you_pronoun">vous</string>
|
||||
<string name="description_via_group_link_incognito">mode incognito via le lien de groupe</string>
|
||||
<string name="simplex_link_contact">Adresse de contact SimpleX</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact.</string>
|
||||
<string name="receiving_files_not_yet_supported">la réception de fichiers n\'est pas encore supportée</string>
|
||||
<string name="connection_local_display_name">connexion <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="description_you_shared_one_time_link">vous avez partagé un lien unique</string>
|
||||
<string name="description_via_one_time_link">via un lien unique</string>
|
||||
<string name="description_via_one_time_link_incognito">mode incognito via un lien unique</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs SMP sont au bon format, séparées par des lignes et ne sont pas dupliquées.</string>
|
||||
<string name="error_setting_network_config">Erreur lors de la mise à jour de la configuration réseau</string>
|
||||
<string name="error_creating_address">Erreur lors de la création de l\'adresse</string>
|
||||
<string name="contact_already_exists">Contact déjà existant</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre.</string>
|
||||
<string name="connection_error">Erreur de connexion</string>
|
||||
<string name="error_adding_members">Erreur lors de l\'ajout de membre·s</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="invalid_message_format">format de message invalide</string>
|
||||
<string name="simplex_link_mode_full">Lien entier</string>
|
||||
<string name="error_saving_smp_servers">Erreur lors de la sauvegarde des serveurs SMP</string>
|
||||
<string name="cannot_receive_file">Impossible de recevoir le fichier</string>
|
||||
<string name="invalid_connection_link">Lien de connection invalide</string>
|
||||
<string name="connection_timeout">Délai de connexion</string>
|
||||
<string name="error_sending_message">Erreur lors de l\'envoi du message</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Vous êtes déjà connecté à <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
|
||||
<string name="connection_error_auth">Erreur de connexion (AUTH)</string>
|
||||
<string name="connection_error_auth_desc">A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s\'agir d\'un bug - veuillez le signaler.
|
||||
\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d\'une connexion réseau stable.</string>
|
||||
<string name="error_accepting_contact_request">Erreur de validation de la demande de contact</string>
|
||||
<string name="error_deleting_group">Erreur lors de la suppression du groupe</string>
|
||||
<string name="error_deleting_contact_request">Erreur lors de la suppression du contact</string>
|
||||
<string name="error_deleting_pending_contact_connection">Erreur lors de la suppression de la connexion en attente</string>
|
||||
<string name="error_changing_address">Erreur de changement d\'adresse</string>
|
||||
<string name="error_smp_test_failed_at_step">Échec du test à l\'étape %s.</string>
|
||||
<string name="error_smp_test_certificate">Il est possible que l\'empreinte du certificat dans l\'adresse du serveur soit incorrecte</string>
|
||||
<string name="smp_server_test_connect">Se connecter</string>
|
||||
<string name="smp_server_test_create_queue">Créer une file d\'attente</string>
|
||||
<string name="smp_server_test_secure_queue">File d\'attente sécurisée</string>
|
||||
<string name="smp_server_test_delete_queue">Supprimer la file d\'attente</string>
|
||||
<string name="smp_server_test_disconnect">Se déconnecter</string>
|
||||
<string name="icon_descr_instant_notifications">Notifications instantanées</string>
|
||||
<string name="service_notifications">Notifications instantanées !</string>
|
||||
<string name="service_notifications_disabled">Les notifications instantanées sont désactivées !</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Il peut être désactivé via les paramètres</b> - les notifications seront toujours affichées lorsque l\'application est en cours d\'exécution.</string>
|
||||
<string name="turning_off_service_and_periodic">L\'optimisation de la batterie est active et désactive le service de fond et les demandes périodiques de nouveaux messages. Vous pouvez les réactiver via les paramètres.</string>
|
||||
<string name="periodic_notifications">Notifications périodiques</string>
|
||||
<string name="periodic_notifications_disabled">Les notifications périodiques sont désactivées !</string>
|
||||
<string name="enter_passphrase_notification_title">Une phrase secrète est nécessaire</string>
|
||||
<string name="turn_off_battery_optimization">Pour l\'utiliser, veuillez <b>désactiver l\'optimisation de la batterie</b> pour <xliff:g id="appName">SimpleX</xliff:g> dans la prochaine fenêtre de dialogue. Sinon, les notifications seront désactivées.</string>
|
||||
<string name="error_smp_test_server_auth">Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe</string>
|
||||
<string name="periodic_notifications_desc">L\'application récupère périodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les données de votre appareil ne sont pas envoyées aux serveurs.</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Pour protéger votre vie privée, au lieu des notifications push, l\'application possède un <b><xliff:g id="appName">SimpleX</xliff:g> service de fond</b> - il utilise quelques pour cent de la batterie par jour.</string>
|
||||
<string name="hide_notification">Cacher</string>
|
||||
<string name="settings_notification_preview_mode_title">Montrer l\'aperçu</string>
|
||||
<string name="notification_preview_mode_contact">Nom du contact</string>
|
||||
<string name="notification_preview_somebody">Contact masqué :</string>
|
||||
<string name="notification_preview_new_message">nouveau message</string>
|
||||
<string name="notification_new_contact_request">Nouvelle demande de contact</string>
|
||||
<string name="notification_contact_connected">Connecté</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
|
||||
<string name="la_notice_turn_on">Activer</string>
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX Lock activé</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Il vous sera demandé de vous authentifier lorsque vous démarrez ou reprenez l\'application après 30 secondes en arrière-plan.</string>
|
||||
<string name="auth_unlock">Déverrouiller</string>
|
||||
<string name="auth_enable_simplex_lock">Activer SimpleX Lock</string>
|
||||
<string name="auth_disable_simplex_lock">Désactiver SimpleX Lock</string>
|
||||
<string name="auth_unavailable">Authentification indisponible</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">L\'authentification de l\'appareil est désactivée. Désactivation de SimpleX Lock.</string>
|
||||
<string name="auth_open_chat_console">Ouvrir la console du chat</string>
|
||||
<string name="message_delivery_error_title">Erreur de distribution du message</string>
|
||||
<string name="message_delivery_error_desc">Il est fort probable que ce contact ait supprimé la connexion avec vous.</string>
|
||||
<string name="reply_verb">Répondre</string>
|
||||
<string name="share_verb">Partager</string>
|
||||
<string name="copy_verb">Copier</string>
|
||||
<string name="delete_verb">Supprimer</string>
|
||||
<string name="save_verb">Sauvegarder</string>
|
||||
<string name="edit_verb">Modifier</string>
|
||||
<string name="reveal_verb">Révéler</string>
|
||||
<string name="hide_verb">Cacher</string>
|
||||
<string name="allow_verb">Autoriser</string>
|
||||
<string name="delete_message__question">Supprimer le message \?</string>
|
||||
<string name="for_me_only">Supprimer pour moi</string>
|
||||
<string name="your_chats">Vos chats</string>
|
||||
<string name="notification_preview_mode_message">Texte du message</string>
|
||||
<string name="notification_preview_mode_hidden">Caché</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">Pour protéger vos informations, activez la fonction SimpleX Lock.
|
||||
\nVous serez invité à confirmer l\'authentification avant que cette fonction ne soit activée.</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">L\'authentification de l\'appareil n\'est pas activée. Vous pouvez activer SimpleX Lock via Paramètres, une fois que vous avez activé l\'authentification de l\'appareil.</string>
|
||||
<string name="database_initialization_error_desc">La base de données ne fonctionne pas correctement. Appuyez ici pour en savoir plus.</string>
|
||||
<string name="ntf_channel_calls">Appels SimpleX Chat</string>
|
||||
<string name="ntf_channel_messages">Messages SimpleX Chat</string>
|
||||
<string name="settings_notifications_mode_title">Service de notification</string>
|
||||
<string name="notifications_mode_periodic">Lancer périodiquement</string>
|
||||
<string name="notifications_mode_off">Exécuter lorsque l’app est ouverte</string>
|
||||
<string name="notifications_mode_service">Toujours activé</string>
|
||||
<string name="failed_to_parse_chat_title">Échec du chargement du chat</string>
|
||||
<string name="failed_to_parse_chats_title">Échec du chargement des chats</string>
|
||||
<string name="contact_developers">Veuillez mettre à jour l’app et contacter les développeurs.</string>
|
||||
<string name="simplex_service_notification_text">Récupération des messages…</string>
|
||||
<string name="settings_notification_preview_title">Aperçu de notification</string>
|
||||
<string name="database_initialization_error_title">Échec d’initialisation de la base de données</string>
|
||||
<string name="enter_passphrase_notification_desc">Pour recevoir des notifications, veuillez entrer la phrase secrète de la base de données</string>
|
||||
<string name="simplex_service_notification_title">service <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="this_text_is_available_in_settings">Ce texte est disponible dans les paramètres</string>
|
||||
<string name="group_preview_join_as">rejoindre en tant que %s</string>
|
||||
<string name="group_preview_you_are_invited">vous êtes invité·e au groupe</string>
|
||||
<string name="chat_with_developers">Discuter avec les développeurs</string>
|
||||
<string name="tap_to_start_new_chat">Appuyez ici pour démarrer une nouvelle discussion</string>
|
||||
<string name="you_have_no_chats">Vous n\'avez aucune discussion</string>
|
||||
<string name="images_limit_title">Trop d’images !</string>
|
||||
<string name="share_file">Partager le fichier…</string>
|
||||
<string name="attach">Attacher</string>
|
||||
<string name="icon_descr_cancel_image_preview">Annuler l’aperçu d’image</string>
|
||||
<string name="icon_descr_cancel_file_preview">Annuler l’aperçu du fichier</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">échec d’envoi</string>
|
||||
<string name="icon_descr_received_msg_status_unread">non lu</string>
|
||||
<string name="welcome">Bienvenue !</string>
|
||||
<string name="contact_connection_pending">connexion…</string>
|
||||
<string name="group_connection_pending">connexion…</string>
|
||||
<string name="share_message">Partager le message…</string>
|
||||
<string name="share_image">Partager l’image…</string>
|
||||
<string name="images_limit_desc">Envoi de 10 images en même temps maximum</string>
|
||||
<string name="personal_welcome">Bienvenue <xliff:g>%1$s</xliff:g> !</string>
|
||||
<string name="notifications_mode_periodic_desc">Vérifie les nouveaux messages toutes les 10 minutes pendant 1 minute au maximum.</string>
|
||||
<string name="notification_preview_mode_contact_desc">Afficher uniquement le contact</string>
|
||||
<string name="for_everybody">Pour tous</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">envoyé</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">envoi non autorisé</string>
|
||||
<string name="icon_descr_context">Icône contextuelle</string>
|
||||
<string name="image_descr">Image</string>
|
||||
<string name="image_decoding_exception_desc">L\'image ne peut pas être décodée. Veuillez essayer une autre image ou contacter les développeurs.</string>
|
||||
<string name="icon_descr_waiting_for_image">En attente de l\'image</string>
|
||||
<string name="icon_descr_asked_to_receive">Demandé à recevoir l\'image</string>
|
||||
<string name="icon_descr_image_snd_complete">Image envoyée</string>
|
||||
<string name="waiting_for_image">En attente de l\'image</string>
|
||||
<string name="image_saved">Image enregistrée dans la phototèque</string>
|
||||
<string name="icon_descr_file">Fichier</string>
|
||||
<string name="large_file">Fichier trop lourd !</string>
|
||||
<string name="file_saved">Fichier sauvegardé</string>
|
||||
<string name="file_not_found">Fichier introuvable</string>
|
||||
<string name="error_saving_file">Erreur lors de la sauvegarde du fichier</string>
|
||||
<string name="delete_contact_question">Supprimer le contact \?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Le contact et tous les messages seront supprimés - impossible de revenir en arrière !</string>
|
||||
<string name="button_delete_contact">Supprimer le contact</string>
|
||||
<string name="icon_descr_server_status_connected">Connecté</string>
|
||||
<string name="icon_descr_send_message">Envoyer un message</string>
|
||||
<string name="switch_receiving_address_question">Changement d\'adresse de réception \?</string>
|
||||
<string name="icon_descr_record_voice_message">Enregistrer un message vocal</string>
|
||||
<string name="allow_voice_messages_question">Autoriser les messages vocaux \?</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Vous devez autoriser votre contact à envoyer des messages vocaux pour pouvoir en envoyer.</string>
|
||||
<string name="voice_messages_prohibited">Messages vocaux interdits !</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Veuillez demander à votre contact de permettre l\'envoi de messages vocaux.</string>
|
||||
<string name="cancel_verb">Annuler</string>
|
||||
<string name="only_group_owners_can_enable_voice">Seuls les propriétaires de groupes peuvent activer les messages vocaux.</string>
|
||||
<string name="back">Retour</string>
|
||||
<string name="no_details">aucun détail</string>
|
||||
<string name="add_contact">Lien d\'invitation unique</string>
|
||||
<string name="copied">Copié dans le presse-papiers</string>
|
||||
<string name="share_one_time_link">Créer un lien d\'invitation unique</string>
|
||||
<string name="add_contact_or_create_group">Commencer une nouvelle discussion</string>
|
||||
<string name="connect_via_link_or_qr">Se connecter via un lien / code QR</string>
|
||||
<string name="scan_QR_code">Scanner un code QR</string>
|
||||
<string name="to_share_with_your_contact">(à partager avec votre contact)</string>
|
||||
<string name="create_group">Créer un groupe secret</string>
|
||||
<string name="from_gallery_button">Depuis la Phototèque</string>
|
||||
<string name="choose_file">Choisir le fichier</string>
|
||||
<string name="to_start_a_new_chat_help_header">Pour démarrer une nouvelle discussion</string>
|
||||
<string name="chat_help_tap_button">Appuyez sur le bouton</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scanner un code QR</b> : pour vous connecter à votre contact qui vous montre un code QR.</string>
|
||||
<string name="to_connect_via_link_title">Pour se connecter via un lien</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si vous avez reçu un lien d\'invitation <xliff:g id="appName">SimpleX Chat</xliff:g>, vous pouvez l\'ouvrir dans votre navigateur :</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 bureau : scanner le code QR affiché depuis l\'app, via <b>Scanner le code QR</b>.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobile : appuyez sur <b>Ouvrir dans l\'application</b>, puis appuyez sur <b>se connecter</b> dans l\'app.</string>
|
||||
<string name="accept_contact_incognito_button">Accepter en incognito</string>
|
||||
<string name="reject_contact_button">Rejeter</string>
|
||||
<string name="clear_chat_question">Effacer la conversation \?</string>
|
||||
<string name="clear_chat_warning">Tous les messages seront supprimés - impossible de revenir en arrière ! Les messages seront supprimés UNIQUEMENT pour vous.</string>
|
||||
<string name="clear_chat_menu_action">Effacer</string>
|
||||
<string name="delete_contact_menu_action">Supprimer</string>
|
||||
<string name="delete_group_menu_action">Supprimer</string>
|
||||
<string name="mark_read">Marquer comme lu</string>
|
||||
<string name="mark_unread">Marquer non lu</string>
|
||||
<string name="set_contact_name">Définir le nom du contact</string>
|
||||
<string name="you_invited_your_contact">Vous avez invité votre contact</string>
|
||||
<string name="you_accepted_connection">Vous avez accepté la connexion</string>
|
||||
<string name="delete_pending_connection__question">Supprimer la connexion en attente \?</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">La connexion que vous avez acceptée sera annulée !</string>
|
||||
<string name="alert_title_contact_connection_pending">Le contact n\'est pas encore connecté !</string>
|
||||
<string name="icon_descr_close_button">Bouton fermer</string>
|
||||
<string name="image_descr_profile_image">image de profil</string>
|
||||
<string name="image_descr_link_preview">image d\'aperçu du lien</string>
|
||||
<string name="icon_descr_cancel_link_preview">annuler l\'aperçu du lien</string>
|
||||
<string name="icon_descr_settings">Paramètres</string>
|
||||
<string name="icon_descr_address">Adresse <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="icon_descr_help">aide</string>
|
||||
<string name="icon_descr_simplex_team">Équipe <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="icon_descr_more_button">Plus</string>
|
||||
<string name="show_QR_code">Afficher le code QR</string>
|
||||
<string name="invalid_QR_code">Code QR invalide</string>
|
||||
<string name="this_QR_code_is_not_a_link">Ce code QR n\'est pas un lien !</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Vous serez connecté·e lorsque votre demande de connexion sera acceptée, veuillez attendre ou vérifier plus tard !</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Vous serez connecté·e lorsque l\'appareil de votre contact sera en ligne, veuillez attendre ou vérifier plus tard !</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Votre contact peut scanner le code QR depuis l\'app.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Votre profil de chat sera envoyé
|
||||
\nà votre contact</string>
|
||||
<string name="share_invitation_link">Partager le lien d\'invitation</string>
|
||||
<string name="your_profile_will_be_sent">Votre profil de chat sera envoyé à votre contact</string>
|
||||
<string name="paste_button">Coller</string>
|
||||
<string name="this_string_is_not_a_connection_link">Cette chaîne n\'est pas un lien de connexion !</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">Vous pouvez aussi vous connecter en cliquant sur le lien. Si il s\'ouvre dans le navigateur, cliquez sur <b>Ouvrir dans l\'app mobile</b>.</string>
|
||||
<string name="create_one_time_link">Créer un lien d\'invitation unique</string>
|
||||
<string name="text_field_set_contact_placeholder">Définir le nom du contact…</string>
|
||||
<string name="icon_descr_server_status_disconnected">Déconnecté</string>
|
||||
<string name="icon_descr_server_status_error">Erreur</string>
|
||||
<string name="icon_descr_server_status_pending">En attente</string>
|
||||
<string name="accept_connection_request__question">Accepter la demande de connexion \?</string>
|
||||
<string name="clear_verb">Effacer</string>
|
||||
<string name="clear_chat_button">Effacer la conversation</string>
|
||||
<string name="paste_connection_link_below_to_connect">Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact.</string>
|
||||
<string name="connect_via_link">Se connecter via un lien</string>
|
||||
<string name="clear_verification">Retirer la vérification</string>
|
||||
<string name="one_time_link">Lien d\'invitation unique</string>
|
||||
<string name="your_contact_address">Votre adresse de contact</string>
|
||||
<string name="scan_code">Scanner le code</string>
|
||||
<string name="incorrect_code">Code de sécurité incorrect !</string>
|
||||
<string name="security_code">Code de sécurité</string>
|
||||
<string name="mark_code_verified">Marquer comme vérifié</string>
|
||||
<string name="view_security_code">Afficher le code de sécurité</string>
|
||||
<string name="verify_security_code">Vérifier le code de sécurité</string>
|
||||
<string name="confirm_verb">Confirmer</string>
|
||||
<string name="reset_verb">Réinitialisation</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scanner ou coller depuis le presse-papiers)</string>
|
||||
<string name="only_stored_on_members_devices">(uniquement stocké par les membres du groupe)</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Votre contact a besoin d\'être en ligne pour completer la connexion.
|
||||
\nVous pouvez annuler la connexion et supprimer le contact (et réessayer plus tard avec un autre lien).</string>
|
||||
<string name="contact_wants_to_connect_with_you">veut établir une connexion !</string>
|
||||
<string name="icon_descr_profile_image_placeholder">image de profil (placeholder)</string>
|
||||
<string name="image_descr_qr_code">Code QR</string>
|
||||
<string name="image_descr_simplex_logo">Logo <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="icon_descr_email">E-mail</string>
|
||||
<string name="connect_button">Se connecter</string>
|
||||
<string name="notifications_mode_off_desc">L\'application peut recevoir des notifications uniquement lorsqu\'elle est en cours d\'exécution, aucun service d\'arrière-plan ne sera lancé.</string>
|
||||
<string name="notifications_mode_service_desc">Le service d\'arrière-plan fonctionne en permanence. Les notifications s\'affichent dès que les messages sont disponibles.</string>
|
||||
<string name="notification_preview_mode_message_desc">Afficher le contact et le message</string>
|
||||
<string name="notification_display_mode_hidden_desc">Masquer le contact et le message</string>
|
||||
<string name="auth_log_in_using_credential">Connectez-vous en utilisant votre identifiant</string>
|
||||
<string name="auth_confirm_credential">Confirmez vos identifiants</string>
|
||||
<string name="auth_stop_chat">Arrêter le chat</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Le message sera supprimé - impossible de revenir en arrière !</string>
|
||||
<string name="delete_message_mark_deleted_warning">Le message sera marqué comme supprimé. Le·s destinataire·s pourrai·ent révéler ce message.</string>
|
||||
<string name="icon_descr_edited">modifié</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">L\'image sera reçue quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tard !</string>
|
||||
<string name="image_decoding_exception_title">Erreur de décodage</string>
|
||||
<string name="contact_sent_large_file">Votre contact a envoyé un fichier dont la taille est supérieure à la taille maximale actuellement prise en charge (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="waiting_for_file">En attente du fichier</string>
|
||||
<string name="voice_message">Message vocal</string>
|
||||
<string name="toast_permission_denied">Autorisation refusée !</string>
|
||||
<string name="use_camera_button">Utiliser l\'Appareil photo</string>
|
||||
<string name="thank_you_for_installing_simplex">Merci d\'avoir installé <xliff:g id="appNameFull">SimpleX Chat</xliff:g> !</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Vous pouvez <font color="#0088ff">vous connecter aux développeurs de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour leur poser des questions et recevoir des réponses :</font>.</string>
|
||||
<string name="above_then_preposition_continuation">ci-dessus, puis :</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Ajouter un nouveau contact</b> : afin de créer un code QR à usage unique pour votre contact.</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si vous choisissez de la rejeter, l\'expéditeur·rice NE sera PAS notifié·e.</string>
|
||||
<string name="accept_contact_button">Accepter</string>
|
||||
<string name="mute_chat">Muet</string>
|
||||
<string name="unmute_chat">Démute</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Le contact avec lequel vous avez partagé ce lien NE pourra PAS se connecter !</string>
|
||||
<string name="invalid_contact_link">Lien invalide !</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Ce lien n\'est pas un lien de connexion valide !</string>
|
||||
<string name="connection_request_sent">Demande de connexion envoyée !</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Le fichier sera reçu quand votre contact sera en ligne, merci d\'attendre ou de revenir plus tard !</string>
|
||||
<string name="voice_message_send_text">Message vocal…</string>
|
||||
<string name="maximum_supported_file_size">La taille maximale supportés des fichiers actuellement est de <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="voice_message_with_duration">Message vocal (<xliff:g id="duration">%1$s</xliff:g>)</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
<string name="switch_receiving_address_desc">Cette fonctionnalité est expérimentale ! Elle ne fonctionnera que si l\'autre client a la version 4.2 installée. Vous devriez voir le message dans la conversation une fois le changement d\'adresse effectué. Vérifiez que vous pouvez toujours recevoir des messages de ce contact (ou membre du groupe).</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Vous serez connecté·e au groupe lorsque l\'appareil de l\'hôte sera en ligne, veuillez attendre ou vérifier plus tard !</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Si vous ne pouvez pas vous rencontrer en personne, <b>montrez le code QR lors d\'un appel vidéo</b>, ou partagez le lien.</string>
|
||||
<string name="scan_code_from_contacts_app">Scannez le code de sécurité depuis l\'application de votre contact.</string>
|
||||
<string name="to_verify_compare">Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils.</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Si vous ne pouvez pas vous rencontrer en personne, vous pouvez <b>scanner un code QR lors d\'un appel vidéo</b>, ou votre contact peut partager un lien d\'invitation.</string>
|
||||
<string name="smp_servers_add">Ajouter un serveur…</string>
|
||||
<string name="markdown_in_messages">Markdown dans les messages</string>
|
||||
<string name="smp_servers_preset_add">Ajouter des serveurs prédéfinis</string>
|
||||
<string name="use_simplex_chat_servers__question">Utiliser les serveurs <xliff:g id="appNameFull">SimpleX Chat</xliff:g> \?</string>
|
||||
<string name="smp_servers_delete_server">Supprimer le serveur</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.</string>
|
||||
<string name="network_enable_socks_info">Accéder aux serveurs via un proxy SOCKS sur le port 9050 \? Le proxy doit être démarré avant d\'activer cette option.</string>
|
||||
<string name="network_use_onion_hosts">Utiliser les hôtes .onions</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Les hôtes .onion seront nécessaires pour la connexion.</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Vous contrôlez par quel·s serveur·s vous pouvez <b>transmettre</b> ainsi que par quel·s serveur·s vous pouvez <b>recevoir</b> les messages de vos contacts.</string>
|
||||
<string name="your_settings">Vos paramètres</string>
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_console">Console du chat</string>
|
||||
<string name="smp_servers">Serveurs SMP</string>
|
||||
<string name="smp_servers_test_servers">Tester les serveurs</string>
|
||||
<string name="smp_servers_save">Sauvegarder les serveurs</string>
|
||||
<string name="smp_servers_scan_qr">Scanner un code QR de serveur</string>
|
||||
<string name="smp_servers_use_server">Utiliser ce serveur</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Utiliser pour les nouvelles connexions</string>
|
||||
<string name="smp_servers_add_to_another_device">Ajouter à un autre appareil</string>
|
||||
<string name="install_simplex_chat_for_terminal">Installer <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour terminal</string>
|
||||
<string name="star_on_github">Star sur GitHub</string>
|
||||
<string name="contribute">Contribuer</string>
|
||||
<string name="rate_the_app">Évaluer l\'app</string>
|
||||
<string name="your_SMP_servers">Vos serveurs SMP</string>
|
||||
<string name="how_to_use_your_servers">Comment utiliser vos serveurs</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Les serveurs WebRTC ICE sauvegardés seront supprimés.</string>
|
||||
<string name="your_ICE_servers">Vos serveurs ICE</string>
|
||||
<string name="configure_ICE_servers">Configurer les serveurs ICE</string>
|
||||
<string name="network_settings">Paramètres réseau avancés</string>
|
||||
<string name="network_settings_title">Paramètres réseau</string>
|
||||
<string name="network_socks_toggle">Utiliser un proxy SOCKS (port 9050)</string>
|
||||
<string name="network_enable_socks">Utiliser un proxy SOCKS \?</string>
|
||||
<string name="network_disable_socks">Utiliser une connexion Internet directe \?</string>
|
||||
<string name="network_disable_socks_info">Si vous confirmez, les serveurs de messagerie seront en mesure de voir votre adresse IP, votre fournisseur ainsi que les serveurs auxquels vous vous connectez.</string>
|
||||
<string name="network_use_onion_hosts_no">Non</string>
|
||||
<string name="network_use_onion_hosts_required">Requis</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
|
||||
<string name="appearance_settings">Apparence</string>
|
||||
<string name="create_address">Créer une adresse</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Vous pouvez partager votre adresse sous forme de lien ou de code QR - n\'importe qui pourra se connecter à vous. Vous ne perdrez pas vos contacts si vous la supprimez par la suite.</string>
|
||||
<string name="your_current_profile">Votre profil de chat</string>
|
||||
<string name="edit_image">Modifier l\'image</string>
|
||||
<string name="save_and_notify_contacts">Sauvegarder et notifier les contacts</string>
|
||||
<string name="save_and_notify_group_members">Sauvegarder et en informer les membres du groupe</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Le profil n\'est partagé qu\'avec vos contacts.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Le nom d\'affichage ne peut pas contenir d\'espace.</string>
|
||||
<string name="full_name_optional__prompt">Nom complet (optionnel)</string>
|
||||
<string name="create_profile_button">Créer</string>
|
||||
<string name="about_simplex">À propos de SimpleX</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Vous pouvez utiliser le format markdown pour mettre en forme les messages :</string>
|
||||
<string name="bold">gras</string>
|
||||
<string name="italic">italique</string>
|
||||
<string name="strikethrough">barré</string>
|
||||
<string name="callstatus_accepted">appel accepté</string>
|
||||
<string name="callstatus_connecting">connexion à l\'appel…</string>
|
||||
<string name="callstatus_error">erreur d\'appel</string>
|
||||
<string name="callstate_received_answer">réponse reçu…</string>
|
||||
<string name="callstate_received_confirmation">confimation reçu…</string>
|
||||
<string name="callstate_connecting">connexion…</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocole et code open-source – n\'importe qui peut heberger un serveur.</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Pour protéger votre vie privée, au lieu d\'IDs utilisés par toutes les autres plateformes, <xliff:g id="appName">SimpleX</xliff:g> possède des IDs pour les queues de messages, distinctes pour chacun de vos contacts.</string>
|
||||
<string name="read_more_in_github">Plus d\'informations sur notre GitHub.</string>
|
||||
<string name="paste_the_link_you_received">Coller le lien reçu</string>
|
||||
<string name="use_chat">Utiliser le chat</string>
|
||||
<string name="onboarding_notifications_mode_title">Notifications privées</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Peut être modifié ultérieurement via les paramètres.</string>
|
||||
<string name="onboarding_notifications_mode_off">Quand l\'application fonctionne</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Périodique</string>
|
||||
<string name="onboarding_notifications_mode_service">Instantanée</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Économie de batterie</b>. Vous recevrez des notifications uniquement lorsque l\'application est en cours d\'exécution, le service de fond ne sera PAS utilisé.</string>
|
||||
<string name="about_simplex_chat">À propos de <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="how_to_use_simplex_chat">Comment l\'utiliser</string>
|
||||
<string name="markdown_help">Aide Markdown</string>
|
||||
<string name="save_servers_button">Sauvegarder</string>
|
||||
<string name="network_and_servers">Réseau et serveurs</string>
|
||||
<string name="save_and_notify_contact">Sauvegarder et en informer les contacts</string>
|
||||
<string name="exit_without_saving">Quitter sans sauvegarder</string>
|
||||
<string name="callstatus_rejected">appel rejeté</string>
|
||||
<string name="callstatus_in_progress">appel en cours</string>
|
||||
<string name="callstatus_ended">appel terminé <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="callstate_starting">lancement…</string>
|
||||
<string name="is_verified">%s est vérifié·e</string>
|
||||
<string name="is_not_verified">%s n\'est pas vérifié·e</string>
|
||||
<string name="your_simplex_contact_address">Votre adresse de contact <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="database_passphrase_and_export">Phrase secrète et exportation de la base de données</string>
|
||||
<string name="chat_with_the_founder">Envoyez vos questions et idées</string>
|
||||
<string name="send_us_an_email">Envoyez nous un e-mail</string>
|
||||
<string name="smp_servers_preset_address">Adresse du serveur prédéfinie</string>
|
||||
<string name="smp_servers_test_server">Tester le serveur</string>
|
||||
<string name="smp_servers_test_failed">Échec du test du serveur !</string>
|
||||
<string name="smp_servers_test_some_failed">Certains serveurs n\'ont pas réussi le test :</string>
|
||||
<string name="smp_servers_enter_manually">Entrer un serveur manuellement</string>
|
||||
<string name="smp_servers_preset_server">Serveur prédéfini</string>
|
||||
<string name="smp_servers_your_server">Votre serveur</string>
|
||||
<string name="smp_servers_your_server_address">Votre adresse de serveur</string>
|
||||
<string name="smp_servers_invalid_address">Adresse de serveur invalide !</string>
|
||||
<string name="smp_servers_check_address">Vérifiez l\'adresse du serveur et réessayez.</string>
|
||||
<string name="using_simplex_chat_servers">Utilise les serveurs <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
|
||||
<string name="how_to">Comment faire</string>
|
||||
<string name="enter_one_ICE_server_per_line">Serveurs ICE (un par ligne)</string>
|
||||
<string name="error_saving_ICE_servers">Erreur lors de la sauvegarde des serveurs ICE</string>
|
||||
<string name="update_onion_hosts_settings_question">Mettre à jour le paramètre des hôtes .onion \?</string>
|
||||
<string name="network_use_onion_hosts_prefer">Quand disponible</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Les hôtes .onion ne seront pas utilisés.</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion.</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Les hôtes .onion ne seront pas utilisés.</string>
|
||||
<string name="delete_address__question">Supprimer l\'adresse \?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connectés.</string>
|
||||
<string name="share_link">Partager le lien</string>
|
||||
<string name="delete_address">Supprimer l\'adresse</string>
|
||||
<string name="contact_requests">Demandes de contact</string>
|
||||
<string name="accept_requests">Accepter les demandes</string>
|
||||
<string name="accept_automatically">Automatiquement</string>
|
||||
<string name="section_title_welcome_message">MESSAGE DE BIENVENUE</string>
|
||||
<string name="display_name__field">Nom affiché :</string>
|
||||
<string name="full_name__field">Nom complet :</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts.
|
||||
\n
|
||||
\nLes serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
|
||||
<string name="delete_image">Supprimer l\'image</string>
|
||||
<string name="save_preferences_question">Sauvegarder les préférences \?</string>
|
||||
<string name="you_control_your_chat">Vous maîtrisez vos discussions !</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plateforme de messagerie et d\'applications qui protège votre vie privée et votre sécurité.</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Nous ne stockons aucun de vos contacts ou messages (une fois délivrés) sur les serveurs.</string>
|
||||
<string name="create_profile">Créer le profil</string>
|
||||
<string name="display_name">Nom affiché</string>
|
||||
<string name="how_to_use_markdown">Comment utiliser markdown</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="colored">coloré</string>
|
||||
<string name="secret">secret</string>
|
||||
<string name="callstatus_calling">appel…</string>
|
||||
<string name="callstatus_missed">appel manqué</string>
|
||||
<string name="callstate_waiting_for_answer">en attente de réponse…</string>
|
||||
<string name="callstate_waiting_for_confirmation">en attente de confirmation…</string>
|
||||
<string name="callstate_connected">connecté</string>
|
||||
<string name="callstate_ended">terminé</string>
|
||||
<string name="next_generation_of_private_messaging">La nouvelle génération de messagerie privée</string>
|
||||
<string name="privacy_redefined">La vie privée redéfinie</string>
|
||||
<string name="first_platform_without_user_ids">La 1ère plateforme sans aucun identifiant d\'utilisateur – privée par design.</string>
|
||||
<string name="immune_to_spam_and_abuse">Protégé du spam et des abus</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">On ne peut se connecter à vous qu’avec les liens que vous partagez.</string>
|
||||
<string name="decentralized">Décentralisé</string>
|
||||
<string name="create_your_profile">Créez votre profil</string>
|
||||
<string name="make_private_connection">Établir une connexion privée</string>
|
||||
<string name="how_it_works">Comment ça fonctionne</string>
|
||||
<string name="how_simplex_works">Comment <xliff:g id="appName">SimpleX</xliff:g> fonctionne</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Beaucoup se demandent : <i>si <xliff:g id="appName">SimpleX</xliff:g> n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?</i></string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un <b>chiffrement de bout en bout à deux couches</b>.</string>
|
||||
<string name="read_more_in_github_with_link">Pour en savoir plus, consultez notre <font color="#0088ff">GitHub repository</font>.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Batterie peu utilisée</b>. Le service de fond vérifie les nouveaux messages toutes les 10 minutes. Vous risquez de manquer des appels et des messages urgents.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Batterie plus utilisée </b> ! Le service de fond est toujours en cours d\'exécution - les notifications s\'afficheront dès que les messages seront disponibles.</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> message⸱s manqué⸱s</string>
|
||||
<string name="integrity_msg_bad_id">ID de message incorrecte</string>
|
||||
<string name="settings_section_title_settings">PARAMÈTRES</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">C\'est possible quand :
|
||||
\n1. Les messages expirent du serveur (après 30 jours si ils ne sont pas reçu).
|
||||
\n2. Le serveur que vous utilisez pour recevoir les messages de ce contact a été mise à jour ou redémarré.
|
||||
\n3. La connection est compromise.
|
||||
\nVeuillez vous connecter aux développeurs via les Paramètres pour recevoir les mises à jour concernant les serveurs.
|
||||
\nNous allons ajouter une redondance des serveurs pour éviter la perte de messages.</string>
|
||||
<string name="icon_descr_call_rejected">Appel rejeté</string>
|
||||
<string name="rcv_group_event_member_deleted">a retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_member_deleted">vous avez retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">invité par votre lien de groupe</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">vous avez changé d\'adresse</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Arrêtez le chat pour exporter, importer ou supprimer la base de données du chat. Vous ne pourrez pas recevoir et envoyer de messages pendant que le chat est arrêté.</string>
|
||||
<string name="enable_automatic_deletion_message">Cette action ne peut être annulée - les messages envoyés et reçus avant la date sélectionnée seront supprimés. Cela peut prendre plusieurs minutes.</string>
|
||||
<string name="encrypted_with_random_passphrase">La base de données est chiffrée à l\'aide d\'une phrase secrète aléatoire, que vous pouvez modifier.</string>
|
||||
<string name="restore_database">Restaurer la sauvegarde de la base de données</string>
|
||||
<string name="restore_passphrase_not_found_desc">La phrase secrète n\'a pas été trouvée dans le Keystore, veuillez la saisir manuellement. Cela a pu se produire si vous avez restauré les données de l\'app à l\'aide d\'un outil de sauvegarde. Si ce n\'est pas le cas, veuillez contacter les développeurs.</string>
|
||||
<string name="restore_database_alert_desc">Veuillez entrer le mot de passe précédent après avoir restauré la sauvegarde de la base de données. Cette action ne peut pas être annulée.</string>
|
||||
<string name="database_restore_error">Erreur de restauration de la base de données</string>
|
||||
<string name="archive_created_on_ts">Créé le <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="encrypted_video_call">appel vidéo (chiffrement de bout en bout)</string>
|
||||
<string name="audio_call_no_encryption">appel audio (sans chiffrement)</string>
|
||||
<string name="encrypted_audio_call">appel audio (chiffrement de bout en bout)</string>
|
||||
<string name="accept">Accepter</string>
|
||||
<string name="reject">Rejeter</string>
|
||||
<string name="icon_descr_video_call">appel vidéo</string>
|
||||
<string name="icon_descr_audio_call">appel audio</string>
|
||||
<string name="accept_call_on_lock_screen">Accepter</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Activer les appels depuis l\'écran verrouillé via les Paramètres.</string>
|
||||
<string name="open_verb">Ouvrir</string>
|
||||
<string name="call_connection_via_relay">via relais</string>
|
||||
<string name="icon_descr_hang_up">Raccrocher</string>
|
||||
<string name="icon_descr_video_on">Vidéo ON</string>
|
||||
<string name="icon_descr_video_off">Vidéo OFF</string>
|
||||
<string name="icon_descr_call_progress">Appel en cours</string>
|
||||
<string name="icon_descr_call_ended">Appel terminé</string>
|
||||
<string name="your_privacy">Votre vie privée</string>
|
||||
<string name="settings_section_title_device">APPAREIL</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="settings_developer_tools">Outils du développeur</string>
|
||||
<string name="settings_section_title_icon">ICONE DE L\'APP</string>
|
||||
<string name="your_chat_database">Votre base de données de chat</string>
|
||||
<string name="run_chat_section">LANCER LE CHAT</string>
|
||||
<string name="stop_chat_question">Arrêter le chat \?</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Redémarrez l\'application pour utiliser la base de données de chat importée.</string>
|
||||
<string name="chat_item_ttl_day">1 jour</string>
|
||||
<string name="delete_messages">Supprimer les messages</string>
|
||||
<string name="save_passphrase_in_keychain">Sauvegarder la phrase secrète dans le keystore</string>
|
||||
<string name="database_encrypted">Base de données chiffrée !</string>
|
||||
<string name="error_encrypting_database">Erreur lors du chiffrement de la base de données</string>
|
||||
<string name="update_database">Mise à jour</string>
|
||||
<string name="encrypt_database">Chiffrer</string>
|
||||
<string name="enter_correct_current_passphrase">Veuillez entrer la phrase secrète actuelle correcte.</string>
|
||||
<string name="database_is_not_encrypted">Votre base de données de chat n\'est pas chiffrée - définissez une phrase secrète pour la protéger.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Veuillez noter</b> : vous NE pourrez PAS récupérer ou modifier la phrase secrète si vous la perdez.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Le Keystore d\'Android sera utilisé pour stocker en toute sécurité la phrase secrète après sa modification ou redémarrage de l\'app - cela permettra de recevoir les notifications.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS accéder au chat si vous la perdez.</string>
|
||||
<string name="passphrase_is_different">La phrase secrète de la base de données est différente de celle enregistrée dans le Keystore.</string>
|
||||
<string name="unknown_error">Erreur inconnue</string>
|
||||
<string name="enter_correct_passphrase">Entrez la phrase secrète correcte.</string>
|
||||
<string name="alert_message_no_group">Ce groupe n\'existe plus.</string>
|
||||
<string name="you_joined_this_group">Vous avez rejoint ce groupe</string>
|
||||
<string name="you_rejected_group_invitation">Vous avez rejeté l\'invitation du groupe</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">vous avez changé d\'adresse pour %s</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">changement d\'adresse…</string>
|
||||
<string name="incoming_video_call">Appel vidéo entrant</string>
|
||||
<string name="video_call_no_encryption">appel vidéo (sans chiffrement)</string>
|
||||
<string name="ignore">Ignorer</string>
|
||||
<string name="call_already_ended">Appel déjà terminé !</string>
|
||||
<string name="settings_audio_video_calls">Appels audio et vidéo</string>
|
||||
<string name="status_e2e_encrypted">chiffré de bout en bout</string>
|
||||
<string name="settings_section_title_develop">DEVELOPPER</string>
|
||||
<string name="settings_experimental_features">Fonctionnalités expérimentales</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_messages">MESSAGES</string>
|
||||
<string name="settings_section_title_calls">APPELS</string>
|
||||
<string name="import_database">Importer la base de données</string>
|
||||
<string name="new_database_archive">Nouvelle archive de base de données</string>
|
||||
<string name="old_database_archive">Archives de l\'ancienne base de données</string>
|
||||
<string name="delete_database">Supprimer la base de données</string>
|
||||
<string name="error_starting_chat">Erreur lors du démarrage du chat</string>
|
||||
<string name="import_database_confirmation">Importer</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Cette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irréversiblement perdus.</string>
|
||||
<string name="chat_database_deleted">Base de données du chat supprimée</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Redémarrez l\'application pour créer un nouveau profil de chat.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Vous devez utiliser la version la plus récente de votre base de données de chat sur un seul appareil UNIQUEMENT, sinon vous risquez de ne plus recevoir les messages de certains contacts.</string>
|
||||
<string name="total_files_count_and_size">%d fichier·s avec une taille totale de %s</string>
|
||||
<string name="chat_item_ttl_none">jamais</string>
|
||||
<string name="chat_item_ttl_week">1 semaine</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">La base de données sera chiffrée et la phrase secrète sera stockée dans le Keystore.</string>
|
||||
<string name="database_encryption_will_be_updated">La phrase secrète de la base de données sera mise à jour et stockée dans le Keystore.</string>
|
||||
<string name="database_passphrase_will_be_updated">La phrase secrète de la base de données sera mise à jour.</string>
|
||||
<string name="store_passphrase_securely">Veuillez conserver votre phrase secrète en lieu sûr, vous NE pourrez PAS la changer si vous la perdez.</string>
|
||||
<string name="wrong_passphrase">Mauvaise phrase secrète pour la base de données</string>
|
||||
<string name="encrypted_database">Base de données chiffrée</string>
|
||||
<string name="database_error">Erreur de base de données</string>
|
||||
<string name="error_with_info">Erreur : %s</string>
|
||||
<string name="cannot_access_keychain">Impossible d\'accéder au Keystore pour enregistrer le mot de passe de la base de données</string>
|
||||
<string name="unknown_database_error_with_info">Erreur de base de données inconnue : %s</string>
|
||||
<string name="wrong_passphrase_title">Mauvaise phrase secrète !</string>
|
||||
<string name="leave_group_question">Quitter le groupe \?</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Vous ne recevrez plus de messages de ce groupe. L\'historique du chat sera conservé.</string>
|
||||
<string name="icon_descr_add_members">Inviter des membres</string>
|
||||
<string name="icon_descr_group_inactive">Groupe inactif</string>
|
||||
<string name="alert_title_group_invitation_expired">Invitation expirée !</string>
|
||||
<string name="alert_message_group_invitation_expired">L\'invitation du groupe n\'est plus valide, elle a été supprimé par l\'expéditeur.</string>
|
||||
<string name="alert_title_no_group">Groupe introuvable !</string>
|
||||
<string name="alert_title_cant_invite_contacts">Impossible d\'inviter les contacts !</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Vous utilisez un profil incognito pour ce groupe - pour éviter de partager votre profil principal ; inviter des contacts n\'est pas possible</string>
|
||||
<string name="you_sent_group_invitation">Vous avez envoyé une invitation de groupe</string>
|
||||
<string name="rcv_group_event_member_left">a quitté</string>
|
||||
<string name="icon_descr_speaker_on">Haut-parleur ON</string>
|
||||
<string name="send_link_previews">Envoi d\'aperçus de liens</string>
|
||||
<string name="error_deleting_database">Erreur lors de la suppression de la base de données du chat</string>
|
||||
<string name="error_stopping_chat">Erreur lors de l\'arrêt du chat</string>
|
||||
<string name="error_exporting_chat_database">Erreur lors de l\'exportation de la base de données du chat</string>
|
||||
<string name="import_database_question">Importer la base de données du chat \?</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Votre base de données de chat actuelle sera SUPPRIMÉE et REMPLACÉE par celle qui a été importée.
|
||||
\nCette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irrémédiablement perdus.</string>
|
||||
<string name="enter_passphrase">Entrez la phrase secrète…</string>
|
||||
<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="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="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>
|
||||
<string name="status_contact_has_no_e2e_encryption">Ce contact n\'a pas le chiffrement de bout en bout</string>
|
||||
<string name="call_connection_peer_to_peer">pair-à-pair</string>
|
||||
<string name="icon_descr_audio_off">Audio OFF</string>
|
||||
<string name="icon_descr_audio_on">Audio ON</string>
|
||||
<string name="icon_descr_speaker_off">Haut-parleur OFF</string>
|
||||
<string name="icon_descr_flip_camera">Retourner la caméra</string>
|
||||
<string name="icon_descr_call_pending_sent">Appel en suspend</string>
|
||||
<string name="icon_descr_call_missed">Appel manqué</string>
|
||||
<string name="icon_descr_call_connecting">Appel en connexion</string>
|
||||
<string name="answer_call">Répondre à l\'appel</string>
|
||||
<string name="integrity_msg_bad_hash">hash de message incorrect</string>
|
||||
<string name="integrity_msg_duplicate">message dupliqué</string>
|
||||
<string name="alert_title_skipped_messages">Messages manqués</string>
|
||||
<string name="privacy_and_security">Vie privée et sécurité</string>
|
||||
<string name="protect_app_screen">Protéger l\'écran de l\'app</string>
|
||||
<string name="auto_accept_images">Images auto-acceptées</string>
|
||||
<string name="transfer_images_faster">Transfert d\'images plus rapide</string>
|
||||
<string name="full_backup">Sauvegarde des données de l\'app</string>
|
||||
<string name="settings_section_title_you">VOUS</string>
|
||||
<string name="settings_section_title_help">AIDE</string>
|
||||
<string name="settings_section_title_support">SOUTENEZ SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_incognito">Mode Incognito</string>
|
||||
<string name="chat_is_running">Le chat est en cours d\'exécution</string>
|
||||
<string name="chat_is_stopped">Le chat est arrêté</string>
|
||||
<string name="chat_database_section">BASE DE DONNÉES DU CHAT</string>
|
||||
<string name="database_passphrase">Phrase secrète de la base de données</string>
|
||||
<string name="export_database">Exporter la base de données</string>
|
||||
<string name="stop_chat_confirmation">Arrêter</string>
|
||||
<string name="set_password_to_export">Définir la phrase secrète pour l\'export</string>
|
||||
<string name="set_password_to_export_desc">La base de données est chiffrée à l\'aide d\'une phrase secrète aléatoire. Veuillez la changer avant d\'exporter.</string>
|
||||
<string name="error_importing_database">Erreur lors de l\'importation de la base de données du chat</string>
|
||||
<string name="chat_database_imported">Base de données du chat importée</string>
|
||||
<string name="delete_chat_profile_question">Supprimer le profil du chat \?</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Arrêter le chat pour agir sur la base de données.</string>
|
||||
<string name="delete_files_and_media_question">Supprimer les fichiers et médias \?</string>
|
||||
<string name="delete_files_and_media_desc">Cette action ne peut être annulée - tous les fichiers et médias reçus et envoyés seront supprimés. Les photos à faible résolution seront conservées.</string>
|
||||
<string name="no_received_app_files">Aucun fichier reçu ou envoyé</string>
|
||||
<string name="chat_item_ttl_month">1 mois</string>
|
||||
<string name="chat_item_ttl_seconds">%s seconde·s</string>
|
||||
<string name="delete_messages_after">Supprimer les messages après</string>
|
||||
<string name="enable_automatic_deletion_question">Activer la suppression automatique des messages \?</string>
|
||||
<string name="error_changing_message_deletion">Erreur de changement de paramètre</string>
|
||||
<string name="remove_passphrase_from_keychain">Retirer la phrase secrète du Keystore \?</string>
|
||||
<string name="notifications_will_be_hidden">Les notifications seront délivrées jusqu\'à ce que l\'application s\'arrête !</string>
|
||||
<string name="remove_passphrase">Supprimer</string>
|
||||
<string name="current_passphrase">Phrase secrète actuelle…</string>
|
||||
<string name="new_passphrase">Nouvelle phrase secrète…</string>
|
||||
<string name="confirm_new_passphrase">Confirmer la nouvelle phrase secrète…</string>
|
||||
<string name="update_database_passphrase">Mise à jour de la phrase secrète de la base de données</string>
|
||||
<string name="keychain_is_storing_securely">Le Keystore d\'Android est utilisé pour stocker en toute sécurité la phrase secrète - elle permet au service de notification de fonctionner.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Vous devez saisir la phrase secrète à chaque fois que l\'application démarre - elle n\'est pas stockée sur l\'appareil.</string>
|
||||
<string name="encrypt_database_question">Chiffrer la base de données \?</string>
|
||||
<string name="change_database_passphrase_question">Changer la phrase secrète de la base de données \?</string>
|
||||
<string name="database_will_be_encrypted">La base de données sera chiffrée.</string>
|
||||
<string name="keychain_error">Erreur de la keychain</string>
|
||||
<string name="file_with_path">Fichier : %s</string>
|
||||
<string name="database_passphrase_is_required">La phrase secrète de la base de données est nécessaire pour ouvrir le chat.</string>
|
||||
<string name="save_passphrase_and_open_chat">Sauvegarder la phrase secrète et ouvrir le chat</string>
|
||||
<string name="open_chat">Ouvrir le chat</string>
|
||||
<string name="database_backup_can_be_restored">La tentative de modification de la phrase secrète de la base de données n\'a pas abouti.</string>
|
||||
<string name="restore_database_alert_title">Restaurer la sauvegarde de la base de données \?</string>
|
||||
<string name="restore_database_alert_confirm">Restaurer</string>
|
||||
<string name="chat_is_stopped_indication">Le chat est arrêté</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Vous pouvez lancer le chat via les Paramètres / la Base de données de l\'app ou en la redémarrant.</string>
|
||||
<string name="chat_archive_header">Archives du chat</string>
|
||||
<string name="chat_archive_section">ARCHIVE DU CHAT</string>
|
||||
<string name="save_archive">Sauvegarder l\'archive</string>
|
||||
<string name="delete_archive">Supprimer l\'archive</string>
|
||||
<string name="delete_chat_archive_question">Supprimer l\'archive du chat \?</string>
|
||||
<string name="group_invitation_item_description">Invitation au groupe <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="join_group_question">Rejoindre le groupe \?</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Vous êtes invité·e dans un groupe. Rejoignez le pour vous connecter avec ses membres.</string>
|
||||
<string name="join_group_button">Rejoindre</string>
|
||||
<string name="join_group_incognito_button">Rejoindre en incognito</string>
|
||||
<string name="joining_group">Entrain de rejoindre le groupe</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Vous avez rejoint ce groupe. Connexion à l\'invitation d\'un membre du groupe.</string>
|
||||
<string name="leave_group_button">Quitter</string>
|
||||
<string name="you_are_invited_to_group">Vous êtes invité·e au groupe</string>
|
||||
<string name="group_invitation_tap_to_join">Appuyez pour rejoindre</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Appuyez pour rejoindre incognito</string>
|
||||
<string name="group_invitation_expired">Invitation au groupe expirée</string>
|
||||
<string name="rcv_group_event_member_added">a invité <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_connected">est connecté·e</string>
|
||||
<string name="rcv_group_event_changed_member_role">a modifié le rôle de %s pour %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">a modifié votre rôle pour %s</string>
|
||||
<string name="rcv_group_event_user_deleted">vous a retiré</string>
|
||||
<string name="rcv_group_event_group_deleted">a supprimé le groupe</string>
|
||||
<string name="rcv_group_event_updated_group_profile">mise à jour du profil de groupe</string>
|
||||
<string name="snd_group_event_changed_member_role">vous avez modifié le rôle de %s pour %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">vous avez modifié votre rôle pour %s</string>
|
||||
<string name="snd_group_event_user_left">vous avez quitté</string>
|
||||
<string name="snd_group_event_group_profile_updated">mise à jour du profil de groupe</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">adresse modifiée pour vous</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">changement d\'adresse pour %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">changement d\'adresse…</string>
|
||||
<string name="group_member_role_member">membre</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
<string name="group_member_role_owner">propriétaire</string>
|
||||
<string name="group_member_status_removed">supprimé</string>
|
||||
<string name="group_member_status_left">a quitté</string>
|
||||
<string name="group_member_status_group_deleted">groupe supprimé</string>
|
||||
<string name="group_member_status_invited">invité·e</string>
|
||||
<string name="group_member_status_introduced">connexion (introduite)</string>
|
||||
<string name="group_member_status_intro_invitation">connexion (introduite par invitation)</string>
|
||||
<string name="group_member_status_accepted">connexion (acceptée)</string>
|
||||
<string name="group_member_status_announced">connexion (annoncée)</string>
|
||||
<string name="group_member_status_connected">connecté</string>
|
||||
<string name="group_member_status_complete">complet</string>
|
||||
<string name="group_member_status_creator">créateur</string>
|
||||
<string name="group_member_status_connecting">connexion</string>
|
||||
<string name="no_contacts_to_add">Aucun contact à ajouter</string>
|
||||
<string name="new_member_role">Nouveau rôle</string>
|
||||
<string name="delete_group_question">Supprimer le groupe \?</string>
|
||||
<string name="group_link">Lien du groupe</string>
|
||||
<string name="button_create_group_link">Créer un lien</string>
|
||||
<string name="button_edit_group_profile">Modifier le profil du groupe</string>
|
||||
<string name="remove_member_confirmation">Supprimer</string>
|
||||
<string name="member_info_section_title_member">MEMBRE</string>
|
||||
<string name="live_message">Message dynamique !</string>
|
||||
<string name="send_live_message">Envoyer un message dynamique</string>
|
||||
<string name="send_live_message_desc">Envoyez un message dynamique - il sera mis à jour pour le⸱s destinataire⸱s au fur et à mesure que vous le tapez</string>
|
||||
<string name="send_verb">Envoyer</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Le rôle sera changé pour «%s». Le membre va recevoir une nouvelle invitation.</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="button_add_members">Inviter des membres</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Vous pouvez partager un lien ou un code QR - n\'importe qui pourra rejoindre le groupe. Vous ne perdrez pas les membres du groupe si vous le supprimez par la suite.</string>
|
||||
<string name="info_row_local_name">Nom local</string>
|
||||
<string name="create_group_link">Créer un lien de groupe</string>
|
||||
<string name="error_deleting_link_for_group">Erreur lors de la suppression du lien du groupe</string>
|
||||
<string name="error_creating_link_for_group">Erreur lors de la création du lien du groupe</string>
|
||||
<string name="only_group_owners_can_change_prefs">Seuls les propriétaires du groupe peuvent modifier les préférences du groupe.</string>
|
||||
<string name="section_title_for_console">POUR TERMINAL</string>
|
||||
<string name="change_member_role_question">Changer le rôle du groupe \?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Le rôle sera changé pour «%s». Les membres du groupe seront notifiés.</string>
|
||||
<string name="icon_descr_contact_checked">Contact vérifié⸱e</string>
|
||||
<string name="clear_contacts_selection_button">Effacer</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contact·s sélectionné·e·s</string>
|
||||
<string name="skip_inviting_button">Passer l’invitation de membres</string>
|
||||
<string name="select_contacts">Sélectionnez des contacts</string>
|
||||
<string name="no_contacts_selected">Aucun contact sélectionné</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBRES</string>
|
||||
<string name="group_info_member_you">vous : <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="button_delete_group">Supprimer le groupe</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Le groupe va être supprimé pour tout les membres - impossible de revenir en arrière !</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Le groupe va être supprimé pour vous - impossible de revenir en arrière !</string>
|
||||
<string name="button_leave_group">Quitter le groupe</string>
|
||||
<string name="delete_link_question">Supprimer le lien \?</string>
|
||||
<string name="delete_link">Supprimer le lien</string>
|
||||
<string name="all_group_members_will_remain_connected">Tous les membres du groupe resteront connectés.</string>
|
||||
<string name="icon_descr_expand_role">Étendre la sélection de rôle</string>
|
||||
<string name="invite_to_group_button">Inviter au groupe</string>
|
||||
<string name="invite_prohibited">Impossible d\'inviter le contact !</string>
|
||||
<string name="invite_prohibited_description">Vous essayez d\'inviter un contact avec lequel vous avez partagé un profil incognito à rejoindre le groupe dans lequel vous utilisez votre profil principal</string>
|
||||
<string name="info_row_database_id">ID de base de données</string>
|
||||
<string name="button_remove_member">Retirer le membre</string>
|
||||
<string name="button_send_direct_message">Envoi de message direct</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Ce membre sera retiré du groupe - impossible de revenir en arrière !</string>
|
||||
<string name="role_in_group">Rôle</string>
|
||||
<string name="change_role">Changer le rôle</string>
|
||||
<string name="change_verb">Changer</string>
|
||||
<string name="switch_verb">Échanger</string>
|
||||
<string name="error_removing_member">Erreur lors de la suppression d\'un membre</string>
|
||||
<string name="error_changing_role">Erreur lors du changement de rôle</string>
|
||||
<string name="group_full_name_field">Nom complet du groupe :</string>
|
||||
<string name="update_network_settings_confirmation">Mise à jour</string>
|
||||
<string name="chat_preferences_on">on</string>
|
||||
<string name="chat_preferences_off">off</string>
|
||||
<string name="direct_messages">Messages dynamiques</string>
|
||||
<string name="full_deletion">Supprimer pour tous</string>
|
||||
<string name="only_you_can_delete_messages">Vous êtes le seul à pouvoir supprimer des messages de manière irréversible (votre contact peut les marquer comme supprimé).</string>
|
||||
<string name="conn_stats_section_title_servers">SERVEURS</string>
|
||||
<string name="receiving_via">Réception via</string>
|
||||
<string name="theme_system">Système</string>
|
||||
<string name="allow_direct_messages">Autoriser l\'envoi de messages directs aux membres.</string>
|
||||
<string name="prohibit_direct_messages">Interdire l\'envoi de messages directs aux membres.</string>
|
||||
<string name="group_members_can_delete">Les membres du groupe peuvent supprimer de manière irréversible les messages envoyés.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">La suppression irréversible de messages est interdite dans ce groupe.</string>
|
||||
<string name="sending_via">Envoi via</string>
|
||||
<string name="network_status">État du réseau</string>
|
||||
<string name="switch_receiving_address">Changer d\'adresse de réception</string>
|
||||
<string name="create_secret_group_title">Créer un groupe secret</string>
|
||||
<string name="group_main_profile_sent">Votre profil de chat sera envoyé aux membres du groupe</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Activer le TCP keep-alive</string>
|
||||
<string name="network_options_save">Sauvegarder</string>
|
||||
<string name="update_network_settings_question">Mettre à jour les paramètres réseau \?</string>
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="incognito_random_profile">Votre profil aléatoire</string>
|
||||
<string name="incognito_random_profile_description">Un profil aléatoire sera envoyé à votre contact</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Un profil aléatoire sera envoyé au contact qui vous a envoyé ce lien</string>
|
||||
<string name="incognito_info_allows">Cela permet d\'avoir plusieurs connections anonymes sans aucune données partagées entre elles sur un même profil.</string>
|
||||
<string name="incognito_info_find">Pour trouver le profil utilisé lors d\'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat.</string>
|
||||
<string name="theme_light">Clair</string>
|
||||
<string name="theme_dark">Sombre</string>
|
||||
<string name="theme">Thème</string>
|
||||
<string name="save_color">Sauvegarder la couleur</string>
|
||||
<string name="reset_color">Réinitialisation des couleurs</string>
|
||||
<string name="color_primary">Principale</string>
|
||||
<string name="chat_preferences_you_allow">Vous autorisez</string>
|
||||
<string name="chat_preferences_contact_allows">Votre contact autorise</string>
|
||||
<string name="chat_preferences_default">par défaut (%s)</string>
|
||||
<string name="chat_preferences_no">non</string>
|
||||
<string name="chat_preferences_always">toujours</string>
|
||||
<string name="chat_preferences">Préférences de chat</string>
|
||||
<string name="contact_preferences">Préférences de contact</string>
|
||||
<string name="group_preferences">Préférences du groupe</string>
|
||||
<string name="set_group_preferences">Définir les préférences du groupe</string>
|
||||
<string name="your_preferences">Vos préférences</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Autorise votre contact à supprimer de façon définitive des messages envoyés.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Autorise vos contacts à envoyer des messages vocaux.</string>
|
||||
<string name="allow_voice_messages_only_if">Autoriser les messages vocaux uniquement si votre contact les autorise.</string>
|
||||
<string name="prohibit_sending_voice_messages">Interdire l\'envoi de messages vocaux.</string>
|
||||
<string name="only_you_can_send_disappearing">Seulement vous pouvez envoyer des messages éphémères.</string>
|
||||
<string name="only_you_can_send_voice">Vous seul pouvez envoyer des messages vocaux.</string>
|
||||
<string name="allow_to_delete_messages">Autoriser la suppression irréversible de messages envoyés.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Les messages éphémères sont interdits dans ce groupe.</string>
|
||||
<string name="group_members_can_send_voice">Les membres du groupe peuvent envoyer des messages vocaux.</string>
|
||||
<string name="delete_after">Supprimer après</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_month">%d mois</string>
|
||||
<string name="ttl_months">%d mois</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_mth">%dm</string>
|
||||
<string name="ttl_hour">%d heure</string>
|
||||
<string name="ttl_hours">%d heures</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="ttl_day">%d jour</string>
|
||||
<string name="ttl_days">%d jours</string>
|
||||
<string name="ttl_d">%dj</string>
|
||||
<string name="ttl_week">%d semaine</string>
|
||||
<string name="ttl_weeks">%d semaines</string>
|
||||
<string name="ttl_w">%dsmn</string>
|
||||
<string name="timed_messages">Messages éphémères</string>
|
||||
<string name="voice_messages">Messages vocaux</string>
|
||||
<string name="feature_enabled">activé</string>
|
||||
<string name="feature_enabled_for_you">activé pour vous</string>
|
||||
<string name="feature_enabled_for_contact">activé pour le contact</string>
|
||||
<string name="feature_off">off</string>
|
||||
<string name="feature_received_prohibited">reçu, non autorisé</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Autorise votre contact à envoyer des messages éphémères.</string>
|
||||
<string name="conn_level_desc_direct">directe</string>
|
||||
<string name="group_is_decentralized">Le groupe est entièrement décentralisé – il n\'est visible que par ses membres.</string>
|
||||
<string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages éphémères.</string>
|
||||
<string name="network_options_revert">Revenir en arrière</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Interdit l’envoi de messages éphémères.</string>
|
||||
<string name="incognito_info_protects">Le mode Incognito protège la confidentialité de votre profil principal — pour chaque nouveau contact un nouveau profil aléatoire est créé.</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</string>
|
||||
<string name="incognito_info_share">Lorsque vous partagez un profil incognito avec quelqu\'un, ce profil sera utilisé pour les groupes auxquels il vous invite.</string>
|
||||
<string name="chat_preferences_yes">oui</string>
|
||||
<string name="allow_disappearing_messages_only_if">Autorise les messages éphémères seulement si votre contact les autorises.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Autoriser la suppression irréversible des messages uniquement si votre contact vous l\'autorise.</string>
|
||||
<string name="only_your_contact_can_delete">Seul votre contact peut supprimer de manière irréversible des messages (vous pouvez les marquer comme supprimé).</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Seulement votre contact peut envoyer des messages éphémères.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Vous et votre contact êtes tous deux en mesure d\'envoyer des messages éphémères.</string>
|
||||
<string name="voice_messages_are_prohibited">Les messages vocaux sont interdits dans ce groupe.</string>
|
||||
<string name="group_display_name_field">Nom affiché du groupe :</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Le mode Incognito n\'est pas supporté ici - votre profil principal sera envoyé aux membres du groupe</string>
|
||||
<string name="conn_level_desc_indirect">indirecte (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="info_row_group">Groupe</string>
|
||||
<string name="info_row_connection">Connexion</string>
|
||||
<string name="network_option_seconds_label">sec</string>
|
||||
<string name="network_option_tcp_connection_timeout">Délai de connexion TCP</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Le profil du groupe est stocké sur les appareils des membres, pas sur les serveurs.</string>
|
||||
<string name="save_group_profile">Sauvegarder le profil du groupe</string>
|
||||
<string name="error_saving_group_profile">Erreur lors de la sauvegarde du profil de groupe</string>
|
||||
<string name="network_options_reset_to_defaults">Réinitialisation des valeurs par défaut</string>
|
||||
<string name="network_option_protocol_timeout">Délai du protocole</string>
|
||||
<string name="network_option_ping_interval">Intervalle de PING</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Vous et votre contact êtes tous deux en mesure de supprimer de manière irréversible les messages envoyés.</string>
|
||||
<string name="message_deletion_prohibited">La suppression irréversible de message est interdite dans ce chat.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Vous et votre contact êtes tous deux en mesure d\'envoyer des messages vocaux.</string>
|
||||
<string name="only_your_contact_can_send_voice">Seul votre contact peut envoyer des messages vocaux.</string>
|
||||
<string name="voice_prohibited_in_this_chat">Les messages vocaux sont interdits dans ce chat.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Les messages éphémères sont interdits dans cette discussion.</string>
|
||||
<string name="allow_to_send_voice">Autoriser l\'envoi de messages vocaux.</string>
|
||||
<string name="prohibit_sending_voice">Interdire l\'envoi de messages vocaux.</string>
|
||||
<string name="allow_to_send_disappearing">Autorise l’envoi de messages éphémères.</string>
|
||||
<string name="prohibit_sending_disappearing">Interdit l’envoi de messages éphémères.</string>
|
||||
<string name="prohibit_message_deletion">Interdire la suppression irréversible des messages.</string>
|
||||
<string name="group_members_can_send_dms">Les membres du groupe peuvent envoyer des messages directs.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans ce groupe.</string>
|
||||
<string name="v4_4_live_messages_desc">Les destinataires voient les mises à jour au fur et à mesure que vous les tapez.</string>
|
||||
<string name="v4_4_verify_connection_security">Vérifier la sécurité de la connexion</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Comparez les codes de sécurité avec vos contacts.</string>
|
||||
<string name="new_in_version">Nouveautés de la %s</string>
|
||||
<string name="v4_2_security_assessment">Évaluation de sécurité</string>
|
||||
<string name="v4_2_group_links">Liens de groupe</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Avec message de bienvenue facultatif.</string>
|
||||
<string name="v4_3_voice_messages">Messages vocaux</string>
|
||||
<string name="v4_3_voice_messages_desc">Max 40 secondes, réception immédiate.</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Suppression irréversible des messages</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Vos contacts peuvent autoriser la suppression complète des messages.</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Une meilleure sécurité et protection de la vie privée</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Masquer l\'écran de l\'app dans les apps récentes.</string>
|
||||
<string name="v4_4_disappearing_messages">Messages éphémères</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Les messages envoyés seront supprimés après une durée déterminée.</string>
|
||||
<string name="v4_4_live_messages">Messages dynamiques</string>
|
||||
<string name="accept_feature">Accepter</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Demandes de contact auto-acceptées</string>
|
||||
<string name="whats_new">Quoi de neuf \?</string>
|
||||
<string name="v4_2_group_links_desc">Les admins peuvent créer les liens qui permettent de rejoindre les groupes.</string>
|
||||
<string name="accept_feature_set_1_day">Définir 1 jour</string>
|
||||
<string name="v4_2_security_assessment_desc">La sécurité de SimpleX Chat a été auditée par Trail of Bits.</string>
|
||||
<string name="v4_3_improved_server_configuration">Configuration de serveur améliorée</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Ajoutez des serveurs en scannant des codes QR.</string>
|
||||
<string name="invalid_data">données invalides</string>
|
||||
<string name="invalid_chat">chat invalide</string>
|
||||
<string name="icon_descr_cancel_live_message">Annuler le message dynamique</string>
|
||||
<string name="feature_offered_item">offert %s</string>
|
||||
<string name="feature_offered_item_with_param">offert %s: %2s</string>
|
||||
<string name="feature_cancelled_item">annulé %s</string>
|
||||
<string name="app_version_title">Version de l\'application</string>
|
||||
<string name="core_simplexmq_version">simplexmq : v%s (%2s)</string>
|
||||
<string name="app_version_code">Build de l\'app : %s</string>
|
||||
<string name="app_version_name">Version de l\'app : v%s</string>
|
||||
<string name="core_build_timestamp">Cœur compilé le : %s</string>
|
||||
<string name="core_version">Version du cœur : v%s</string>
|
||||
<string name="network_option_ping_count">Nombre de PING</string>
|
||||
<string name="users_delete_all_chats_deleted">Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !</string>
|
||||
<string name="delete_files_and_media_all">Effacer tous les fichiers</string>
|
||||
<string name="users_delete_profile_for">Supprimer le profil de chat pour</string>
|
||||
<string name="network_session_mode_user_description">Une connexion TCP distincte (et un identifiant SOCKS) sera utilisée <b>pour chaque profil de chat que vous avez dans l\'application</b>.</string>
|
||||
<string name="users_delete_question">Supprimer le profil du chat \?</string>
|
||||
<string name="files_and_media_section">Fichiers & médias</string>
|
||||
<string name="messages_section_title">Messages</string>
|
||||
<string name="smp_servers_per_user">Les serveurs pour les nouvelles connexions de votre profil de chat actuel</string>
|
||||
<string name="messages_section_description">Ce paramètre s\'applique aux messages de votre profil de chat actuel</string>
|
||||
<string name="network_session_mode_entity">Connexion</string>
|
||||
<string name="delete_files_and_media_for_all_users">Effacer les fichiers de tous les profils de chat</string>
|
||||
<string name="users_delete_with_connections">Profil et connexions au serveur</string>
|
||||
<string name="network_session_mode_transport_isolation">Isolement du transport</string>
|
||||
<string name="update_network_session_mode_question">Mettre à jour le mode d\'isolation du transport \?</string>
|
||||
<string name="your_chat_profiles_stored_locally">Vos profils de chat sont stockés localement, uniquement sur votre appareil</string>
|
||||
<string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>.
|
||||
\n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.</string>
|
||||
<string name="network_session_mode_user">Profil de chat</string>
|
||||
<string name="users_add">Ajouter un profil</string>
|
||||
<string name="users_delete_data_only">Données de profil local uniquement</string>
|
||||
<string name="error_deleting_user">Erreur lors de la suppression du profil utilisateur</string>
|
||||
<string name="your_chat_profiles">Vos profils de chat</string>
|
||||
<string name="failed_to_active_user_title">Erreur lors du changement de profil !</string>
|
||||
<string name="failed_to_create_user_title">Erreur lors de la création du profil !</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Vous avez déjà un profil de chat avec ce même nom affiché. Veuillez choisir un autre nom.</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Nom d\'affichage en double !</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="share_image">चित्र साझा करें…</string>
|
||||
<string name="chat_preferences_off">बंद</string>
|
||||
<string name="accept_feature_set_1_day">1 दिन निर्धारित करें</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">क्यूआर संहिता स्कैन करके सर्वर जोड़ें।</string>
|
||||
<string name="group_preview_you_are_invited">आपको समूह में आमंत्रित किया जाता है</string>
|
||||
<string name="icon_descr_server_status_connected">जुड़े हुए</string>
|
||||
<string name="use_camera_button">कैमरे का प्रयोग करें</string>
|
||||
<string name="above_then_preposition_continuation">ऊपर,तब:</string>
|
||||
<string name="accept_contact_button">स्वीकार करना</string>
|
||||
<string name="connect_button">जुडिये</string>
|
||||
<string name="your_contact_address">आपका संपर्क पता</string>
|
||||
<string name="smp_servers_add_to_another_device">दूसरे उपकरण में जोड़ें</string>
|
||||
<string name="bold">निडर</string>
|
||||
<string name="answer_call">कॉल का उत्तर दें</string>
|
||||
<string name="settings_section_title_you">तुम</string>
|
||||
<string name="settings_section_title_settings">समायोजन</string>
|
||||
<string name="chat_item_ttl_month">1 महीना</string>
|
||||
<string name="rcv_group_event_member_connected">जुड़े हुए</string>
|
||||
<string name="group_member_role_admin">व्यवस्थापक</string>
|
||||
<string name="all_group_members_will_remain_connected">समूह के सभी सदस्य जुड़े रहेंगे।</string>
|
||||
<string name="change_verb">परिवर्तन</string>
|
||||
<string name="sending_via">माध्यम से भेजा जा रहा है</string>
|
||||
<string name="feature_off">बंद</string>
|
||||
<string name="whats_new">नया क्या है</string>
|
||||
<string name="v4_2_group_links_desc">व्यवस्थापक समूहों में शामिल होने के लिए लिंक बना सकते हैं।</string>
|
||||
<string name="chat_item_ttl_day">1 दिन</string>
|
||||
<string name="chat_item_ttl_week">1 सप्ताह</string>
|
||||
<string name="about_simplex">सिंपलएक्स के बारे में</string>
|
||||
<string name="about_simplex_chat">बारे में <xliff:g id="appNameFull">सिंप्लेक्स चैट</xliff:g></string>
|
||||
<string name="accept_call_on_lock_screen">स्वीकार करना</string>
|
||||
<string name="accept">स्वीकार करना</string>
|
||||
<string name="accept_feature">स्वीकार करना</string>
|
||||
<string name="accept_connection_request__question">संबंध अनुरोध स्वीकार करें\?</string>
|
||||
<string name="callstatus_accepted">स्वीकृत कॉल</string>
|
||||
<string name="accept_contact_incognito_button">गुप्त स्वीकार करें</string>
|
||||
<string name="accept_requests">निवेदन स्वीकार करो</string>
|
||||
<string name="smp_servers_preset_add">पूर्वनिर्धारित सर्वर जोड़ें</string>
|
||||
<string name="users_add">प्रोफ़ाइल जोड़ें</string>
|
||||
<string name="smp_servers_add">सर्वर जोड़े…</string>
|
||||
<string name="notifications_mode_service">हमेशा बने रहें</string>
|
||||
<string name="attach">संलग्न करना</string>
|
||||
<string name="network_settings">उन्नत संजाल समायोजन</string>
|
||||
<string name="users_delete_all_chats_deleted">सभी बातचीत और संदेश हटा दिए जाएंगे - इसे पूर्ववत नहीं किया जा सकता!</string>
|
||||
<string name="chat_preferences_always">हमेशा</string>
|
||||
<string name="allow_verb">अनुमति देना</string>
|
||||
<string name="appearance_settings">दिखावट</string>
|
||||
<string name="cancel_verb">रद्द करना</string>
|
||||
<string name="icon_descr_cancel_file_preview">फ़ाइल पूर्वावलोकन रद्द करें</string>
|
||||
<string name="icon_descr_cancel_image_preview">छवि पूर्वावलोकन रद्द करें</string>
|
||||
<string name="clear_verb">साफ़</string>
|
||||
<string name="colored">रंगीन</string>
|
||||
<string name="callstate_connected">जुड़े हुए</string>
|
||||
<string name="smp_server_test_connect">जुडिये</string>
|
||||
<string name="connect_via_link_verb">जुडिये</string>
|
||||
<string name="server_connected">जुड़े हुए</string>
|
||||
<string name="group_member_role_owner">स्वामी</string>
|
||||
<string name="group_member_status_connected">जुड़े हुए</string>
|
||||
<string name="notification_contact_connected">जुड़े हुए</string>
|
||||
<string name="you_joined_this_group">आप इस समूह में शामिल हो गए</string>
|
||||
<string name="group_info_member_you">तुम: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="you_are_invited_to_group">आपको समूह में आमंत्रित किया जाता है</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">तुमने पता बदल लिया</string>
|
||||
<string name="snd_group_event_user_left">आप चले गए</string>
|
||||
<string name="unknown_error">अज्ञात त्रुटि</string>
|
||||
<string name="chat_preferences_you_allow">आप आज्ञा दें</string>
|
||||
<string name="welcome">स्वागत!</string>
|
||||
<string name="la_notice_turn_on">चालू करो</string>
|
||||
<string name="section_title_welcome_message">स्वागत संदेश</string>
|
||||
<string name="unknown_message_format">अज्ञात संदेश प्रारूप</string>
|
||||
<string name="personal_welcome">स्वागत <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="callstate_starting">शुरुआत</string>
|
||||
<string name="send_verb">भेजना</string>
|
||||
<string name="save_color">रंग बचाओ</string>
|
||||
<string name="share_verb">साझा करना</string>
|
||||
<string name="reject_contact_button">अस्वीकार</string>
|
||||
<string name="network_use_onion_hosts_required">आवश्यक</string>
|
||||
<string name="reject">अस्वीकार</string>
|
||||
<string name="open_verb">खुला</string>
|
||||
<string name="group_member_status_removed">निकाला गया</string>
|
||||
<string name="rcv_group_event_member_deleted">निकाला गया <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="reply_verb">जवाब दे दो</string>
|
||||
<string name="leave_group_button">छोड़ना</string>
|
||||
<string name="mark_read">पढ़ा हुआ चिह्नित करें</string>
|
||||
<string name="icon_descr_more_button">अधिक</string>
|
||||
<string name="network_use_onion_hosts_no">नहीं</string>
|
||||
<string name="chat_item_ttl_none">कभी नहीं</string>
|
||||
<string name="group_member_status_invited">आमंत्रित</string>
|
||||
<string name="delete_after">बाद मिटा दें</string>
|
||||
<string name="display_name_invited_to_connect">जुड़ने के लिए आमंत्रित किया</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">आपके समूह लिंक के माध्यम से आमंत्रित किया गया</string>
|
||||
<string name="rcv_group_event_member_added">आमंत्रित <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="icon_descr_add_members">सदस्यों को आमंत्रित करो</string>
|
||||
<string name="v4_3_irreversible_message_deletion">अपरिवर्तनीय संदेश विलोपन</string>
|
||||
<string name="button_add_members">सदस्यों को आमंत्रित करो</string>
|
||||
<string name="invite_to_group_button">समूह में आमंत्रित करें</string>
|
||||
<string name="message_deletion_prohibited_in_chat">इस समूह में अपरिवर्तनीय संदेश हटाना प्रतिबंधित है।</string>
|
||||
<string name="italic">तिरछा</string>
|
||||
<string name="join_group_button">जोड़ना</string>
|
||||
<string name="join_group_incognito_button">गुप्त में शामिल हों</string>
|
||||
<string name="joining_group">समूह में शामिल होना</string>
|
||||
<string name="join_group_question">समूह में शामिल हों\?</string>
|
||||
<string name="thousand_abbreviation">क</string>
|
||||
<string name="keychain_error">चाबी का गुच्छा त्रुटि</string>
|
||||
<string name="button_leave_group">समूह छोड़ दें</string>
|
||||
<string name="leave_group_question">समूह छोड़ दें</string>
|
||||
<string name="rcv_group_event_member_left">बाएं</string>
|
||||
<string name="group_member_status_left">बाएं</string>
|
||||
<string name="theme_light">रोशनी</string>
|
||||
<string name="info_row_local_name">स्थानीय नाम</string>
|
||||
<string name="users_delete_data_only">केवल स्थानीय प्रोफ़ाइल डेटा</string>
|
||||
<string name="auth_log_in_using_credential">अपने क्रेडेंशियल का उपयोग करके लॉग इन करें</string>
|
||||
<string name="make_private_connection">एक निजी संबंध बनाओ</string>
|
||||
<string name="marked_deleted_description">मिटाया हुआ चिह्नित किया गया</string>
|
||||
<string name="mark_unread">अपठित को चिह्नित करें</string>
|
||||
<string name="v4_3_voice_messages_desc">अधिकतम 40 सेकंड, तुरन्त प्राप्त हुआ।</string>
|
||||
<string name="you_sent_group_invitation">आपने समूह आमंत्रण भेजा</string>
|
||||
<string name="message_delivery_error_desc">सबसे अधिक संभावना है कि इस संपर्क ने आपके साथ संबंध हटा दिया है।</string>
|
||||
<string name="mute_chat">मूक</string>
|
||||
<string name="network_status">नेटवर्क की स्थिति</string>
|
||||
<string name="notification_new_contact_request">नया संपर्क अनुरोध</string>
|
||||
<string name="delete_files_and_media_all">सभी फाइलों को मिटा दें</string>
|
||||
<string name="delete_archive">संग्रह हटाएं</string>
|
||||
<string name="new_database_archive">नया डेटाबेस संग्रह</string>
|
||||
<string name="new_member_role">नए सदस्य की भूमिका</string>
|
||||
<string name="settings_notifications_mode_title">अधिसूचना सेवा</string>
|
||||
<string name="notification_preview_new_message">नया सन्देश</string>
|
||||
<string name="no_contacts_to_add">जोड़ने के लिए कोई संपर्क नहीं है</string>
|
||||
<string name="chat_preferences_no">नहीं</string>
|
||||
<string name="no_contacts_selected">कोई संपर्क नहीं चुना गया</string>
|
||||
<string name="no_details">कोई विवरण नहीं</string>
|
||||
<string name="settings_notification_preview_title">अधिसूचना पूर्वावलोकन</string>
|
||||
<string name="notifications">सूचनाएं</string>
|
||||
<string name="full_deletion">सभी के लिए हटाएं</string>
|
||||
<string name="delete_chat_archive_question">चैट संग्रह मिटाएं\?</string>
|
||||
<string name="delete_chat_profile_question">चैट प्रोफ़ाइल हटाएं\?</string>
|
||||
<string name="users_delete_question">चैट प्रोफ़ाइल हटाएं\?</string>
|
||||
<string name="users_delete_profile_for">के लिए चैट प्रोफ़ाइल हटाएं</string>
|
||||
<string name="button_delete_contact">संपर्क मिटा दें</string>
|
||||
<string name="deleted_description">हटाए गए</string>
|
||||
<string name="delete_contact_question">संपर्क मिटा दें\?</string>
|
||||
<string name="rcv_group_event_group_deleted">हटाए गए समूह</string>
|
||||
<string name="delete_image">छवि हटाएं</string>
|
||||
<string name="button_delete_group">समूह हटाएं</string>
|
||||
<string name="for_me_only">मेरे लिए हटाएं</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,970 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="simplex_link_mode">Link di SimpleX</string>
|
||||
<string name="network_error_desc">Controlla la tua connessione di rete con <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> e riprova.</string>
|
||||
<string name="service_notifications_disabled">Le notifiche istantanee sono disattivate!</string>
|
||||
<string name="contact_connection_pending">in connessione…</string>
|
||||
<string name="attach">Allega</string>
|
||||
<string name="icon_descr_cancel_image_preview">Annulla anteprima immagine</string>
|
||||
<string name="images_limit_desc">Possono essere inviate solo 10 immagini alla volta</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">L\'immagine verrà ricevuta quando il tuo contatto sarà in linea, aspetta o controlla più tardi!</string>
|
||||
<string name="waiting_for_image">In attesa dell\'immagine</string>
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="connect_via_invitation_link">Connettere via link di invito\?</string>
|
||||
<string name="connect_via_group_link">Connettere via link del gruppo\?</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Il tuo profilo verrà inviato al contatto da cui hai ricevuto questo link.</string>
|
||||
<string name="connect_via_link_verb">Connetti</string>
|
||||
<string name="server_connected">connesso</string>
|
||||
<string name="server_error">errore</string>
|
||||
<string name="server_connecting">in connessione</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Sei connesso al server usato per ricevere messaggi da questo contatto.</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Tentativo di connessione al server usato per ricevere messaggi da questo contatto.</string>
|
||||
<string name="deleted_description">eliminato</string>
|
||||
<string name="marked_deleted_description">contrassegnato eliminato</string>
|
||||
<string name="sending_files_not_yet_supported">l\'invio di file non è ancora supportato</string>
|
||||
<string name="receiving_files_not_yet_supported">la ricezione di file non è ancora supportata</string>
|
||||
<string name="sender_you_pronoun">tu</string>
|
||||
<string name="unknown_message_format">formato messaggio sconosciuto</string>
|
||||
<string name="invalid_message_format">formato messaggio non valido</string>
|
||||
<string name="live">IN DIRETTA</string>
|
||||
<string name="invalid_chat">conversazione non valida</string>
|
||||
<string name="invalid_data">dati non validi</string>
|
||||
<string name="display_name_connection_established">connessione stabilita</string>
|
||||
<string name="display_name_invited_to_connect">invitato a connettersi</string>
|
||||
<string name="display_name_connecting">in connessione…</string>
|
||||
<string name="description_you_shared_one_time_link">hai condiviso un link una tantum</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">hai condiviso un link incognito una tantum</string>
|
||||
<string name="description_via_group_link">via link di gruppo</string>
|
||||
<string name="description_via_group_link_incognito">incognito via link di gruppo</string>
|
||||
<string name="description_via_contact_address_link">via link indirizzo del contatto</string>
|
||||
<string name="description_via_contact_address_link_incognito">incognito via link indirizzo del contatto</string>
|
||||
<string name="description_via_one_time_link">via link una tantum</string>
|
||||
<string name="description_via_one_time_link_incognito">incognito via link una tantum</string>
|
||||
<string name="simplex_link_contact">Indirizzo del contatto SimpleX</string>
|
||||
<string name="simplex_link_invitation">Invito SimpleX una tantum</string>
|
||||
<string name="simplex_link_group">Link gruppo SimpleX</string>
|
||||
<string name="simplex_link_mode_full">Link completo</string>
|
||||
<string name="simplex_link_mode_browser">Via browser</string>
|
||||
<string name="error_saving_smp_servers">Errore di salvataggio server SMP</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi dei server SMP siano nel formato giusto, uno per riga e non doppi.</string>
|
||||
<string name="error_setting_network_config">Errore di aggiornamento della configurazione di rete</string>
|
||||
<string name="failed_to_parse_chat_title">Caricamento conversazione fallito</string>
|
||||
<string name="failed_to_parse_chats_title">Caricamento delle chat fallito</string>
|
||||
<string name="contact_developers">Aggiorna l\'app e contatta gli sviluppatori.</string>
|
||||
<string name="connection_timeout">Connessione scaduta</string>
|
||||
<string name="connection_error">Errore di connessione</string>
|
||||
<string name="error_sending_message">Errore di invio del messaggio</string>
|
||||
<string name="error_adding_members">Errore di aggiunta del/i membro/i</string>
|
||||
<string name="error_joining_group">Errore di entrata nel gruppo</string>
|
||||
<string name="cannot_receive_file">Impossibile ricevere il file</string>
|
||||
<string name="sender_cancelled_file_transfer">Il mittente ha annullato il trasferimento del file.</string>
|
||||
<string name="error_receiving_file">Errore di ricezione del file</string>
|
||||
<string name="error_creating_address">Errore di creazione dell\'indirizzo</string>
|
||||
<string name="contact_already_exists">Il contatto esiste già</string>
|
||||
<string name="invalid_connection_link">Link di connessione non valido</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro.</string>
|
||||
<string name="connection_error_auth">Errore di connessione (AUTH)</string>
|
||||
<string name="error_accepting_contact_request">Errore di accettazione della richiesta del contatto</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">Il mittente potrebbe aver eliminato la richiesta di connessione.</string>
|
||||
<string name="error_deleting_contact">Errore di eliminazione del contatto</string>
|
||||
<string name="error_deleting_group">Errore di eliminazione del gruppo</string>
|
||||
<string name="error_deleting_contact_request">Errore di eliminazione della richiesta di contatto</string>
|
||||
<string name="error_deleting_pending_contact_connection">Errore di eliminazione della connessione del contatto in attesa</string>
|
||||
<string name="error_changing_address">Errore di modifica dell\'indirizzo</string>
|
||||
<string name="error_smp_test_failed_at_step">Test fallito al passo %s.</string>
|
||||
<string name="error_smp_test_server_auth">Il server richiede l\'autorizzazione di creare code, controlla la password</string>
|
||||
<string name="smp_server_test_connect">Connetti</string>
|
||||
<string name="smp_server_test_create_queue">Crea coda</string>
|
||||
<string name="smp_server_test_secure_queue">Coda sicura</string>
|
||||
<string name="smp_server_test_delete_queue">Elimina coda</string>
|
||||
<string name="smp_server_test_disconnect">Disconnetti</string>
|
||||
<string name="icon_descr_instant_notifications">Notifiche istantanee</string>
|
||||
<string name="service_notifications">Notifiche istantanee!</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Può essere disattivato nelle impostazioni</b>; le notifiche verranno comunque mostrate mentre l\'app è in uso.</string>
|
||||
<string name="turning_off_service_and_periodic">L\'ottimizzazione della batteria è attiva, spegnimento del servizio in secondo piano e delle richieste periodiche di messaggi nuovi. Puoi riattivarli nelle impostazioni.</string>
|
||||
<string name="periodic_notifications">Notifiche periodiche</string>
|
||||
<string name="periodic_notifications_disabled">Le notifiche periodiche sono disattivate!</string>
|
||||
<string name="periodic_notifications_desc">L\'app cerca nuovi messaggi periodicamente, utilizza una piccola percentuale di batteria al giorno. L\'app non usa notifiche push, non vengono inviati dati dal tuo dispositivo ai server.</string>
|
||||
<string name="enter_passphrase_notification_title">Password necessaria</string>
|
||||
<string name="enter_passphrase_notification_desc">Per ricevere notifiche, inserisci la password del database</string>
|
||||
<string name="database_initialization_error_title">Impossibile inizializzare il database</string>
|
||||
<string name="database_initialization_error_desc">Il database non funziona bene. Tocca per maggiori informazioni</string>
|
||||
<string name="simplex_service_notification_text">Ricezione messaggi…</string>
|
||||
<string name="hide_notification">Nascondi</string>
|
||||
<string name="ntf_channel_messages">Messaggi di SimpleX Chat</string>
|
||||
<string name="ntf_channel_calls">Chiamate di SimpleX Chat</string>
|
||||
<string name="settings_notifications_mode_title">Servizio di notifica</string>
|
||||
<string name="settings_notification_preview_mode_title">Mostra anteprima</string>
|
||||
<string name="settings_notification_preview_title">Anteprima notifica</string>
|
||||
<string name="notifications_mode_off">Quando l\'app è aperta</string>
|
||||
<string name="notifications_mode_periodic">Periodicamente</string>
|
||||
<string name="notifications_mode_service">Sempre attivo</string>
|
||||
<string name="notifications_mode_off_desc">L\'app può ricevere notifiche solo quando è attiva, non verrà avviato alcun servizio in secondo piano</string>
|
||||
<string name="notifications_mode_periodic_desc">Controlla messaggi nuovi ogni 10 minuti per massimo 1 minuto</string>
|
||||
<string name="notification_preview_mode_message">Testo del messaggio</string>
|
||||
<string name="notification_preview_mode_contact">Nome del contatto</string>
|
||||
<string name="notification_preview_mode_hidden">Nascosta</string>
|
||||
<string name="notification_preview_mode_message_desc">Mostra contatto e messaggio</string>
|
||||
<string name="notification_preview_mode_contact_desc">Mostra solo il contatto</string>
|
||||
<string name="notification_display_mode_hidden_desc">Nascondi contatto e messaggio</string>
|
||||
<string name="notification_preview_somebody">Contatto nascosto:</string>
|
||||
<string name="notification_preview_new_message">messaggio nuovo</string>
|
||||
<string name="notification_new_contact_request">Nuova richiesta di contatto</string>
|
||||
<string name="notification_contact_connected">Connesso</string>
|
||||
<string name="la_notice_turn_on">Attiva</string>
|
||||
<string name="auth_unlock">Sblocca</string>
|
||||
<string name="auth_log_in_using_credential">Accedi usando le tue credenziali</string>
|
||||
<string name="auth_enable_simplex_lock">Attiva SimpleX Lock</string>
|
||||
<string name="auth_disable_simplex_lock">Disattiva SimpleX Lock</string>
|
||||
<string name="auth_confirm_credential">Conferma le tue credenziali</string>
|
||||
<string name="auth_unavailable">Autenticazione non disponibile</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">L\'autenticazione del dispositivo è disattivata. Disattivazione di SimpleX Lock.</string>
|
||||
<string name="auth_stop_chat">Ferma la chat</string>
|
||||
<string name="auth_open_chat_console">Apri la console della chat</string>
|
||||
<string name="message_delivery_error_title">Errore di recapito del messaggio</string>
|
||||
<string name="message_delivery_error_desc">Probabilmente questo contatto ha eliminato la connessione con te.</string>
|
||||
<string name="reply_verb">Rispondi</string>
|
||||
<string name="share_verb">Condividi</string>
|
||||
<string name="copy_verb">Copia</string>
|
||||
<string name="save_verb">Salva</string>
|
||||
<string name="edit_verb">Modifica</string>
|
||||
<string name="delete_verb">Elimina</string>
|
||||
<string name="reveal_verb">Rivela</string>
|
||||
<string name="hide_verb">Nascondi</string>
|
||||
<string name="allow_verb">Consenti</string>
|
||||
<string name="delete_message__question">Eliminare il messaggio\?</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Il messaggio verrà eliminato, non è reversibile!</string>
|
||||
<string name="for_me_only">Elimina per me</string>
|
||||
<string name="for_everybody">Per tutti</string>
|
||||
<string name="icon_descr_edited">modificato</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">inviato</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">invio non autorizzato</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">invio fallito</string>
|
||||
<string name="icon_descr_received_msg_status_unread">non letto</string>
|
||||
<string name="personal_welcome">Benvenuto/a <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="welcome">Benvenuto/a!</string>
|
||||
<string name="this_text_is_available_in_settings">Questo testo è disponibile nelle impostazioni</string>
|
||||
<string name="your_chats">Le tue chat</string>
|
||||
<string name="group_preview_you_are_invited">sei stato invitato in un gruppo</string>
|
||||
<string name="group_preview_join_as">entra come %s</string>
|
||||
<string name="group_connection_pending">in connessione…</string>
|
||||
<string name="tap_to_start_new_chat">Tocca per iniziare una conversazione</string>
|
||||
<string name="chat_with_developers">Scrivi agli sviluppatori</string>
|
||||
<string name="you_have_no_chats">Non hai chat</string>
|
||||
<string name="share_image">Condividi immagine…</string>
|
||||
<string name="share_file">Condividi file…</string>
|
||||
<string name="icon_descr_context">Icona contestuale</string>
|
||||
<string name="icon_descr_cancel_file_preview">Annulla anteprima file</string>
|
||||
<string name="images_limit_title">Troppe immagini!</string>
|
||||
<string name="image_decoding_exception_title">Errore di decodifica</string>
|
||||
<string name="image_decoding_exception_desc">L\'immagine non può essere decodificata. Prova con un\'altra o contatta gli sviluppatori.</string>
|
||||
<string name="image_descr">Immagine</string>
|
||||
<string name="icon_descr_waiting_for_image">In attesa dell\'immagine</string>
|
||||
<string name="icon_descr_asked_to_receive">Richiesta di ricezione immagine</string>
|
||||
<string name="icon_descr_image_snd_complete">Immagine inviata</string>
|
||||
<string name="image_saved">Immagine salvata nella Galleria</string>
|
||||
<string name="icon_descr_file">File</string>
|
||||
<string name="large_file">File grande!</string>
|
||||
<string name="maximum_supported_file_size">Attualmente la dimensione massima supportata è di <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="waiting_for_file">In attesa del file</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Il file verrà ricevuto quando il tuo contatto sarà in linea, aspetta o controlla più tardi!</string>
|
||||
<string name="file_saved">File salvato</string>
|
||||
<string name="file_not_found">File non trovato</string>
|
||||
<string name="error_saving_file">Errore di salvataggio del file</string>
|
||||
<string name="voice_message">Messaggio vocale</string>
|
||||
<string name="voice_message_with_duration">Messaggio vocale (<xliff:g id="duration">%1$s</xliff:g>)</string>
|
||||
<string name="voice_message_send_text">Messaggio vocale…</string>
|
||||
<string name="notifications">Notifiche</string>
|
||||
<string name="delete_contact_question">Eliminare il contatto\?</string>
|
||||
<string name="button_delete_contact">Elimina contatto</string>
|
||||
<string name="text_field_set_contact_placeholder">Imposta nome del contatto…</string>
|
||||
<string name="icon_descr_server_status_connected">Connesso</string>
|
||||
<string name="icon_descr_server_status_disconnected">Disconnesso</string>
|
||||
<string name="icon_descr_server_status_error">Errore</string>
|
||||
<string name="icon_descr_server_status_pending">In attesa</string>
|
||||
<string name="switch_receiving_address_question">Cambiare l\'indirizzo di ricezione\?</string>
|
||||
<string name="view_security_code">Vedi codice di sicurezza</string>
|
||||
<string name="verify_security_code">Verifica codice di sicurezza</string>
|
||||
<string name="icon_descr_send_message">Invia messaggio</string>
|
||||
<string name="icon_descr_record_voice_message">Registra messaggio vocale</string>
|
||||
<string name="allow_voice_messages_question">Permettere i messaggi vocali\?</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Devi consentire al tuo contatto di inviare messaggi vocali per poterli inviare anche tu.</string>
|
||||
<string name="voice_messages_prohibited">Messaggi vocali vietati!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Chiedi al tuo contatto di attivare l\'invio dei messaggi vocali.</string>
|
||||
<string name="send_live_message">Invia messaggio in diretta</string>
|
||||
<string name="live_message">Messaggio in diretta!</string>
|
||||
<string name="send_verb">Invia</string>
|
||||
<string name="back">Indietro</string>
|
||||
<string name="cancel_verb">Annulla</string>
|
||||
<string name="confirm_verb">Conferma</string>
|
||||
<string name="reset_verb">Ripristina</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="connect_via_contact_link">Connettere via link del contatto\?</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="you_will_join_group">Entrerai in un gruppo a cui si riferisce questo link e ti connetterai ai suoi membri.</string>
|
||||
<string name="connection_local_display_name">connessione <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="simplex_link_mode_description">Descrizione</string>
|
||||
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
<string name="simplex_link_mode_browser_warning">Aprire il link nel browser può ridurre la privacy e la sicurezza della connessione. I link SimpleX non fidati saranno in rosso.</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Sei già connesso a <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
|
||||
<string name="connection_error_auth_desc">A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo.
|
||||
\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.</string>
|
||||
<string name="error_smp_test_certificate">Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Per rispettare la tua privacy, invece delle notifiche push l\'app ha un <b>servizio <xliff:g id="appName">SimpleX</xliff:g> in secondo piano</b>; usa una piccola percentuale di batteria al giorno.</string>
|
||||
<string name="turn_off_battery_optimization">Per poterlo usare, <b>disattiva l\'ottimizzazione della batteria</b> per <xliff:g id="appName">SimpleX</xliff:g> nella prossima schermata. Altrimenti le notifiche saranno disattivate.</string>
|
||||
<string name="simplex_service_notification_title">Servizio <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="notifications_mode_service_desc">Servizio in secondo piano sempre attivo. Le notifiche verranno mostrate appena i messaggi saranno disponibili.</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Lock</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">Per proteggere le tue informazioni, attiva SimpleX Lock.
|
||||
\nTi verrà chiesto di completare l\'autenticazione prima di attivare questa funzionalità.</string>
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX Lock attivo</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Dovrai autenticarti quando avvii o riapri l\'app dopo 30 secondi in secondo piano.</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">L\'autenticazione del dispositivo non è attiva. Potrai attivare SimpleX Lock nelle impostazioni, quando avrai attivato l\'autenticazione del dispositivo.</string>
|
||||
<string name="delete_message_mark_deleted_warning">Il messaggio verrà contrassegnato per l\'eliminazione. I destinatari potranno rivelare questo messaggio.</string>
|
||||
<string name="share_message">Condividi messaggio…</string>
|
||||
<string name="contact_sent_large_file">Il tuo contatto ha inviato un file più grande della dimensione massima supportata (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Il contatto e tutti i messaggi verranno eliminati, non è reversibile!</string>
|
||||
<string name="switch_receiving_address_desc">Questa funzionalità è sperimentale! Funzionerà solo se l\'altro client ha la versione 4.2 installata. Dovresti vedere il messaggio nella conversazione una volta completato il cambio di indirizzo. Controlla di potere ancora ricevere messaggi da questo contatto (o membro del gruppo).</string>
|
||||
<string name="only_group_owners_can_enable_voice">Solo i proprietari del gruppo possono attivare i messaggi vocali.</string>
|
||||
<string name="send_live_message_desc">Invia un messaggio in diretta: si aggiornerà per i destinatari mentre lo digiti</string>
|
||||
<string name="chat_item_ttl_day">1 giorno</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex_chat">Riguardo <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="group_member_role_admin">amministratore</string>
|
||||
<string name="chat_item_ttl_week">1 settimana</string>
|
||||
<string name="smp_servers_add_to_another_device">Aggiungi ad un altro dispositivo</string>
|
||||
<string name="accept">Accetta</string>
|
||||
<string name="v4_2_group_links_desc">Gli amministratori possono creare i link per entrare nei gruppi.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Consenti i messaggi a tempo solo se il tuo contatto li consente.</string>
|
||||
<string name="allow_to_delete_messages">Permetti di eliminare irreversibilmente i messaggi inviati.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Permetti ai tuoi contatti di inviare messaggi a tempo.</string>
|
||||
<string name="accept_requests">Accetta le richieste</string>
|
||||
<string name="network_enable_socks_info">Accedere ai server via proxy SOCKS sulla porta 9050\? Il proxy deve essere avviato prima di attivare questa opzione.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Aggiungi server scansionando codici QR.</string>
|
||||
<string name="all_group_members_will_remain_connected">Tutti i membri del gruppo resteranno connessi.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Consenti l\'eliminazione irreversibile dei messaggi solo se il contatto la consente a te.</string>
|
||||
<string name="above_then_preposition_continuation">sopra, quindi:</string>
|
||||
<string name="accept_contact_button">Accetta</string>
|
||||
<string name="accept_connection_request__question">Accettare la richiesta di connessione\?</string>
|
||||
<string name="accept_contact_incognito_button">Accetta in incognito</string>
|
||||
<string name="clear_chat_warning">Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te.</string>
|
||||
<string name="smp_servers_preset_add">Aggiungi server preimpostati</string>
|
||||
<string name="smp_servers_add">Aggiungi server…</string>
|
||||
<string name="network_settings">Impostazioni di rete avanzate</string>
|
||||
<string name="about_simplex">Riguardo SimpleX</string>
|
||||
<string name="callstatus_accepted">chiamata accettata</string>
|
||||
<string name="accept_call_on_lock_screen">Accetta</string>
|
||||
<string name="color_primary">Principale</string>
|
||||
<string name="accept_feature">Accetta</string>
|
||||
<string name="allow_voice_messages_only_if">Consenti i messaggi vocali solo se il tuo contatto li consente.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Permetti ai tuoi contatti di eliminare irreversibilmente i messaggi inviati.</string>
|
||||
<string name="allow_direct_messages">Permetti l\'invio di messaggi diretti ai membri.</string>
|
||||
<string name="allow_to_send_disappearing">Permetti l\'invio di messaggi a tempo.</string>
|
||||
<string name="allow_to_send_voice">Permetti l\'invio di messaggi vocali.</string>
|
||||
<string name="chat_item_ttl_month">1 mese</string>
|
||||
<string name="error_importing_database">Errore nell\'importazione del database della chat</string>
|
||||
<string name="group_full_name_field">Nome completo del gruppo:</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Se non potete incontrarvi di persona, puoi <b>scansionare il codice QR nella videochiamata</b>, oppure il tuo contatto può condividere un link di invito.</string>
|
||||
<string name="full_backup">Backup dei dati dell\'app</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore è usato per memorizzare in modo sicuro la password; permette il funzionamento del servizio di notifica.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Permetti ai tuoi contatti di inviare messaggi vocali.</string>
|
||||
<string name="chat_database_deleted">Database della chat eliminato</string>
|
||||
<string name="settings_section_title_icon">ICONA APP</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Verrà inviato un profilo casuale al contatto da cui hai ricevuto questo link</string>
|
||||
<string name="incognito_random_profile_description">Verrà inviato un profilo casuale al tuo contatto</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione, il servizio in secondo piano NON verrà usato.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Consuma più batteria</b>! Il servizio in secondo piano è sempre attivo: le notifiche verranno mostrate non appena i messaggi saranno disponibili.</string>
|
||||
<string name="callstatus_calling">chiamata…</string>
|
||||
<string name="icon_descr_cancel_link_preview">annulla anteprima link</string>
|
||||
<string name="cannot_access_keychain">Impossibile accedere al Keystore per salvare la password del database</string>
|
||||
<string name="alert_title_cant_invite_contacts">Impossibile invitare i contatti!</string>
|
||||
<string name="change_role">Cambia ruolo</string>
|
||||
<string name="chat_archive_section">ARCHIVIO CHAT</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">cambio indirizzo…</string>
|
||||
<string name="chat_is_stopped">Chat fermata</string>
|
||||
<string name="group_member_status_introduced">connessione (presentato)</string>
|
||||
<string name="contact_requests">Richieste del contatto</string>
|
||||
<string name="connection_request_sent">Richiesta di connessione inviata!</string>
|
||||
<string name="delete_link_question">Eliminare il link\?</string>
|
||||
<string name="delete_link">Elimina link</string>
|
||||
<string name="create_address">Crea indirizzo</string>
|
||||
<string name="button_create_group_link">Crea link</string>
|
||||
<string name="database_encryption_will_be_updated">La password di crittografia del database verrà aggiornata e conservata nel Keystore.</string>
|
||||
<string name="encrypted_with_random_passphrase">Il database è crittografato con una password casuale, puoi cambiarla.</string>
|
||||
<string name="database_passphrase_is_required">La password del database è necessaria per aprire la chat.</string>
|
||||
<string name="delete_group_menu_action">Elimina</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">I messaggi diretti tra i membri sono vietati in questo gruppo.</string>
|
||||
<string name="display_name">Nome da mostrare</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Aggiungi un contatto</b>: per creare il tuo codice QR una tantum per il tuo contatto.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scansiona codice QR</b>: per connetterti al contatto che ti mostra il codice QR.</string>
|
||||
<string name="choose_file">Scegli file</string>
|
||||
<string name="clear_chat_button">Svuota chat</string>
|
||||
<string name="clear_chat_question">Svuotare la chat\?</string>
|
||||
<string name="clear_verb">Svuota</string>
|
||||
<string name="connect_via_link_or_qr">Connetti via link / codice QR</string>
|
||||
<string name="copied">Copiato negli appunti</string>
|
||||
<string name="share_one_time_link">Crea link di invito una tantum</string>
|
||||
<string name="create_group">Crea gruppo segreto</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scansiona dall\'app il codice QR mostrato, tramite <b>Scansiona codice QR</b>.</string>
|
||||
<string name="from_gallery_button">Dalla Galleria</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Se scegli di rifiutare, il mittente NON verrà avvisato.</string>
|
||||
<string name="clear_chat_menu_action">Svuota</string>
|
||||
<string name="icon_descr_close_button">Pulsante di chiusura</string>
|
||||
<string name="alert_title_contact_connection_pending">Il contatto non è ancora connesso!</string>
|
||||
<string name="delete_contact_menu_action">Elimina</string>
|
||||
<string name="delete_pending_connection__question">Eliminare la connessione in attesa\?</string>
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="icon_descr_help">aiuto</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Se non potete incontrarvi di persona, <b>mostra il codice QR nella videochiamata</b>, oppure condividi il link.</string>
|
||||
<string name="chat_console">Console della chat</string>
|
||||
<string name="clear_verification">Annulla la verifica</string>
|
||||
<string name="connect_button">Connetti</string>
|
||||
<string name="connect_via_link">Connetti via link</string>
|
||||
<string name="create_one_time_link">Crea link di invito una tantum</string>
|
||||
<string name="database_passphrase_and_export">Password del database ed esportazione</string>
|
||||
<string name="smp_servers_enter_manually">Inserisci il server manualmente</string>
|
||||
<string name="how_to_use_simplex_chat">Come si usa</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Tutti i tuoi contatti resteranno connessi.</string>
|
||||
<string name="appearance_settings">Aspetto</string>
|
||||
<string name="smp_servers_check_address">Controlla l\'indirizzo del server e riprova.</string>
|
||||
<string name="configure_ICE_servers">Configura server ICE</string>
|
||||
<string name="contribute">Contribuisci</string>
|
||||
<string name="delete_address">Elimina indirizzo</string>
|
||||
<string name="delete_address__question">Eliminare l\'indirizzo\?</string>
|
||||
<string name="smp_servers_delete_server">Elimina server</string>
|
||||
<string name="error_saving_ICE_servers">Errore nel salvataggio dei server ICE</string>
|
||||
<string name="how_to">Come si fa</string>
|
||||
<string name="how_to_use_your_servers">Come usare i tuoi server</string>
|
||||
<string name="enter_one_ICE_server_per_line">Server ICE (uno per riga)</string>
|
||||
<string name="accept_automatically">Automaticamente</string>
|
||||
<string name="bold">grassetto</string>
|
||||
<string name="callstatus_ended">chiamata terminata <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="callstatus_error">errore di chiamata</string>
|
||||
<string name="callstatus_in_progress">chiamata in corso</string>
|
||||
<string name="colored">colorato</string>
|
||||
<string name="callstate_connected">connesso</string>
|
||||
<string name="callstate_connecting">connessione…</string>
|
||||
<string name="callstatus_connecting">connessione chiamata…</string>
|
||||
<string name="create_profile_button">Crea</string>
|
||||
<string name="create_profile">Crea profilo</string>
|
||||
<string name="delete_image">Elimina immagine</string>
|
||||
<string name="display_name__field">Nome da mostrare:</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Il nome da mostrare non può contenere spazi.</string>
|
||||
<string name="edit_image">Modifica immagine</string>
|
||||
<string name="exit_without_saving">Esci senza salvare</string>
|
||||
<string name="full_name__field">Nome completo:</string>
|
||||
<string name="full_name_optional__prompt">Nome completo (facoltativo)</string>
|
||||
<string name="how_to_use_markdown">Come usare il markdown</string>
|
||||
<string name="icon_descr_audio_call">chiamata audio</string>
|
||||
<string name="audio_call_no_encryption">chiamata audio (non crittografata e2e)</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Buono per la batteria</b>. Il servizio in secondo piano controlla nuovi messaggi ogni 10 minuti. Potresti perdere chiamate e messaggi urgenti.</string>
|
||||
<string name="call_already_ended">Chiamata già terminata!</string>
|
||||
<string name="create_your_profile">Crea il tuo profilo</string>
|
||||
<string name="decentralized">Decentralizzato</string>
|
||||
<string name="encrypted_audio_call">Chiamata crittografata e2e</string>
|
||||
<string name="encrypted_video_call">Videochiamata crittografata e2e</string>
|
||||
<string name="callstate_ended">terminata</string>
|
||||
<string name="how_it_works">Come funziona</string>
|
||||
<string name="how_simplex_works">Come funziona <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="answer_call">Rispondi alla chiamata</string>
|
||||
<string name="icon_descr_audio_off">Audio spento</string>
|
||||
<string name="icon_descr_audio_on">Audio acceso</string>
|
||||
<string name="settings_audio_video_calls">Chiamate audio e video</string>
|
||||
<string name="auto_accept_images">Auto-accetta immagini</string>
|
||||
<string name="integrity_msg_bad_hash">hash del messaggio errato</string>
|
||||
<string name="integrity_msg_bad_id">ID messaggio errato</string>
|
||||
<string name="icon_descr_call_ended">Chiamata terminata</string>
|
||||
<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="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>
|
||||
<string name="integrity_msg_duplicate">messaggio duplicato</string>
|
||||
<string name="status_e2e_encrypted">crittografato e2e</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Attiva le chiamate dalla schermata di blocco tramite le impostazioni.</string>
|
||||
<string name="icon_descr_flip_camera">Fotocamera frontale/posteriore</string>
|
||||
<string name="icon_descr_hang_up">Riaggancia</string>
|
||||
<string name="settings_section_title_calls">CHIAMATE</string>
|
||||
<string name="chat_database_section">DATABASE DELLA CHAT</string>
|
||||
<string name="chat_database_imported">Database della chat importato</string>
|
||||
<string name="chat_is_running">Chat in esecuzione</string>
|
||||
<string name="settings_section_title_chats">CHAT</string>
|
||||
<string name="set_password_to_export_desc">Il database è crittografato con una password casuale. Cambiala prima di esportare.</string>
|
||||
<string name="database_passphrase">Password del database</string>
|
||||
<string name="delete_chat_profile_question">Eliminare il profilo di chat\?</string>
|
||||
<string name="delete_database">Elimina database</string>
|
||||
<string name="settings_section_title_develop">SVILUPPA</string>
|
||||
<string name="settings_developer_tools">Strumenti di sviluppo</string>
|
||||
<string name="settings_section_title_device">DISPOSITIVO</string>
|
||||
<string name="error_deleting_database">Errore nell\'eliminazione del database della chat</string>
|
||||
<string name="error_exporting_chat_database">Errore nell\'esportazione del database della chat</string>
|
||||
<string name="error_starting_chat">Errore nell\'avvio della chat</string>
|
||||
<string name="error_stopping_chat">Errore nell\'interruzione della chat</string>
|
||||
<string name="settings_experimental_features">Funzionalità sperimentali</string>
|
||||
<string name="export_database">Esporta database</string>
|
||||
<string name="settings_section_title_help">AIUTO</string>
|
||||
<string name="chat_archive_header">Archivio chat</string>
|
||||
<string name="chat_is_stopped_indication">Chat fermata</string>
|
||||
<string name="archive_created_on_ts">Creato il <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="database_error">Errore del database</string>
|
||||
<string name="passphrase_is_different">La password del database è diversa da quella salvata nel Keystore.</string>
|
||||
<string name="delete_archive">Elimina archivio</string>
|
||||
<string name="delete_chat_archive_question">Eliminare l\'archivio della chat\?</string>
|
||||
<string name="encrypted_database">Database crittografato</string>
|
||||
<string name="enter_correct_passphrase">Inserisci la password giusta.</string>
|
||||
<string name="enter_passphrase">Inserisci la password…</string>
|
||||
<string name="error_with_info">Errore: %s</string>
|
||||
<string name="file_with_path">File: %s</string>
|
||||
<string name="icon_descr_group_inactive">Gruppo inattivo</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">indirizzo cambiato per te</string>
|
||||
<string name="rcv_group_event_changed_member_role">cambiato il ruolo di %s in %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">cambiato il tuo ruolo in %s</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">cambio indirizzo…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">cambio indirizzo per %s…</string>
|
||||
<string name="rcv_group_event_member_connected">connesso</string>
|
||||
<string name="group_member_status_connected">connesso</string>
|
||||
<string name="group_member_status_accepted">connessione (accettato)</string>
|
||||
<string name="group_member_status_announced">connessione (annunciato)</string>
|
||||
<string name="group_member_status_intro_invitation">connessione (invito di presentazione)</string>
|
||||
<string name="rcv_group_event_group_deleted">gruppo eliminato</string>
|
||||
<string name="group_member_status_group_deleted">gruppo eliminato</string>
|
||||
<string name="group_invitation_expired">Invito al gruppo scaduto</string>
|
||||
<string name="alert_message_group_invitation_expired">L\'invito al gruppo non è più valido, è stato rimosso dal mittente.</string>
|
||||
<string name="alert_title_no_group">Gruppo non trovato!</string>
|
||||
<string name="snd_group_event_group_profile_updated">profilo del gruppo aggiornato</string>
|
||||
<string name="invite_prohibited">Impossibile invitare il contatto!</string>
|
||||
<string name="change_verb">Cambia</string>
|
||||
<string name="change_member_role_question">Cambiare il ruolo del gruppo\?</string>
|
||||
<string name="clear_contacts_selection_button">Svuota</string>
|
||||
<string name="group_member_status_complete">completo</string>
|
||||
<string name="group_member_status_connecting">connessione</string>
|
||||
<string name="icon_descr_contact_checked">Contatto controllato</string>
|
||||
<string name="create_group_link">Crea link del gruppo</string>
|
||||
<string name="group_member_status_creator">creatore</string>
|
||||
<string name="info_row_database_id">ID database</string>
|
||||
<string name="button_delete_group">Elimina gruppo</string>
|
||||
<string name="delete_group_question">Eliminare il gruppo\?</string>
|
||||
<string name="button_edit_group_profile">Modifica il profilo del gruppo</string>
|
||||
<string name="error_creating_link_for_group">Errore nella creazione del link del gruppo</string>
|
||||
<string name="error_deleting_link_for_group">Errore nell\'eliminazione del link del gruppo</string>
|
||||
<string name="icon_descr_expand_role">Espandi la selezione dei ruoli</string>
|
||||
<string name="section_title_for_console">PER CONSOLE</string>
|
||||
<string name="group_link">Link del gruppo</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">Il gruppo verrà eliminato per tutti i membri. Non è reversibile!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Il gruppo verrà eliminato per te. Non è reversibile!</string>
|
||||
<string name="info_row_connection">Connessione</string>
|
||||
<string name="create_secret_group_title">Crea gruppo segreto</string>
|
||||
<string name="conn_level_desc_direct">diretta</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Attiva il keep-alive TCP</string>
|
||||
<string name="error_changing_role">Errore nel cambio di ruolo</string>
|
||||
<string name="error_removing_member">Errore nella rimozione del membro</string>
|
||||
<string name="error_saving_group_profile">Errore nel salvataggio del profilo del gruppo</string>
|
||||
<string name="info_row_group">Gruppo</string>
|
||||
<string name="group_display_name_field">Nome da mostrare del gruppo:</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Il profilo del gruppo è memorizzato sui dispositivi dei membri, non sui server.</string>
|
||||
<string name="chat_preferences_always">sempre</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Sia tu che il tuo contatto potete eliminare irreversibilmente i messaggi inviati.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Sia tu che il tuo contatto potete inviare messaggi a tempo.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Sia tu che il tuo contatto potete inviare messaggi vocali.</string>
|
||||
<string name="chat_preferences">Preferenze della chat</string>
|
||||
<string name="chat_preferences_contact_allows">Il contatto lo consente</string>
|
||||
<string name="contact_preferences">Preferenze del contatto</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">I contatti possono contrassegnare i messaggi per l\'eliminazione; potrai vederli.</string>
|
||||
<string name="theme_dark">Scuro</string>
|
||||
<string name="chat_preferences_default">predefinito (%s)</string>
|
||||
<string name="full_deletion">Elimina per tutti</string>
|
||||
<string name="direct_messages">Messaggi diretti</string>
|
||||
<string name="timed_messages">Messaggi a tempo</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">I messaggi a tempo sono vietati in questa conversazione.</string>
|
||||
<string name="feature_enabled">attivato</string>
|
||||
<string name="feature_enabled_for_contact">attivato per il contatto</string>
|
||||
<string name="feature_enabled_for_you">attivato per te</string>
|
||||
<string name="group_preferences">Preferenze del gruppo</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Auto-accetta richieste di contatto</string>
|
||||
<string name="ttl_d">%dg</string>
|
||||
<string name="ttl_day">%d giorno</string>
|
||||
<string name="ttl_days">%d giorni</string>
|
||||
<string name="delete_after">Elimina dopo</string>
|
||||
<string name="ttl_h">%do</string>
|
||||
<string name="ttl_hour">%d ora</string>
|
||||
<string name="ttl_hours">%d ore</string>
|
||||
<string name="disappearing_messages_are_prohibited">I messaggi a tempo sono vietati in questo gruppo.</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_month">%d mese</string>
|
||||
<string name="ttl_months">%d mesi</string>
|
||||
<string name="ttl_mth">%dmese</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_w">%dset</string>
|
||||
<string name="ttl_week">%d settimana</string>
|
||||
<string name="ttl_weeks">%d settimane</string>
|
||||
<string name="v4_2_group_links">Link del gruppo</string>
|
||||
<string name="group_members_can_delete">I membri del gruppo possono eliminare irreversibilmente i messaggi inviati.</string>
|
||||
<string name="group_members_can_send_dms">I membri del gruppo possono inviare messaggi diretti.</string>
|
||||
<string name="group_members_can_send_disappearing">I membri del gruppo possono inviare messaggi a tempo.</string>
|
||||
<string name="group_members_can_send_voice">I membri del gruppo possono inviare messaggi vocali.</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Confronta i codici di sicurezza con i tuoi contatti.</string>
|
||||
<string name="v4_4_disappearing_messages">Messaggi a tempo</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Nascondi la schermata dell\'app nelle app recenti.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore verrà usato per memorizzare in modo sicuro la password dopo il riavvio dell\'app o la modifica della password; consentirà di ricevere le notifiche.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Nota bene</b>: NON potrai recuperare o cambiare la password se la perdi.</string>
|
||||
<string name="change_database_passphrase_question">Cambiare password del database\?</string>
|
||||
<string name="confirm_new_passphrase">Conferma password nuova…</string>
|
||||
<string name="current_passphrase">Password attuale…</string>
|
||||
<string name="database_encrypted">Database crittografato!</string>
|
||||
<string name="database_passphrase_will_be_updated">La password di crittografia del database verrà aggiornata.</string>
|
||||
<string name="database_will_be_encrypted">Il database verrà crittografato.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">Il database verrà crittografato e la password conservata nel Keystore.</string>
|
||||
<string name="delete_files_and_media_question">Eliminare i file e i multimediali\?</string>
|
||||
<string name="delete_messages">Elimina messaggi</string>
|
||||
<string name="delete_messages_after">Elimina messaggi dopo</string>
|
||||
<string name="total_files_count_and_size">%d file con dimensione totale di %s</string>
|
||||
<string name="enable_automatic_deletion_question">Attivare l\'eliminazione automatica dei messaggi\?</string>
|
||||
<string name="encrypt_database_question">Crittografare il database\?</string>
|
||||
<string name="encrypt_database">Crittografare</string>
|
||||
<string name="error_changing_message_deletion">Errore nella modifica dell\'impostazione</string>
|
||||
<string name="error_encrypting_database">Errore nella crittografia del database</string>
|
||||
<string name="your_settings">Le tue impostazioni</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Verrai connesso/a al gruppo quando il dispositivo dell\'host del gruppo sarà in linea, attendi o controlla più tardi!</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Se hai ricevuto il link di invito a <xliff:g id="appName">SimpleX Chat</xliff:g>, puoi aprirlo nel tuo browser:</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobile: tocca <b>Apri nell\'app mobile</b>, quindi <b>Connetti</b> nell\'app.</string>
|
||||
<string name="no_details">nessun dettaglio</string>
|
||||
<string name="add_contact">Link di invito una tantum</string>
|
||||
<string name="only_stored_on_members_devices">(memorizzato solo dai membri del gruppo)</string>
|
||||
<string name="toast_permission_denied">Autorizzazione negata!</string>
|
||||
<string name="reject_contact_button">Rifiuta</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scansiona o incolla dagli appunti)</string>
|
||||
<string name="scan_QR_code">Scansiona codice QR</string>
|
||||
<string name="add_contact_or_create_group">Inizia una nuova conversazione</string>
|
||||
<string name="chat_help_tap_button">Tocca il pulsante</string>
|
||||
<string name="thank_you_for_installing_simplex">Grazie per aver installato <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="to_connect_via_link_title">Per connettersi via link</string>
|
||||
<string name="to_share_with_your_contact">(da condividere con il tuo contatto)</string>
|
||||
<string name="to_start_a_new_chat_help_header">Per iniziare una nuova chat</string>
|
||||
<string name="use_camera_button">Usa la fotocamera</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Puoi <font color="#0088ff">connetterti con gli sviluppatori di <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per porre domande e ricevere aggiornamenti</font>.</string>
|
||||
<string name="invalid_contact_link">Link non valido!</string>
|
||||
<string name="invalid_QR_code">Codice QR non valido</string>
|
||||
<string name="image_descr_link_preview">immagine di anteprima link</string>
|
||||
<string name="mark_read">Segna come già letto</string>
|
||||
<string name="mark_unread">Segna come non letto</string>
|
||||
<string name="icon_descr_more_button">Altro</string>
|
||||
<string name="mute_chat">Silenzia</string>
|
||||
<string name="image_descr_profile_image">immagine del profilo</string>
|
||||
<string name="icon_descr_profile_image_placeholder">segnaposto immagine del profilo</string>
|
||||
<string name="image_descr_qr_code">Codice QR</string>
|
||||
<string name="set_contact_name">Imposta il nome del contatto</string>
|
||||
<string name="icon_descr_settings">Impostazioni</string>
|
||||
<string name="show_QR_code">Mostra codice QR</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">La connessione che hai accettato verrà annullata!</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Il contatto con cui hai condiviso questo link NON sarà in grado di connettersi!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Questo non è un link di connessione valido!</string>
|
||||
<string name="this_QR_code_is_not_a_link">Questo codice QR non è un link!</string>
|
||||
<string name="unmute_chat">Riattiva audio</string>
|
||||
<string name="contact_wants_to_connect_with_you">vuole connettersi con te!</string>
|
||||
<string name="image_descr_simplex_logo">Logo di <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="icon_descr_address">Indirizzo di <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="icon_descr_simplex_team">Squadra di <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="you_accepted_connection">Hai accettato la connessione</string>
|
||||
<string name="you_invited_your_contact">Hai invitato il contatto</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Il tuo profilo di chat verrà inviato
|
||||
\nal tuo contatto</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Il tuo contatto può scansionare il codice QR dall\'app.</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Il tuo contatto deve essere in linea per completare la connessione.
|
||||
\nPuoi annullare questa connessione e rimuovere il contatto (e riprovare più tardi con un link nuovo).</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Verrai connesso/a quando la tua richiesta di connessione verrà accettata, attendi o controlla più tardi!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Verrai connesso/a quando il dispositivo del tuo contatto sarà in linea, attendi o controlla più tardi!</string>
|
||||
<string name="incorrect_code">Codice di sicurezza sbagliato!</string>
|
||||
<string name="smp_servers_invalid_address">Indirizzo del server non valido!</string>
|
||||
<string name="markdown_help">Aiuto sul markdown</string>
|
||||
<string name="markdown_in_messages">Markdown nei messaggi</string>
|
||||
<string name="mark_code_verified">Segna come verificato/a</string>
|
||||
<string name="one_time_link">Link di invito una tantum</string>
|
||||
<string name="paste_button">Incolla</string>
|
||||
<string name="paste_connection_link_below_to_connect">Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto.</string>
|
||||
<string name="smp_servers_preset_server">Server preimpostato</string>
|
||||
<string name="smp_servers_preset_address">Indirizzo server preimpostato</string>
|
||||
<string name="smp_servers_save">Salva i server</string>
|
||||
<string name="scan_code">Scansiona codice</string>
|
||||
<string name="scan_code_from_contacts_app">Scansiona il codice di sicurezza dall\'app del tuo contatto.</string>
|
||||
<string name="smp_servers_scan_qr">Scansiona codice QR del server</string>
|
||||
<string name="security_code">Codice di sicurezza</string>
|
||||
<string name="chat_with_the_founder">Invia domande e idee</string>
|
||||
<string name="send_us_an_email">Inviaci un\'email</string>
|
||||
<string name="smp_servers_test_failed">Test del server fallito!</string>
|
||||
<string name="share_invitation_link">Condividi link di invito</string>
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="is_not_verified">%s non è verificato/a</string>
|
||||
<string name="is_verified">%s è verificato/a</string>
|
||||
<string name="smp_servers">Server SMP</string>
|
||||
<string name="smp_servers_test_some_failed">Alcuni server hanno fallito il test:</string>
|
||||
<string name="smp_servers_test_server">Testa server</string>
|
||||
<string name="smp_servers_test_servers">Testa i server</string>
|
||||
<string name="this_string_is_not_a_connection_link">Questa stringa non è un link di connessione!</string>
|
||||
<string name="to_verify_compare">Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi.</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">Usa per connessioni nuove</string>
|
||||
<string name="smp_servers_use_server">Usa il server</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">Puoi anche connetterti cliccando il link. Se si apre nel browser, clicca il pulsante <b>Apri nell\'app mobile</b>.</string>
|
||||
<string name="your_profile_will_be_sent">Il tuo profilo di chat verrà inviato al tuo contatto</string>
|
||||
<string name="your_contact_address">Il tuo indirizzo di contatto</string>
|
||||
<string name="smp_servers_your_server">Il tuo server</string>
|
||||
<string name="smp_servers_your_server_address">L\'indirizzo del tuo server</string>
|
||||
<string name="your_simplex_contact_address">Il tuo indirizzo di contatto di <xliff:g id="appName">SimpleX</xliff:g>.</string>
|
||||
<string name="network_disable_socks_info">Se confermi, i server di messaggistica saranno in grado di vedere il tuo indirizzo IP e il tuo fornitore, a quali server ti stai connettendo.</string>
|
||||
<string name="install_simplex_chat_for_terminal">Installa <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per terminale</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi.</string>
|
||||
<string name="network_and_servers">Rete e server</string>
|
||||
<string name="network_settings_title">Impostazioni di rete</string>
|
||||
<string name="network_use_onion_hosts_no">No</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione.</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Gli host Onion saranno necessari per la connessione.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Gli host Onion verranno usati quando disponibili.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Gli host Onion verranno usati quando disponibili.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Gli host Onion non verranno usati.</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Gli host Onion non verranno usati.</string>
|
||||
<string name="rate_the_app">Valuta l\'app</string>
|
||||
<string name="network_use_onion_hosts_required">Obbligatorio</string>
|
||||
<string name="save_servers_button">Salva</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">I server WebRTC ICE salvati verranno rimossi.</string>
|
||||
<string name="share_link">Condividi link</string>
|
||||
<string name="star_on_github">Stella su GitHub</string>
|
||||
<string name="update_onion_hosts_settings_question">Aggiornare l\'impostazione degli host .onion\?</string>
|
||||
<string name="network_disable_socks">Usare una connessione internet diretta\?</string>
|
||||
<string name="network_use_onion_hosts">Usa gli host .onion</string>
|
||||
<string name="network_enable_socks">Usare il proxy SOCKS\?</string>
|
||||
<string name="network_socks_toggle">Usa il proxy SOCKS (porta 9050)</string>
|
||||
<string name="use_simplex_chat_servers__question">Usare i server di <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\?</string>
|
||||
<string name="using_simplex_chat_servers">Stai usando i server di <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
|
||||
<string name="network_use_onion_hosts_prefer">Quando disponibili</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te. Non perderai i tuoi contatti se in seguito lo elimini.</string>
|
||||
<string name="your_ICE_servers">I tuoi server ICE</string>
|
||||
<string name="your_SMP_servers">I tuoi server SMP</string>
|
||||
<string name="italic">corsivo</string>
|
||||
<string name="callstatus_missed">chiamata persa</string>
|
||||
<string name="callstate_received_answer">risposta ricevuta…</string>
|
||||
<string name="callstate_received_confirmation">conferma ricevuta…</string>
|
||||
<string name="callstatus_rejected">chiamata rifiutata</string>
|
||||
<string name="save_and_notify_contact">Salva e avvisa il contatto</string>
|
||||
<string name="save_and_notify_contacts">Salva e avvisa i contatti</string>
|
||||
<string name="save_and_notify_group_members">Salva e avvisa i membri del gruppo</string>
|
||||
<string name="save_preferences_question">Salvare le preferenze\?</string>
|
||||
<string name="secret">segreto</string>
|
||||
<string name="callstate_starting">avvio…</string>
|
||||
<string name="strikethrough">barrato</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La piattaforma di messaggistica che protegge la tua privacy e sicurezza.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Il profilo è condiviso solo con i tuoi contatti.</string>
|
||||
<string name="callstate_waiting_for_answer">in attesa di risposta…</string>
|
||||
<string name="callstate_waiting_for_confirmation">in attesa di conferma…</string>
|
||||
<string name="we_do_not_store_contacts_or_messages_on_servers">Non memorizziamo nessuno dei tuoi contatti o messaggi (una volta recapitati) sui server.</string>
|
||||
<string name="section_title_welcome_message">MESSAGGIO DI BENVENUTO</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Puoi usare il markdown per formattare i messaggi:</string>
|
||||
<string name="you_control_your_chat">Sei tu a controllare la tua chat!</string>
|
||||
<string name="your_current_profile">Il tuo profilo attuale</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.
|
||||
\n
|
||||
\nI server di <xliff:g id="appName">SimpleX</xliff:g> non possono vedere il tuo profilo.</string>
|
||||
<string name="ignore">Ignora</string>
|
||||
<string name="immune_to_spam_and_abuse">Immune a spam e abusi</string>
|
||||
<string name="incoming_audio_call">Chiamata in arrivo</string>
|
||||
<string name="incoming_video_call">Videochiamata in arrivo</string>
|
||||
<string name="onboarding_notifications_mode_service">Istantaneo</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Può essere cambiato in seguito via impostazioni.</string>
|
||||
<string name="make_private_connection">Crea una connessione privata</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Molte persone hanno chiesto: <i>se <xliff:g id="appName">SimpleX</xliff:g> non ha identificatori utente, come può recapitare i messaggi\?</i></string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Solo i dispositivi client memorizzano i profili utente, i contatti, i gruppi e i messaggi inviati con <b>crittografia end-to-end a 2 livelli</b>.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Protocollo e codice open source: chiunque può gestire i server.</string>
|
||||
<string name="paste_the_link_you_received">Incolla il link ricevuto</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Le persone possono connettersi a te solo tramite i link che condividi.</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periodico</string>
|
||||
<string name="privacy_redefined">Privacy ridefinita</string>
|
||||
<string name="onboarding_notifications_mode_title">Notifiche private</string>
|
||||
<string name="read_more_in_github_with_link">Maggiori informazioni nel nostro <font color="#0088ff">repository GitHub</font>.</string>
|
||||
<string name="read_more_in_github">Maggiori informazioni nel nostro repository GitHub.</string>
|
||||
<string name="reject">Rifiuta</string>
|
||||
<string name="first_platform_without_user_ids">La prima piattaforma senza alcun identificatore utente – privata by design.</string>
|
||||
<string name="next_generation_of_private_messaging">La nuova generazione di messaggistica privata</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Per proteggere la privacy, invece degli ID utente usati da tutte le altre piattaforme, <xliff:g id="appName">SimpleX</xliff:g> dispone di identificatori per le code dei messaggi, separati per ciascuno dei tuoi contatti.</string>
|
||||
<string name="use_chat">Usa la chat</string>
|
||||
<string name="icon_descr_video_call">videochiamata</string>
|
||||
<string name="video_call_no_encryption">videochiamata (non crittografata e2e)</string>
|
||||
<string name="onboarding_notifications_mode_off">Quando l\'app è in esecuzione</string>
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> vuole connettersi con te via</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Puoi controllare attraverso quale/i server <b>ricevere</b> i messaggi, i tuoi contatti – i server che usi per inviare loro i messaggi.</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Può accadere quando:
|
||||
\n1. I messaggi scadono sul server se non sono stati ricevuti per 30 giorni,
|
||||
\n2. Il server usato per ricevere i messaggi da questo contatto è stato aggiornato e riavviato.
|
||||
\n3. La connessione è compromessa.
|
||||
\nConnettiti agli sviluppatori tramite Impostazioni per ricevere aggiornamenti riguardo i server.
|
||||
\nAggiungeremo la ridondanza del server per prevenire la perdita di messaggi.</string>
|
||||
<string name="icon_descr_call_rejected">Chiamata rifiutata</string>
|
||||
<string name="icon_descr_call_missed">Chiamata persa</string>
|
||||
<string name="status_no_e2e_encryption">nessuna crittografia e2e</string>
|
||||
<string name="open_verb">Apri</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Apri <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per accettare la chiamata</string>
|
||||
<string name="call_connection_peer_to_peer">peer-to-peer</string>
|
||||
<string name="icon_descr_call_pending_sent">Chiamata in sospeso</string>
|
||||
<string name="privacy_and_security">Privacy e sicurezza</string>
|
||||
<string name="protect_app_screen">Proteggi la schermata dell\'app</string>
|
||||
<string name="show_call_on_lock_screen">Mostra</string>
|
||||
<string name="alert_title_skipped_messages">Messaggi saltati</string>
|
||||
<string name="icon_descr_speaker_off">Altoparlante spento</string>
|
||||
<string name="icon_descr_speaker_on">Altoparlante acceso</string>
|
||||
<string name="call_connection_via_relay">via relay</string>
|
||||
<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="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>
|
||||
<string name="your_privacy">La tua privacy</string>
|
||||
<string name="import_database_confirmation">Importa</string>
|
||||
<string name="import_database_question">Importare il database della chat\?</string>
|
||||
<string name="import_database">Importa database</string>
|
||||
<string name="settings_section_title_incognito">Modalità incognito</string>
|
||||
<string name="settings_section_title_messages">MESSAGGI</string>
|
||||
<string name="new_database_archive">Nuovo archivio database</string>
|
||||
<string name="old_database_archive">Vecchio archivio del database</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Riavvia l\'app per creare un profilo di chat nuovo.</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Riavvia l\'app per usare il database della chat importato.</string>
|
||||
<string name="run_chat_section">AVVIA CHAT</string>
|
||||
<string name="send_link_previews">Invia anteprime dei link</string>
|
||||
<string name="set_password_to_export">Imposta la password per esportare</string>
|
||||
<string name="settings_section_title_settings">IMPOSTAZIONI</string>
|
||||
<string name="settings_section_title_socks">PROXY SOCKS</string>
|
||||
<string name="stop_chat_confirmation">Ferma</string>
|
||||
<string name="stop_chat_question">Fermare la chat\?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Ferma la chat per esportare, importare o eliminare il database della chat. Non potrai ricevere e inviare messaggi mentre la chat è ferma.</string>
|
||||
<string name="settings_section_title_support">SUPPORTA SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_themes">TEMI</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</string>
|
||||
<string name="transfer_images_faster">Trasferisci immagini più velocemente</string>
|
||||
<string name="settings_section_title_you">TU</string>
|
||||
<string name="your_chat_database">Il tuo database della chat</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Il tuo attuale database di chat verrà ELIMINATO e SOSTITUITO con quello importato.
|
||||
\nQuesta azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</string>
|
||||
<string name="alert_title_group_invitation_expired">Invito scaduto!</string>
|
||||
<string name="group_invitation_item_description">invito al gruppo <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="icon_descr_add_members">Invita membri</string>
|
||||
<string name="join_group_button">Entra</string>
|
||||
<string name="join_group_question">Entrare nel gruppo\?</string>
|
||||
<string name="join_group_incognito_button">Entra in incognito</string>
|
||||
<string name="joining_group">Ingresso nel gruppo</string>
|
||||
<string name="keychain_error">Errore del portachiavi</string>
|
||||
<string name="leave_group_button">Esci</string>
|
||||
<string name="leave_group_question">Uscire dal gruppo\?</string>
|
||||
<string name="open_chat">Apri chat</string>
|
||||
<string name="restore_passphrase_not_found_desc">Password non trovata nel Keystore, inseriscila a mano. Potrebbe essere successo se hai ripristinato i dati dell\'app usando uno strumento di backup. In caso contrario, contatta gli sviluppatori.</string>
|
||||
<string name="restore_database_alert_desc">Inserisci la password precedente dopo aver ripristinato il backup del database. Questa azione non può essere annullata.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Conserva la password in modo sicuro, NON potrai accedere alla chat se la perdi.</string>
|
||||
<string name="restore_database_alert_confirm">Ripristina</string>
|
||||
<string name="restore_database">Ripristina backup del database</string>
|
||||
<string name="restore_database_alert_title">Ripristinare il backup del database\?</string>
|
||||
<string name="database_restore_error">Errore di ripristino del database</string>
|
||||
<string name="save_archive">Salva archivio</string>
|
||||
<string name="save_passphrase_and_open_chat">Salva la password e apri la chat</string>
|
||||
<string name="database_backup_can_be_restored">Il tentativo di cambiare la password del database non è stato completato.</string>
|
||||
<string name="unknown_database_error_with_info">Errore del database sconosciuto: %s</string>
|
||||
<string name="unknown_error">Errore sconosciuto</string>
|
||||
<string name="wrong_passphrase">Password del database sbagliata</string>
|
||||
<string name="wrong_passphrase_title">Password sbagliata!</string>
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Sei stato/a invitato/a al gruppo. Entra per connetterti con i suoi membri.</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puoi avviare la chat tramite Impostazioni -> Database o riavviando l\'app.</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sei entrato/a in questo gruppo. Connessione al membro del gruppo invitante.</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata.</string>
|
||||
<string name="group_member_status_invited">invitato</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">invitato via link del tuo gruppo</string>
|
||||
<string name="rcv_group_event_member_added">invitato <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_left">uscito/a</string>
|
||||
<string name="group_member_status_left">uscito/a</string>
|
||||
<string name="group_member_role_member">membro</string>
|
||||
<string name="group_member_role_owner">proprietario</string>
|
||||
<string name="group_member_status_removed">rimosso</string>
|
||||
<string name="rcv_group_event_member_deleted">rimosso <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_user_deleted">sei stato/a rimosso/a</string>
|
||||
<string name="group_invitation_tap_to_join">Tocca per entrare</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Toccare per entrare in incognito</string>
|
||||
<string name="alert_message_no_group">Questo gruppo non esiste più.</string>
|
||||
<string name="rcv_group_event_updated_group_profile">profilo del gruppo aggiornato</string>
|
||||
<string name="you_are_invited_to_group">Sei stato/a invitato/a al gruppo</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">hai cambiato indirizzo</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">hai cambiato l\'indirizzo per %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">hai cambiato il tuo ruolo in %s</string>
|
||||
<string name="snd_group_event_changed_member_role">hai cambiato il ruolo di %s in %s</string>
|
||||
<string name="you_joined_this_group">Sei entrato/a in questo gruppo</string>
|
||||
<string name="snd_group_event_user_left">sei uscito/a</string>
|
||||
<string name="you_rejected_group_invitation">Hai rifiutato l\'invito al gruppo</string>
|
||||
<string name="snd_group_event_member_deleted">hai rimosso <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Stai usando un profilo in incognito per questo gruppo: per impedire la condivisione del tuo profilo principale non è consentito invitare contatti</string>
|
||||
<string name="you_sent_group_invitation">Hai inviato un invito al gruppo</string>
|
||||
<string name="button_add_members">Invita membri</string>
|
||||
<string name="invite_to_group_button">Invita al gruppo</string>
|
||||
<string name="button_leave_group">Esci dal gruppo</string>
|
||||
<string name="info_row_local_name">Nome locale</string>
|
||||
<string name="member_info_section_title_member">MEMBRO</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Il membro verrà rimosso dal gruppo, non è reversibile!</string>
|
||||
<string name="new_member_role">Nuovo ruolo del membro</string>
|
||||
<string name="no_contacts_selected">Nessun contatto selezionato</string>
|
||||
<string name="no_contacts_to_add">Nessun contatto da aggiungere</string>
|
||||
<string name="only_group_owners_can_change_prefs">Solo i proprietari del gruppo possono modificarne le preferenze.</string>
|
||||
<string name="remove_member_confirmation">Rimuovi</string>
|
||||
<string name="button_remove_member">Rimuovi membro</string>
|
||||
<string name="role_in_group">Ruolo</string>
|
||||
<string name="select_contacts">Seleziona i contatti</string>
|
||||
<string name="button_send_direct_message">Invia messaggio diretto</string>
|
||||
<string name="skip_inviting_button">Salta l\'invito di membri</string>
|
||||
<string name="switch_verb">Cambia</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contatto/i selezionato/i</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBRI</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini.</string>
|
||||
<string name="invite_prohibited_description">Stai tentando di invitare un contatto con cui hai condiviso un profilo in incognito nel gruppo in cui stai usando il tuo profilo principale</string>
|
||||
<string name="group_info_member_you">tu: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">La modalità in incognito non è supportata qui: il tuo profilo principale verrà inviato ai membri del gruppo</string>
|
||||
<string name="incognito_info_protects">La modalità in incognito protegge la privacy del nome e dell\'immagine del tuo profilo principale: per ogni nuovo contatto viene creato un nuovo profilo casuale.</string>
|
||||
<string name="conn_level_desc_indirect">indiretta (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="incognito_info_allows">Permette di avere molte connessioni anonime senza dati condivisi tra di loro in un unico profilo di chat.</string>
|
||||
<string name="theme_light">Chiaro</string>
|
||||
<string name="network_status">Stato della rete</string>
|
||||
<string name="network_option_ping_interval">Intervallo PING</string>
|
||||
<string name="network_option_protocol_timeout">Scadenza del protocollo</string>
|
||||
<string name="receiving_via">Ricezione via</string>
|
||||
<string name="network_options_reset_to_defaults">Ripristina i predefiniti</string>
|
||||
<string name="network_options_revert">Annulla</string>
|
||||
<string name="network_options_save">Salva</string>
|
||||
<string name="save_group_profile">Salva il profilo del gruppo</string>
|
||||
<string name="network_option_seconds_label">sec</string>
|
||||
<string name="sending_via">Invio tramite</string>
|
||||
<string name="conn_stats_section_title_servers">SERVER</string>
|
||||
<string name="switch_receiving_address">Cambia indirizzo di ricezione</string>
|
||||
<string name="theme_system">Sistema</string>
|
||||
<string name="network_option_tcp_connection_timeout">Scadenza connessione TCP</string>
|
||||
<string name="group_is_decentralized">Il gruppo è completamente decentralizzato: è visibile solo ai membri.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito.</string>
|
||||
<string name="incognito_info_find">Per trovare il profilo usato per una connessione in incognito, tocca il nome del contatto o del gruppo in cima alla chat.</string>
|
||||
<string name="update_network_settings_confirmation">Aggiorna</string>
|
||||
<string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</string>
|
||||
<string name="incognito_info_share">Quando condividi un profilo in incognito con qualcuno, questo profilo verrà utilizzato per i gruppi a cui ti invitano.</string>
|
||||
<string name="group_main_profile_sent">Il tuo profilo di chat verrà inviato ai membri del gruppo</string>
|
||||
<string name="incognito_random_profile">Il tuo profilo casuale</string>
|
||||
<string name="message_deletion_prohibited">L\'eliminazione irreversibile dei messaggi è vietata in questa chat.</string>
|
||||
<string name="chat_preferences_no">no</string>
|
||||
<string name="chat_preferences_off">off</string>
|
||||
<string name="feature_off">off</string>
|
||||
<string name="chat_preferences_on">on</string>
|
||||
<string name="only_you_can_delete_messages">Solo tu puoi eliminare irreversibilmente i messaggi (il tuo contatto può contrassegnarli per l\'eliminazione).</string>
|
||||
<string name="only_you_can_send_disappearing">Solo tu puoi inviare messaggi a tempo.</string>
|
||||
<string name="only_your_contact_can_delete">Solo il tuo contatto può eliminare irreversibilmente i messaggi (tu puoi contrassegnarli per l\'eliminazione).</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Solo il tuo contatto può inviare messaggi a tempo.</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Proibisci l\'invio di messaggi a tempo.</string>
|
||||
<string name="prohibit_sending_voice_messages">Proibisci l\'invio di messaggi vocali.</string>
|
||||
<string name="feature_received_prohibited">ricevuto, vietato</string>
|
||||
<string name="reset_color">Ripristina i colori</string>
|
||||
<string name="save_color">Salva colore</string>
|
||||
<string name="accept_feature_set_1_day">Imposta 1 giorno</string>
|
||||
<string name="set_group_preferences">Imposta le preferenze del gruppo</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="voice_messages">Messaggi vocali</string>
|
||||
<string name="chat_preferences_yes">sì</string>
|
||||
<string name="chat_preferences_you_allow">Lo consenti</string>
|
||||
<string name="your_preferences">Le tue preferenze</string>
|
||||
<string name="v4_3_improved_server_configuration">Configurazione del server migliorata</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Eliminazione irreversibile del messaggio</string>
|
||||
<string name="message_deletion_prohibited_in_chat">L\'eliminazione irreversibile dei messaggi è vietata in questo gruppo.</string>
|
||||
<string name="v4_3_voice_messages_desc">Max 40 secondi, ricevuto istantaneamente.</string>
|
||||
<string name="new_in_version">Novità nella %s</string>
|
||||
<string name="only_you_can_send_voice">Solo tu puoi inviare messaggi vocali.</string>
|
||||
<string name="only_your_contact_can_send_voice">Solo il tuo contatto può inviare messaggi vocali.</string>
|
||||
<string name="prohibit_message_deletion">Proibisci l\'eliminazione irreversibile dei messaggi.</string>
|
||||
<string name="prohibit_direct_messages">Proibisci l\'invio di messaggi diretti ai membri.</string>
|
||||
<string name="prohibit_sending_disappearing">Proibisci l\'invio di messaggi a tempo.</string>
|
||||
<string name="prohibit_sending_voice">Proibisci l\'invio di messaggi vocali.</string>
|
||||
<string name="v4_2_security_assessment">Valutazione della sicurezza</string>
|
||||
<string name="v4_2_security_assessment_desc">La sicurezza di SimpleX Chat è stata verificata da Trail of Bits.</string>
|
||||
<string name="v4_3_voice_messages">Messaggi vocali</string>
|
||||
<string name="voice_prohibited_in_this_chat">I messaggi vocali sono vietati in questa chat.</string>
|
||||
<string name="voice_messages_are_prohibited">I messaggi vocali sono vietati in questo gruppo.</string>
|
||||
<string name="whats_new">Novità</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Con messaggio di benvenuto facoltativo.</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">I tuoi contatti possono consentire l\'eliminazione completa dei messaggi.</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Privacy e sicurezza migliorate</string>
|
||||
<string name="v4_4_live_messages">Messaggi in diretta</string>
|
||||
<string name="v4_4_live_messages_desc">I destinatari vedono gli aggiornamenti mentre li digiti.</string>
|
||||
<string name="v4_4_disappearing_messages_desc">I messaggi inviati verranno eliminati dopo il tempo impostato.</string>
|
||||
<string name="v4_4_verify_connection_security">Verifica la sicurezza della connessione</string>
|
||||
<string name="chat_item_ttl_none">mai</string>
|
||||
<string name="new_passphrase">Nuova password…</string>
|
||||
<string name="no_received_app_files">Nessun file ricevuto o inviato</string>
|
||||
<string name="notifications_will_be_hidden">Le notifiche verranno mostrate solo fino all\'arresto dell\'app!</string>
|
||||
<string name="enter_correct_current_passphrase">Inserisci la password attuale corretta.</string>
|
||||
<string name="store_passphrase_securely">Conserva la password in modo sicuro, NON potrai cambiarla se la perdi.</string>
|
||||
<string name="remove_passphrase">Rimuovi</string>
|
||||
<string name="remove_passphrase_from_keychain">Rimuovere la password dal Keystore\?</string>
|
||||
<string name="save_passphrase_in_keychain">Salva la password nel Keystore</string>
|
||||
<string name="chat_item_ttl_seconds">%s secondo/i</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Ferma la chat per attivare le azioni del database.</string>
|
||||
<string name="delete_files_and_media_desc">Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione.</string>
|
||||
<string name="enable_automatic_deletion_message">Questa azione non può essere annullata: i messaggi inviati e ricevuti prima di quanto selezionato verranno eliminati. Potrebbe richiedere diversi minuti.</string>
|
||||
<string name="update_database">Aggiorna</string>
|
||||
<string name="update_database_passphrase">Aggiorna la password del database</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Devi inserire la password ogni volta che si avvia l\'app: non viene memorizzata sul dispositivo.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Devi usare la versione più recente del tuo database della chat SOLO su un dispositivo, altrimenti potresti non ricevere più i messaggi da alcuni contatti.</string>
|
||||
<string name="database_is_not_encrypted">Il database della chat non è crittografato: imposta la password per proteggerlo.</string>
|
||||
<string name="icon_descr_cancel_live_message">Annulla messaggio in diretta</string>
|
||||
<string name="feature_offered_item">offerto %s</string>
|
||||
<string name="feature_offered_item_with_param">offerto %s: %2s</string>
|
||||
<string name="feature_cancelled_item">annullato %s</string>
|
||||
<string name="network_option_ping_count">Conteggio PING</string>
|
||||
<string name="app_version_title">Versione dell\'app</string>
|
||||
<string name="core_version">Versione core: v%s</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="app_version_code">Build dell\'app: %s</string>
|
||||
<string name="app_version_name">Versione app: v%s</string>
|
||||
<string name="core_build_timestamp">Core compilato il: %s</string>
|
||||
<string name="smp_servers_per_user">I server per le nuove connessioni del profilo di chat attuale</string>
|
||||
<string name="users_add">Aggiungi profilo</string>
|
||||
<string name="users_delete_question">Eliminare il profilo di chat\?</string>
|
||||
<string name="network_session_mode_user_description">Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni profilo di chat presente nell\'app</b>.</string>
|
||||
<string name="delete_files_and_media_all">Elimina tutti i file</string>
|
||||
<string name="users_delete_data_only">Solo dati del profilo locale</string>
|
||||
<string name="messages_section_title">Messaggi</string>
|
||||
<string name="files_and_media_section">File e multimediali</string>
|
||||
<string name="update_network_session_mode_question">Aggiornare la modalità di isolamento del trasporto\?</string>
|
||||
<string name="users_delete_all_chats_deleted">Tutte le chat e i messaggi verranno eliminati. Non è reversibile!</string>
|
||||
<string name="network_session_mode_user">Profilo di chat</string>
|
||||
<string name="network_session_mode_entity_description">Verrà usata una connessione TCP separata (e le credenziali SOCKS) <b> per ogni contatto e membro del gruppo </b>.
|
||||
\n<b> Nota: </b>: se hai molte connessioni, il consumo di batteria e traffico può essere notevolmente superiore e alcune connessioni potrebbero fallire.</string>
|
||||
<string name="network_session_mode_entity">Connessione</string>
|
||||
<string name="messages_section_description">Questa impostazione si applica ai messaggi del profilo di chat attuale</string>
|
||||
<string name="network_session_mode_transport_isolation">Isolamento del trasporto</string>
|
||||
<string name="users_delete_profile_for">Elimina il profilo di chat per</string>
|
||||
<string name="delete_files_and_media_for_all_users">Elimina i file per tutti i profili di chat</string>
|
||||
<string name="failed_to_active_user_title">Errore nel cambio di profilo!</string>
|
||||
<string name="failed_to_create_user_title">Errore nella creazione del profilo!</string>
|
||||
<string name="your_chat_profiles_stored_locally">I tuoi profili di chat sono memorizzati localmente, solo sul tuo dispositivo</string>
|
||||
<string name="error_deleting_user">Errore nell\'eliminazione del profilo utente</string>
|
||||
<string name="users_delete_with_connections">Profilo e connessioni al server</string>
|
||||
<string name="your_chat_profiles">I tuoi profili di chat</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Hai già un profilo chat con lo stesso nome da mostrare. Scegli un altro nome.</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Nome da mostrare doppio!</string>
|
||||
<string name="v4_5_italian_interface_descr">Grazie agli utenti – contribuite via Weblate!</string>
|
||||
<string name="v4_4_french_interface">Interfaccia francese</string>
|
||||
<string name="v4_5_italian_interface">Interfaccia italiana</string>
|
||||
<string name="v4_5_message_draft">Bozza dei messaggi</string>
|
||||
<string name="v4_5_message_draft_descr">Conserva la bozza dell\'ultimo messaggio, con gli allegati.</string>
|
||||
<string name="v4_5_private_filenames">Nomi di file privati</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per profilo di chat (predefinito) o per connessione (BETA).</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Altri miglioramenti sono in arrivo!</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Profili di chat multipli</string>
|
||||
<string name="v4_5_reduced_battery_usage">Consumo di batteria ridotto</string>
|
||||
<string name="v4_4_french_interface_descr">Grazie agli utenti – contribuite via Weblate!</string>
|
||||
<string name="v4_5_transport_isolation">Isolamento del trasporto</string>
|
||||
<string name="v4_5_private_filenames_descr">Per proteggere il fuso orario, i file immagine/vocali usano UTC.</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Nomi e avatar diversi, isolamento del trasporto.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="chat_item_ttl_day">1日</string>
|
||||
<string name="chat_item_ttl_week">1週間</string>
|
||||
<string name="callstatus_accepted">受けた通話</string>
|
||||
<string name="smp_servers_preset_add">既存サーバを追加</string>
|
||||
<string name="group_member_role_admin">管理者</string>
|
||||
<string name="v4_2_group_links_desc">管理者はグループの参加リンクを発行できます。</string>
|
||||
<string name="network_settings">ネットワーク詳細設定</string>
|
||||
<string name="chat_item_ttl_month">1ヶ月</string>
|
||||
<string name="about_simplex">SimpleXについて</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>について</string>
|
||||
<string name="color_primary">アクセント色</string>
|
||||
<string name="accept_contact_button">承諾</string>
|
||||
<string name="accept_connection_request__question">繋がりを承諾しますか?</string>
|
||||
<string name="accept">承諾</string>
|
||||
<string name="accept_feature">承諾</string>
|
||||
<string name="accept_call_on_lock_screen">承諾</string>
|
||||
<string name="accept_contact_incognito_button">シークレットモードで承諾</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">QRコードでサーバを追加</string>
|
||||
<string name="smp_servers_add_to_another_device">別の端末に追加</string>
|
||||
<string name="users_add">プロフィールを追加</string>
|
||||
<string name="smp_servers_add">サーバを追加…</string>
|
||||
<string name="network_enable_socks_info">SOCKSプロキシ(ポート9050)経由で接続しますか?(※設定する前にプロキシ起動が必要※)</string>
|
||||
<string name="users_delete_all_chats_deleted">全チャットとメッセージが削除されます(※元に戻せません※)!</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,407 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="callstatus_error">oproepfout</string>
|
||||
<string name="callstatus_calling">bellen…</string>
|
||||
<string name="call_on_lock_screen">Oproepen op het vergrendelingsscherm:</string>
|
||||
<string name="callstatus_in_progress">gesprek gaande</string>
|
||||
<string name="icon_descr_call_progress">Gesprek gaande</string>
|
||||
<string name="settings_section_title_calls">OPROEPEN</string>
|
||||
<string name="cancel_verb">Annuleren</string>
|
||||
<string name="icon_descr_cancel_file_preview">Bestandsvoorbeeld annuleren</string>
|
||||
<string name="icon_descr_cancel_image_preview">Afbeeldingsvoorbeeld annuleren</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">veranderen van adres…</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">Wijzig</string>
|
||||
<string name="network_settings">Geavanceerde netwerkinstellingen</string>
|
||||
<string name="network_enable_socks_info">Toegang tot de servers via SOCKS proxy op poort 9050\? De proxy moet worden gestart voordat u deze optie inschakelt.</string>
|
||||
<string name="alert_title_cant_invite_contacts">Kan contacten niet uitnodigen</string>
|
||||
<string name="allow_direct_messages">Directe berichten sturen naar leden toestaan.</string>
|
||||
<string name="allow_to_delete_messages">Onherroepelijk wissen van verzonden berichten toestaan.</string>
|
||||
<string name="allow_to_send_voice">Sta toe om spraakberichten te versturen.</string>
|
||||
<string name="chat_is_running">Chat is aktief</string>
|
||||
<string name="clear_chat_menu_action">Clear</string>
|
||||
<string name="chat_database_section">CHAT DATABASE</string>
|
||||
<string name="chat_archive_section">CHAT ARCHIEF</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="chat_database_imported">Chat database geïmporteerd</string>
|
||||
<string name="chat_database_deleted">Chat database verwijderd</string>
|
||||
<string name="chat_item_ttl_week">1 week</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="accept_contact_button">Accepteer</string>
|
||||
<string name="accept_call_on_lock_screen">Accepteer</string>
|
||||
<string name="color_primary">Accent</string>
|
||||
<string name="accept">Accepteer</string>
|
||||
<string name="accept_connection_request__question">Verbindingsverzoek accepteren\?</string>
|
||||
<string name="callstatus_accepted">aanvaarde oproep</string>
|
||||
<string name="accept_contact_incognito_button">Accepteer incognito</string>
|
||||
<string name="smp_servers_preset_add">Vooraf ingestelde servers toevoegen</string>
|
||||
<string name="users_add">Profiel toevoegen</string>
|
||||
<string name="smp_servers_add">Server toevoegen…</string>
|
||||
<string name="smp_servers_add_to_another_device">Toevoegen aan een ander apparaat</string>
|
||||
<string name="v4_2_group_links_desc">Admins kunnen de links naar groepen aanmaken.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Servers toevoegen door QR-codes te scannen.</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
<string name="all_group_members_will_remain_connected">Alle groepsleden blijven verbonden.</string>
|
||||
<string name="allow_verb">"Sta toe."</string>
|
||||
<string name="chat_item_ttl_day">1 dag</string>
|
||||
<string name="accept_feature">Accepteer</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Een willekeurig profiel wordt gestuurd naar het contact waarvan u deze link heeft ontvangen.</string>
|
||||
<string name="network_session_mode_entity_description">Er wordt een aparte TCP-verbinding (en SOCKS-credential) gebruikt <b>voor elk contact en groepslid</b>.
|
||||
\n<b>Let op</b>: als u veel verbindingen hebt, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen mislukken.</string>
|
||||
<string name="icon_descr_audio_call">audio-oproep</string>
|
||||
<string name="icon_descr_audio_on">Geluid aan</string>
|
||||
<string name="settings_audio_video_calls">Audio- en videogesprekken</string>
|
||||
<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">Automatisch contactverzoeken accepteren</string>
|
||||
<string name="bold">vet</string>
|
||||
<string name="incognito_random_profile_description">Een willekeurig profiel wordt naar uw contactpersoon gestuurd</string>
|
||||
<string name="attach">Voeg toe</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Laat onomkeerbare verwijdering van berichten alleen toe als uw contactpersoon u dat toestaat.</string>
|
||||
<string name="allow_to_send_disappearing">Laat verdwijnende berichten toe.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Laat uw contacten spraakberichten versturen.</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Al uw contacten blijven verbonden.</string>
|
||||
<string name="allow_voice_messages_question">Spraakberichten toestaan\?</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Goed voor de batterij</b>. De achtergronddienst 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="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scan QR code</b>: om verbinding te maken met uw contactpersoon die de QR code aan u toont.</string>
|
||||
<string name="integrity_msg_bad_id">Onjuiste bericht-ID</string>
|
||||
<string name="call_already_ended">De oproep is 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">About <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="above_then_preposition_continuation">hierboven, dan:</string>
|
||||
<string name="accept_requests">Verzoeken accepteren</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle chats 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">Laat verdwijnende berichten alleen toe als uw contact dat toestaat.</string>
|
||||
<string name="allow_voice_messages_only_if">Sta spraakberichten alleen toe als uw contactpersoon ze toestaat.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Laat uw contacten onherroepelijk verzonden berichten verwijderen.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Laat uw contacten verdwijnende berichten sturen.</string>
|
||||
<string name="chat_preferences_always">altijd</string>
|
||||
<string name="icon_descr_audio_off">Geluid uit</string>
|
||||
<string name="full_backup">App gegevens back-up</string>
|
||||
<string name="answer_call">Beantwoord de oproep</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore wordt gebruikt om passphrase veilig op te slaan - het laat notificatiedienst werken.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore wordt gebruikt om de passphrase veilig op te slaan nadat u de app opnieuw hebt opgestart of de passphrase hebt 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 als hij draait, er wordt geen achtergronddienst 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="network_session_mode_user_description">Er wordt een aparte TCP-verbinding (en SOCKS-credential) gebruikt <b>voor elk chatprofiel dat u 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">De achtergronddienst draait altijd - meldingen worden getoond 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 contactpersoon aan te maken.</string>
|
||||
<string name="icon_descr_call_ended">Oproep beëindigd</string>
|
||||
<string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief en schakelt de achtergronddienst en periodieke verzoeken om nieuwe berichten uit. U kunt ze opnieuw inschakelen via de 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 getoond terwijl de app draait.</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Zowel u als uw contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contact kunnen verdwijnende berichten sturen.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Zowel u als uw contactpersoon kunnen spraakberichten versturen.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Let op</b>: u kunt de wachtwoordzin NIET herstellen of wijzigen als u deze verliest.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Verbruikt meer batterij</b>! Achtergronddienst draait altijd - meldingen worden getoond zodra de berichten beschikbaar zijn.</string>
|
||||
<string name="icon_descr_cancel_link_preview">link preview 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>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">adres wijzigen voor %s…</string>
|
||||
<string name="invite_prohibited">Kan contact niet uitnodigen!</string>
|
||||
<string name="cannot_access_keychain">Kan geen toegang krijgen tot Keystore om database wachtwoord op te slaan</string>
|
||||
<string name="cannot_receive_file">Kan het bestand niet ontvangen</string>
|
||||
<string name="change_role">Rol wijzigen</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">veranderen van adres…</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">veranderd adres voor jou</string>
|
||||
<string name="rcv_group_event_changed_member_role">rol van %s veranderd in %s</string>
|
||||
<string name="change_member_role_question">Groepsrol wijzigen\?</string>
|
||||
<string name="chat_is_stopped">Chat is gestopt</string>
|
||||
<string name="notifications_mode_periodic_desc">Controleert nieuwe berichten elke 10 minuten gedurende maximaal 1 minuut</string>
|
||||
<string name="rcv_group_event_changed_your_role">uw rol veranderd in %s</string>
|
||||
<string name="chat_archive_header">Chat archief</string>
|
||||
<string name="change_database_passphrase_question">Database wachtwoord wijzigen\?</string>
|
||||
<string name="chat_is_stopped_indication">Chat is gestopt</string>
|
||||
<string name="chat_preferences">Chat voorkeuren</string>
|
||||
<string name="network_session_mode_user">Chat profiel</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="chat_with_developers">Chat 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="clear_verb">Clear</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Vergelijk beveiligingscodes met je contacten.</string>
|
||||
<string name="icon_descr_contact_checked">Contact gecontroleerd</string>
|
||||
<string name="notification_contact_connected">Verbonden</string>
|
||||
<string name="display_name_connecting">Verbinden…</string>
|
||||
<string name="connection_local_display_name">verbinding <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="connection_error">Verbindingsfout</string>
|
||||
<string name="group_member_status_introduced">verbinden (geïntroduceerd)</string>
|
||||
<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="share_one_time_link">"Maak een eenmalige uitnodigings link"</string>
|
||||
<string name="create_address">Adres aanmaken</string>
|
||||
<string name="create_group_link">Groeps link maken</string>
|
||||
<string name="create_group">Maak een geheime groep aan</string>
|
||||
<string name="database_will_be_encrypted">Database wordt versleuteld.</string>
|
||||
<string name="group_member_status_creator">Starter</string>
|
||||
<string name="delete_address__question">Adres verwijderen\?</string>
|
||||
<string name="database_passphrase_and_export">Database wachtwoord zin & exporteren</string>
|
||||
<string name="passphrase_is_different">De wachtwoord zin van de database verschilt van de wachtwoord zin die is opgeslagen in de keystore.</string>
|
||||
<string name="ttl_d">%dd</string>
|
||||
<string name="delete_verb">Verwijderen</string>
|
||||
<string name="delete_after">Verwijderen na</string>
|
||||
<string name="connect_via_link_verb">Verbind</string>
|
||||
<string name="server_connected">verbonden</string>
|
||||
<string name="server_connecting">Verbinden</string>
|
||||
<string name="connect_via_contact_link">Verbinden via contact link\?</string>
|
||||
<string name="connect_via_group_link">Verbinden via groeps link\?</string>
|
||||
<string name="connect_via_invitation_link">Verbinden via uitnodigings link\?</string>
|
||||
<string name="notification_preview_mode_contact">Contact naam</string>
|
||||
<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="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="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>
|
||||
<string name="callstate_connected">verbonden</string>
|
||||
<string name="callstate_connecting">Verbinden…</string>
|
||||
<string name="decentralized">Gedecentraliseerd</string>
|
||||
<string name="create_your_profile">Maak je profiel aan</string>
|
||||
<string name="ttl_day">%d dag</string>
|
||||
<string name="ttl_days">%d dagen</string>
|
||||
<string name="encrypted_with_random_passphrase">De database is versleuteld met een willekeurige wachtwoord zin, u kunt deze wijzigen.</string>
|
||||
<string name="database_encryption_will_be_updated">De wachtwoord zin voor database codering wordt bijgewerkt en opgeslagen in de sleutel kluis.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">De database wordt gecodeerd en de wachtwoord zin wordt opgeslagen in de Keystore.</string>
|
||||
<string name="database_passphrase_will_be_updated">De wachtwoordzin voor databasecodering wordt bijgewerkt.</string>
|
||||
<string name="database_error">Database fout</string>
|
||||
<string name="database_passphrase_is_required">Databases wachtwoord zin is vereist om de chat te openen.</string>
|
||||
<string name="contact_already_exists">Contact bestaat al</string>
|
||||
<string name="icon_descr_call_connecting">Oproep verbinden</string>
|
||||
<string name="button_create_group_link">Maak link</string>
|
||||
<string name="smp_server_test_connect">Verbind</string>
|
||||
<string name="connection_error_auth">Verbindingsfout (AUTH)</string>
|
||||
<string name="smp_server_test_create_queue">Maak een wachtrij</string>
|
||||
<string name="auth_confirm_credential">Bevestig uw inloggegevens</string>
|
||||
<string name="contact_connection_pending">Verbinden…</string>
|
||||
<string name="group_connection_pending">Verbinden…</string>
|
||||
<string name="icon_descr_context">Context icon</string>
|
||||
<string name="copy_verb">Kopiëren</string>
|
||||
<string name="clear_chat_question">Wis gesprek</string>
|
||||
<string name="icon_descr_close_button">Sluiten</string>
|
||||
<string name="clear_chat_button">Chat wissen</string>
|
||||
<string name="alert_title_contact_connection_pending">Contact is nog niet verbonden!</string>
|
||||
<string name="delete_contact_menu_action">Verwijderen</string>
|
||||
<string name="delete_group_menu_action">Verwijderen</string>
|
||||
<string name="clear_verification">Verwijderd verificatie</string>
|
||||
<string name="connect_button">Verbind</string>
|
||||
<string name="connect_via_link">Maak verbinding via link</string>
|
||||
<string name="create_one_time_link">Maak een eenmalige uitnodigings link</string>
|
||||
<string name="colored">gekleurd</string>
|
||||
<string name="callstatus_connecting">Oproep verbinden…</string>
|
||||
<string name="contact_requests">Contact verzoeken</string>
|
||||
<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="status_contact_has_e2e_encryption">contact heeft e2e encryptie</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">contact heeft geen e2e-encryptie</string>
|
||||
<string name="set_password_to_export_desc">De database is versleuteld met een willekeurige wachtwoordzin. Wijzig dit voordat u exporteert.</string>
|
||||
<string name="database_passphrase">Database-wachtwoordzin</string>
|
||||
<string name="confirm_new_passphrase">Bevestig nieuwe wachtwoordzin…</string>
|
||||
<string name="current_passphrase">Huidige wachtwoordzin…</string>
|
||||
<string name="database_encrypted">Database versleuteld!</string>
|
||||
<string name="rcv_group_event_member_connected">verbonden</string>
|
||||
<string name="archive_created_on_ts">Gemaakt op <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="group_member_status_complete">compleet</string>
|
||||
<string name="clear_contacts_selection_button">Duidelijk</string>
|
||||
<string name="group_member_status_connected">verbonden</string>
|
||||
<string name="group_member_status_connecting">Verbinden</string>
|
||||
<string name="group_member_status_accepted">verbinden (geaccepteerd)</string>
|
||||
<string name="group_member_status_announced">verbinden (aangekondigd)</string>
|
||||
<string name="info_row_connection">Verbinding</string>
|
||||
<string name="create_secret_group_title">Maak een geheime groep aan</string>
|
||||
<string name="info_row_database_id">Database ID</string>
|
||||
<string name="chat_preferences_contact_allows">Contact staat toe</string>
|
||||
<string name="contact_preferences">Contact voorkeuren</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken.</string>
|
||||
<string name="theme_dark">Donker</string>
|
||||
<string name="chat_preferences_default">standaard (%s)</string>
|
||||
<string name="delete_chat_archive_question">Chat archief verwijderen\?</string>
|
||||
<string name="delete_archive">Archief verwijderen</string>
|
||||
<string name="delete_contact_question">Verwijder contact\?</string>
|
||||
<string name="delete_chat_profile_question">Chat profiel verwijderen\?</string>
|
||||
<string name="full_deletion">Verwijderen voor iedereen</string>
|
||||
<string name="delete_link">Link verwijderen</string>
|
||||
<string name="conn_level_desc_direct">direct</string>
|
||||
<string name="settings_section_title_develop">ONTWIKKELEN</string>
|
||||
<string name="settings_section_title_device">APPARAAT</string>
|
||||
<string name="delete_files_and_media_all">Verwijder alle bestanden</string>
|
||||
<string name="delete_messages_after">Berichten verwijderen na</string>
|
||||
<string name="direct_messages">Privéberichten</string>
|
||||
<string name="ttl_month">%d maand</string>
|
||||
<string name="delete_image">Verwijder afbeelding</string>
|
||||
<string name="delete_database">Database verwijderen</string>
|
||||
<string name="rcv_group_event_group_deleted">verwijderde groep</string>
|
||||
<string name="delete_files_and_media_question">Bestanden en media verwijderen\?</string>
|
||||
<string name="delete_group_question">Groep verwijderen\?</string>
|
||||
<string name="delete_message__question">Verwijder bericht\?</string>
|
||||
<string name="delete_messages">Verwijder berichten</string>
|
||||
<string name="smp_server_test_delete_queue">Wachtrij verwijderen</string>
|
||||
<string name="delete_files_and_media_for_all_users">Verwijder bestanden voor alle chatprofielen</string>
|
||||
<string name="for_me_only">Verwijder voor mij</string>
|
||||
<string name="button_delete_group">Groep verwijderen</string>
|
||||
<string name="delete_link_question">Link verwijderen\?</string>
|
||||
<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="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="direct_messages_are_prohibited_in_chat">Privéberichten tussen leden zijn in deze groep verboden.</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 deze chat.</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX Lock 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>
|
||||
<string name="display_name__field">Weergavenaam:</string>
|
||||
<string name="display_name_cannot_contain_whitespace">Weergavenaam mag geen spatie bevatten.</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_months">%d maanden</string>
|
||||
<string name="failed_to_create_user_title">Fout bij aanmaken van profiel!</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="button_delete_contact">Verwijder contact</string>
|
||||
<string name="smp_servers_delete_server">Server verwijderen</string>
|
||||
<string name="disappearing_messages_are_prohibited">Verdwijnende berichten zijn verboden in deze groep.</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="ttl_hours">%d uren</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="users_delete_question">Chat profiel verwijderen\?</string>
|
||||
<string name="users_delete_profile_for">Chat profiel verwijderen voor</string>
|
||||
<string name="deleted_description">verwijderd</string>
|
||||
<string name="simplex_link_mode_description">Beschrijving</string>
|
||||
<string name="error_receiving_file">Fout bij ontvangen van bestand</string>
|
||||
<string name="error_joining_group">Fout bij lid worden van groep</string>
|
||||
<string name="error_deleting_group">Fout bij verwijderen van groep</string>
|
||||
<string name="error_deleting_contact">Fout bij het verwijderen van contact</string>
|
||||
<string name="error_deleting_contact_request">Fout bij verwijderen van contact verzoek</string>
|
||||
<string name="full_name__field">Volledige naam:</string>
|
||||
<string name="error_importing_database">Fout bij het importeren van de chat database</string>
|
||||
<string name="encrypt_database_question">Database versleutelen\?</string>
|
||||
<string name="alert_title_no_group">Groep niet gevonden!</string>
|
||||
<string name="group_display_name_field">Weergave naam groep:</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Dubbele weergavenaam!</string>
|
||||
<string name="error_sending_message">Fout bij verzenden van bericht</string>
|
||||
<string name="failed_to_active_user_title">Fout bij wisselen van profiel!</string>
|
||||
<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="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="full_name_optional__prompt">Volledige naam (optioneel)</string>
|
||||
<string name="encrypted_video_call">e2e versleuteld videogesprek</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">Schakel oproepen vanaf het vergrendelscherm in via Instellingen.</string>
|
||||
<string name="icon_descr_hang_up">Ophangen</string>
|
||||
<string name="settings_section_title_help">HELP</string>
|
||||
<string name="settings_experimental_features">Experimentele functies</string>
|
||||
<string name="error_starting_chat">Fout bij het starten van de chat</string>
|
||||
<string name="export_database">Database exporteren</string>
|
||||
<string name="error_deleting_database">Fout bij het verwijderen van de chat database</string>
|
||||
<string name="error_exporting_chat_database">Fout bij het exporteren van de chat database</string>
|
||||
<string name="error_stopping_chat">Fout bij het stoppen van de chat</string>
|
||||
<string name="files_and_media_section">Bestanden en media</string>
|
||||
<string name="error_changing_message_deletion">Fout bij wijzigen van instelling</string>
|
||||
<string name="error_encrypting_database">Fout bij het versleutelen van de database</string>
|
||||
<string name="file_with_path">Bestand: %s</string>
|
||||
<string name="enter_passphrase">Voer wachtwoordzin in…</string>
|
||||
<string name="icon_descr_group_inactive">Groep inactief</string>
|
||||
<string name="alert_message_group_invitation_expired">Groeps uitnodiging is niet meer geldig, deze is verwijderd door de afzender.</string>
|
||||
<string name="snd_group_event_group_profile_updated">groeps 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="error_creating_link_for_group">Fout bij maken van groeps link</string>
|
||||
<string name="error_deleting_link_for_group">Fout bij verwijderen groeps link</string>
|
||||
<string name="group_link">Groeps link</string>
|
||||
<string name="error_changing_role">Fout bij wisselen van rol</string>
|
||||
<string name="error_removing_member">Fout bij verwijderen van lid</string>
|
||||
<string name="info_row_group">Groep</string>
|
||||
<string name="group_full_name_field">Volledige naam groep:</string>
|
||||
<string name="group_preferences">Groeps voorkeuren</string>
|
||||
<string name="feature_enabled">ingeschakeld</string>
|
||||
<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">Groeps leden kunnen verzonden berichten onherroepelijk verwijderen.</string>
|
||||
<string name="group_members_can_send_dms">Groeps leden kunnen directe berichten sturen.</string>
|
||||
<string name="group_members_can_send_voice">Groeps leden kunnen spraak berichten verzenden.</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per chatprofiel (standaard) of per verbinding (BETA).</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Verschillende namen, avatars en transportisolatie.</string>
|
||||
<string name="v4_4_french_interface">Franse interface</string>
|
||||
<string name="error_saving_group_profile">Fout bij opslaan van groeps profiel</string>
|
||||
<string name="encrypted_audio_call">e2e versleutelde audio-oproep</string>
|
||||
<string name="status_e2e_encrypted">e2e versleuteld</string>
|
||||
<string name="edit_verb">Bewerk</string>
|
||||
<string name="enable_automatic_deletion_question">Automatisch verwijderen van berichten aanzetten\?</string>
|
||||
<string name="enter_correct_passphrase">Voer de juiste wachtwoordzin in.</string>
|
||||
<string name="button_edit_group_profile">Groepsprofiel bewerken</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 lid (leden)</string>
|
||||
<string name="smp_servers_enter_manually">Voer de server handmatig in</string>
|
||||
<string name="error_accepting_contact_request">Fout bij het accepteren van een contactverzoek</string>
|
||||
<string name="group_invitation_expired">Groeps uitnodiging verlopen</string>
|
||||
<string name="icon_descr_file">Bestand</string>
|
||||
<string name="section_title_for_console">VOOR CONSOLE</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Groeps proces 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="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">Van 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">Groeps leden kunnen verdwijnende berichten sturen.</string>
|
||||
<string name="ttl_week">%d week</string>
|
||||
<string name="ttl_w">%dw</string>
|
||||
<string name="ttl_weeks">%d weken</string>
|
||||
<string name="v4_2_group_links">Groeps links</string>
|
||||
<string name="encrypted_database">Versleutelde database</string>
|
||||
<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="error_setting_network_config">Fout bij updaten van netwerk configuratie</string>
|
||||
<string name="failed_to_parse_chat_title">Kan chat niet laden</string>
|
||||
<string name="failed_to_parse_chats_title">Kan chats niet laden</string>
|
||||
<string name="simplex_link_mode_full">Volledige link</string>
|
||||
<string name="integrity_msg_duplicate">dubbel bericht</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -1,8 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
|
||||
<string name="thousand_abbreviation">т</string>
|
||||
|
||||
<!-- Connect via Link - MainActivity.kt -->
|
||||
<string name="connect_via_contact_link">Соединиться через ссылку-контакт?</string>
|
||||
<string name="connect_via_invitation_link">Соединиться через ссылку-приглашение?</string>
|
||||
@@ -10,7 +9,6 @@
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Ваш профиль будет отправлен контакту, от которого вы получили эту ссылку.</string>
|
||||
<string name="you_will_join_group">Вы вступите в группу, на которую ссылается эта ссылка.</string>
|
||||
<string name="connect_via_link_verb">Соединиться</string>
|
||||
|
||||
<!-- Server info - ChatModel.kt -->
|
||||
<string name="server_connected">соединено</string>
|
||||
<string name="server_error">ошибка</string>
|
||||
@@ -18,7 +16,6 @@
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Установлено соединение с сервером, через который вы получаете сообщения от этого контакта.</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта.</string>
|
||||
|
||||
<!-- Item Content - ChatModel.kt -->
|
||||
<string name="deleted_description">удалено</string>
|
||||
<string name="marked_deleted_description">помечено к удалению</string>
|
||||
@@ -27,7 +24,6 @@
|
||||
<string name="sender_you_pronoun">вы</string>
|
||||
<string name="unknown_message_format">неизвестный формат сообщения</string>
|
||||
<string name="invalid_message_format">неверный формат сообщения</string>
|
||||
|
||||
<!-- PendingContactConnection - ChatModel.kt -->
|
||||
<string name="connection_local_display_name">connection <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">соединение установлено</string>
|
||||
@@ -41,7 +37,6 @@
|
||||
<string name="description_via_contact_address_link_incognito">инкогнито через ссылку-контакт</string>
|
||||
<string name="description_via_one_time_link">через одноразовую ссылку</string>
|
||||
<string name="description_via_one_time_link_incognito">инкогнито через одноразовую ссылку</string>
|
||||
|
||||
<!-- FormattedText, SimpleX links - ChatModel.kt -->
|
||||
<string name="simplex_link_contact">SimpleX ссылка-контакт</string>
|
||||
<string name="simplex_link_invitation">SimpleX одноразовая ссылка</string>
|
||||
@@ -52,12 +47,10 @@
|
||||
<string name="simplex_link_mode_full">Полная ссылка</string>
|
||||
<string name="simplex_link_mode_browser">В браузере</string>
|
||||
<string name="simplex_link_mode_browser_warning">Использование ссылки в браузере может уменьшить конфиденциальность и безопасность соединения. Ссылки на неизвестные сайты будут красными.</string>
|
||||
|
||||
<!-- SimpleXAPI.kt -->
|
||||
<string name="error_saving_smp_servers">Ошибка при сохранении SMP серверов</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Пожалуйста, проверьте, что адреса SMP серверов имеют правильный формат, каждый адрес на отдельной строке и не повторяется.</string>
|
||||
<string name="error_setting_network_config">Ошибка при сохранении настроек сети</string>
|
||||
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Превышено время соединения</string>
|
||||
<string name="connection_error">Ошибка соединения</string>
|
||||
@@ -90,7 +83,6 @@
|
||||
<string name="smp_server_test_secure_queue">Защита очереди</string>
|
||||
<string name="smp_server_test_delete_queue">Удаление очереди</string>
|
||||
<string name="smp_server_test_disconnect">Разрыв соединения</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Мгновенные уведомления</string>
|
||||
<string name="service_notifications">Мгновенные уведомления!</string>
|
||||
@@ -106,16 +98,13 @@
|
||||
<string name="enter_passphrase_notification_desc">Для получения уведомлений, пожалуйста, введите пароль от базы данных</string>
|
||||
<string name="database_initialization_error_title">Ошибка базы данных</string>
|
||||
<string name="database_initialization_error_desc">Ошибка при инициализации базы данных. Нажмите чтобы узнать больше</string>
|
||||
|
||||
<!-- SimpleX Chat foreground Service -->
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> сервис</string>
|
||||
<string name="simplex_service_notification_text">Приём сообщений…</string>
|
||||
<string name="hide_notification">Скрыть</string>
|
||||
|
||||
<!-- Notification channels -->
|
||||
<string name="ntf_channel_messages">SimpleX Chat сообщения</string>
|
||||
<string name="ntf_channel_calls">SimpleX Chat звонки</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="settings_notifications_mode_title">Сервис уведомлений</string>
|
||||
<string name="settings_notification_preview_mode_title">Показывать уведомления</string>
|
||||
@@ -136,12 +125,10 @@
|
||||
<string name="notification_preview_new_message">новое сообщение</string>
|
||||
<string name="notification_new_contact_request">Новый запрос на соединение</string>
|
||||
<string name="notification_contact_connected">Соединен(а)</string>
|
||||
|
||||
<!-- local authentication notice - SimpleXAPI.kt -->
|
||||
<string name="la_notice_title_simplex_lock">Блокировка SimpleX</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">Чтобы защитить вашу информацию, включите блокировку <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.\nВам будет нужно пройти аутентификацию для включения блокировки.</string>
|
||||
<string name="la_notice_turn_on">Включить</string>
|
||||
|
||||
<!-- LocalAuthentication.kt -->
|
||||
<string name="auth_simplex_lock_turned_on">Блокировка SimpleX включена</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.</string>
|
||||
@@ -155,11 +142,9 @@
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Аутентификация устройства выключена. Отключение блокировки SimpleX Chat.</string>
|
||||
<string name="auth_stop_chat">Остановить чат</string>
|
||||
<string name="auth_open_chat_console">Открыть консоль</string>
|
||||
|
||||
<!-- Chat Alerts - ChatItemView.kt -->
|
||||
<string name="message_delivery_error_title">Ошибка доставки сообщения</string>
|
||||
<string name="message_delivery_error_desc">Скорее всего, этот контакт удалил соединение с вами.</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Ответить</string>
|
||||
<string name="share_verb">Поделиться</string>
|
||||
@@ -175,14 +160,12 @@
|
||||
<string name="delete_message_mark_deleted_warning">Сообщение будет помечено на удаление. Получатель(и) сможет(смогут) посмотреть это сообщение.</string>
|
||||
<string name="for_me_only">Удалить для меня</string>
|
||||
<string name="for_everybody">Для всех</string>
|
||||
|
||||
<!-- CIMetaView.kt -->
|
||||
<string name="icon_descr_edited">отредактировано</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">отправлено</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">ошибка авторизации при отправке</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">ошибка при отправке</string>
|
||||
<string name="icon_descr_received_msg_status_unread">не прочитано</string>
|
||||
|
||||
<!-- ChatListView.kt -->
|
||||
<string name="personal_welcome">Здравствуйте <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="welcome">Здравствуйте!</string>
|
||||
@@ -195,12 +178,10 @@
|
||||
<string name="tap_to_start_new_chat">Нажмите, чтобы начать чат</string>
|
||||
<string name="chat_with_developers">Соединиться с разработчиками</string>
|
||||
<string name="you_have_no_chats">У вас нет чатов</string>
|
||||
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Отправить сообщение…</string>
|
||||
<string name="share_image">Отправить изображение…</string>
|
||||
<string name="share_file">Отправить файл…</string>
|
||||
|
||||
<!-- ComposeView.kt, helpers -->
|
||||
<string name="attach">Прикрепить</string>
|
||||
<string name="icon_descr_context">Значок контекста</string>
|
||||
@@ -210,7 +191,6 @@
|
||||
<string name="images_limit_desc">Только 10 изображений могут быть отправлены одномоментно</string>
|
||||
<string name="image_decoding_exception_title">Ошибка декодирования</string>
|
||||
<string name="image_decoding_exception_desc">Не получается декодировать изображение. Пожалуйста, попробуйте другое изображение или свяжитесь с разработчиками.</string>
|
||||
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Изображение</string>
|
||||
<string name="icon_descr_waiting_for_image">Ожидается прием изображения</string>
|
||||
@@ -219,7 +199,6 @@
|
||||
<string name="waiting_for_image">Ожидается прием изображения</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">Изображение будет принято, когда ваш контакт будет в сети, подождите или проверьте позже!</string>
|
||||
<string name="image_saved">Изображение сохранено в Галерею</string>
|
||||
|
||||
<!-- Files - CIFileView.kt -->
|
||||
<string name="icon_descr_file">Файл</string>
|
||||
<string name="large_file">Большой файл!</string>
|
||||
@@ -230,15 +209,12 @@
|
||||
<string name="file_saved">Файл сохранен</string>
|
||||
<string name="file_not_found">Файл не найден</string>
|
||||
<string name="error_saving_file">Ошибка сохранения файла</string>
|
||||
|
||||
<!-- Voice messages -->
|
||||
<string name="voice_message">Голосовое сообщение</string>
|
||||
<string name="voice_message_with_duration">Голосовое сообщение (<xliff:g id="duration">%1$s</xliff:g>)</string>
|
||||
<string name="voice_message_send_text">Голосовое сообщение…</string>
|
||||
|
||||
<!-- Chat Info Settings - ChatInfoView.kt -->
|
||||
<string name="notifications">Уведомления</string>
|
||||
|
||||
<!-- Chat Info Actions - ChatInfoView.kt -->
|
||||
<string name="delete_contact_question">Удалить контакт?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">Контакт и все сообщения будут удалены - это действие нельзя отменить!</string>
|
||||
@@ -250,7 +226,6 @@
|
||||
<string name="icon_descr_server_status_pending">Ожидается соединение с сервером</string>
|
||||
<string name="switch_receiving_address_question">Переключить адрес получения?</string>
|
||||
<string name="switch_receiving_address_desc">Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Отправить сообщение</string>
|
||||
<string name="icon_descr_record_voice_message">Записать голосовое сообщение</string>
|
||||
@@ -259,7 +234,6 @@
|
||||
<string name="voice_messages_prohibited">Голосовые сообщения запрещены!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Попросите вашего контакта разрешить отправку голосовых сообщений.</string>
|
||||
<string name="only_group_owners_can_enable_voice">Только владельцы группы могут разрешить голосовые сообщения.</string>
|
||||
|
||||
<!-- General Actions / Responses -->
|
||||
<string name="back">Назад</string>
|
||||
<string name="cancel_verb">Отменить</string>
|
||||
@@ -269,7 +243,6 @@
|
||||
<string name="no_details">нет описания</string>
|
||||
<string name="add_contact">Одноразовая ссылка</string>
|
||||
<string name="copied">Скопировано в буфер обмена</string>
|
||||
|
||||
<!-- NewChatSheet -->
|
||||
<string name="add_contact_or_create_group">Начать новый разговор</string>
|
||||
<string name="share_one_time_link">Создать ссылку-приглашение</string>
|
||||
@@ -279,13 +252,11 @@
|
||||
<string name="to_share_with_your_contact">(чтобы отправить вашему контакту)</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(сканировать или вставить из буфера)</string>
|
||||
<string name="only_stored_on_members_devices">(хранится только у членов группы)</string>
|
||||
|
||||
<!-- GetImageView -->
|
||||
<string name="toast_permission_denied">Разрешение не получено!</string>
|
||||
<string name="use_camera_button">Камера</string>
|
||||
<string name="from_gallery_button">Галерея</string>
|
||||
<string name="choose_file">Файлы</string>
|
||||
|
||||
<!-- help - ChatHelpView.kt -->
|
||||
<string name="thank_you_for_installing_simplex">Спасибо, что установили <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Вы можете <font color="#0088ff">соединиться с разработчиками</font>, чтобы задать любые вопросы или получать уведомления о новых версиях.</string>
|
||||
@@ -298,14 +269,12 @@
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Если вы получили ссылку с приглашением из <xliff:g id="appName">SimpleX Chat</xliff:g>, вы можете открыть ее в браузере:</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 на компьютере: сосканируйте показанный QR код из приложения через <b>Сканировать QR код</b>.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 на мобильном: намжите кнопку <b>Open in mobile app</b> на веб странице, затем нажмите <b>Соединиться</b> в приложении.</string>
|
||||
|
||||
<!-- Contact Request Alert Dialogue - CharListNavLinkView.kt -->
|
||||
<string name="accept_connection_request__question">Принять запрос на соединение?</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Отправителю НЕ будет послано уведомление, если вы отклоните запрос на соединение.</string>
|
||||
<string name="accept_contact_button">Принять</string>
|
||||
<string name="accept_contact_incognito_button">Принять инкогнито</string>
|
||||
<string name="reject_contact_button">Отклонить</string>
|
||||
|
||||
<!-- Clear Chat - ChatListNavLinkView.kt -->
|
||||
<string name="clear_chat_question">Очистить чат?</string>
|
||||
<string name="clear_chat_warning">Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для вас.</string>
|
||||
@@ -317,29 +286,23 @@
|
||||
<string name="mark_read">Прочитано</string>
|
||||
<string name="mark_unread">Не прочитано</string>
|
||||
<string name="set_contact_name">Имя контакта</string>
|
||||
|
||||
<!-- Actions - ChatListNavLinkView.kt -->
|
||||
<string name="mute_chat">Без звука</string>
|
||||
<string name="unmute_chat">Уведомлять</string>
|
||||
|
||||
<!-- Pending contact connection alert dialogues -->
|
||||
<string name="you_invited_your_contact">Вы пригласили ваш контакт</string>
|
||||
<string name="you_accepted_connection">Вы приняли приглашение соединиться</string>
|
||||
<string name="delete_pending_connection__question">Удалить ожидаемое соединение?</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">Контакт, которому вы отправили эту ссылку, не сможет соединиться!</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">Подтвержденное соединение будет отменено!</string>
|
||||
|
||||
<!-- Connection Pending Alert Dialogue - ChatListNavLinkView.kt -->
|
||||
<string name="alert_title_contact_connection_pending">Соединение еще не установлено!</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Ваш контакт должен быть в сети чтобы установить соединение.\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой).</string>
|
||||
|
||||
<!-- Contact Request Information - ContactRequestView.kt -->
|
||||
<string name="contact_wants_to_connect_with_you">хочет соединиться с вами!</string>
|
||||
|
||||
<!-- Image Placeholder - ChatInfoImage.kt -->
|
||||
<string name="icon_descr_profile_image_placeholder">аватар не установлен</string>
|
||||
<string name="image_descr_profile_image">аватар</string>
|
||||
|
||||
<!-- Content Descriptions -->
|
||||
<string name="icon_descr_close_button">закрыть</string>
|
||||
<string name="image_descr_link_preview">изображение превью ссылки</string>
|
||||
@@ -352,10 +315,8 @@
|
||||
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> логотип</string>
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="icon_descr_more_button">Больше</string>
|
||||
|
||||
<!-- Connection info - ContactConnectionInfoView.kt -->
|
||||
<string name="show_QR_code">Показать QR код</string>
|
||||
|
||||
<!-- Add Contact - AddContactView.kt -->
|
||||
<string name="invalid_QR_code">Неверный QR код</string>
|
||||
<string name="this_QR_code_is_not_a_link">Этот QR код не является ссылкой!</string>
|
||||
@@ -372,16 +333,13 @@
|
||||
<string name="share_invitation_link">Поделиться ссылкой</string>
|
||||
<string name="paste_connection_link_below_to_connect">Чтобы соединиться, вставьте в это поле ссылку, полученную от вашего контакта.</string>
|
||||
<string name="your_profile_will_be_sent">Ваш профиль будет отправлен вашему контакту</string>
|
||||
|
||||
<!-- PasteToConnect.kt -->
|
||||
<string name="connect_button">Соединиться</string>
|
||||
<string name="paste_button">Вставить</string>
|
||||
|
||||
<!-- CreateLinkView.kt -->
|
||||
<string name="create_one_time_link">Создать одноразовую ссылку</string>
|
||||
<string name="one_time_link">Одноразовая ссылка</string>
|
||||
<string name="your_contact_address">Ваш SimpleX адрес</string>
|
||||
|
||||
<!-- settings - SettingsView.kt -->
|
||||
<string name="your_settings">Настройки</string>
|
||||
<string name="your_simplex_contact_address">Ваш <xliff:g id="appName">SimpleX</xliff:g> адрес</string>
|
||||
@@ -450,7 +408,6 @@
|
||||
<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="appearance_settings">Интерфейс</string>
|
||||
|
||||
<!-- Address Items - UserAddressView.kt -->
|
||||
<string name="create_address">Создать адрес</string>
|
||||
<string name="delete_address__question">Удалить адрес?</string>
|
||||
@@ -458,17 +415,15 @@
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
|
||||
<string name="share_link">Поделиться\nссылкой</string>
|
||||
<string name="delete_address">Удалить\nадрес</string>
|
||||
|
||||
<!-- AcceptRequestsView.kt -->
|
||||
<string name="contact_requests">Запросы контактов</string>
|
||||
<string name="accept_requests">Принимать запросы</string>
|
||||
<string name="accept_automatically">Автоматически</string>
|
||||
<string name="section_title_welcome_message">ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ</string>
|
||||
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Имя профиля:</string>
|
||||
<string name="full_name__field">"Полное имя:</string>
|
||||
<string name="your_chat_profile">Ваш профиль</string>
|
||||
<string name="your_current_profile">Ваш активный профиль</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профиль хранится на вашем устройстве и отправляется только вашим контактам.\n\n<xliff:g id="appName">SimpleX</xliff:g> серверы не могут получить доступ к вашему профилю.</string>
|
||||
<string name="edit_image">Поменять аватар</string>
|
||||
<string name="delete_image">Удалить аватар</string>
|
||||
@@ -477,7 +432,6 @@
|
||||
<string name="save_and_notify_contacts">Сохранить и уведомить контакты</string>
|
||||
<string name="save_and_notify_group_members">Сохранить и уведомить членов группы</string>
|
||||
<string name="exit_without_saving">Выйти без сохранения</string>
|
||||
|
||||
<!-- Welcome Prompts - WelcomeView.kt -->
|
||||
<string name="you_control_your_chat">Вы котролируете ваш чат!</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Платформа для сообщений и приложений, которая защищает вашу личную информацию и безопасность.</string>
|
||||
@@ -490,7 +444,6 @@
|
||||
<string name="full_name_optional__prompt">Полное имя (не обязательно)</string>
|
||||
<string name="create_profile_button">Создать</string>
|
||||
<string name="about_simplex">О SimpleX</string>
|
||||
|
||||
<!-- markdown demo - MarkdownHelpView.kt -->
|
||||
<string name="how_to_use_markdown">Как форматировать</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Вы можете форматировать сообщения:</string>
|
||||
@@ -503,7 +456,6 @@
|
||||
<string name="connect_via_link">Соединиться через ссылку</string>
|
||||
<string name="this_string_is_not_a_connection_link">Эта строка не является ссылкой-приглашением!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Открыть в приложении</b>.</string>
|
||||
|
||||
<!-- CICallStatus -->
|
||||
<string name="callstatus_calling">входящий звонок…</string>
|
||||
<string name="callstatus_missed">пропущенный звонок</string>
|
||||
@@ -513,7 +465,6 @@
|
||||
<string name="callstatus_in_progress">активный звонок</string>
|
||||
<string name="callstatus_ended">звонок завершён <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="callstatus_error">ошибка звонка</string>
|
||||
|
||||
<!-- CallState -->
|
||||
<string name="callstate_starting">инициализация…</string>
|
||||
<string name="callstate_waiting_for_answer">ожидается ответ…</string>
|
||||
@@ -523,7 +474,6 @@
|
||||
<string name="callstate_connecting">соединяется…</string>
|
||||
<string name="callstate_connected">соединено</string>
|
||||
<string name="callstate_ended">завершен</string>
|
||||
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">Новое поколение приватных сообщений</string>
|
||||
<string name="privacy_redefined">Более конфиденциальный</string>
|
||||
@@ -535,7 +485,6 @@
|
||||
<string name="create_your_profile">Создать профиль</string>
|
||||
<string name="make_private_connection">Добавьте контакт</string>
|
||||
<string name="how_it_works">Как это работает</string>
|
||||
|
||||
<!-- How SimpleX Works -->
|
||||
<string name="how_simplex_works">Как <xliff:g id="appName">SimpleX</xliff:g> работает</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Много пользователей спросили: <i>как <xliff:g id="appName">SimpleX</xliff:g> доставляет сообщения без идентификаторов пользователей?</i></string>
|
||||
@@ -544,10 +493,10 @@
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются <b>с двухуровневым end-to-end шифрованием</b>.</string>
|
||||
<string name="read_more_in_github">Узнайте больше из нашего GitHub репозитория.</string>
|
||||
<string name="read_more_in_github_with_link">Узнайте больше из нашего <font color="#0088ff">GitHub репозитория</font>.</string>
|
||||
|
||||
<!-- SetNotificationsMode.kt -->
|
||||
<string name="use_chat">Использовать чат</string>
|
||||
<!-- MakeConnection -->
|
||||
<string name="paste_the_link_you_received">Вставить полученную ссылку</string>
|
||||
|
||||
<!-- Call -->
|
||||
<string name="incoming_video_call">Входящий видеозвонок</string>
|
||||
<string name="incoming_audio_call">Входящий аудиозвонок</string>
|
||||
@@ -562,7 +511,6 @@
|
||||
<string name="call_already_ended">Звонок уже завершен!</string>
|
||||
<string name="icon_descr_video_call">видеозвонок</string>
|
||||
<string name="icon_descr_audio_call">аудиозвонок</string>
|
||||
|
||||
<!-- Call settings -->
|
||||
<string name="settings_audio_video_calls">Аудио- и видеозвонки</string>
|
||||
<string name="your_calls">Ваши звонки</string>
|
||||
@@ -573,12 +521,10 @@
|
||||
<string name="no_call_on_lock_screen">Выключить</string>
|
||||
<string name="your_ice_servers">Ваши ICE серверы</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE серверы</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>
|
||||
<string name="open_verb">Открыть</string>
|
||||
|
||||
<!-- Call overlay -->
|
||||
<string name="status_e2e_encrypted">e2e зашифровано</string>
|
||||
<string name="status_no_e2e_encryption">нет e2e шифрования</string>
|
||||
@@ -594,7 +540,6 @@
|
||||
<string name="icon_descr_speaker_off">Выключить спикер</string>
|
||||
<string name="icon_descr_speaker_on">Включить спикер</string>
|
||||
<string name="icon_descr_flip_camera">Перевернуть камеру</string>
|
||||
|
||||
<!-- Call items -->
|
||||
<string name="icon_descr_call_pending_sent">Входящий звонок</string>
|
||||
<string name="icon_descr_call_missed">Пропущенный звонок</string>
|
||||
@@ -603,7 +548,6 @@
|
||||
<string name="icon_descr_call_progress">Текущий звонок</string>
|
||||
<string name="icon_descr_call_ended">Звонок завершен</string>
|
||||
<string name="answer_call">Принять звонок</string>
|
||||
|
||||
<!-- Message integrity -->
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> пропущенных сообщений</string>
|
||||
<string name="integrity_msg_bad_hash">ошибка хэш сообщения</string>
|
||||
@@ -611,7 +555,6 @@
|
||||
<string name="integrity_msg_duplicate">повторное сообщение</string>
|
||||
<string name="alert_title_skipped_messages">Пропущенные сообщения</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Это может случится, когда:\n1. Сервер удалил сообщения, если они не были доставлены в течение 30 дней.\n2. Сервер, через который вы получаете сообщения от контакта, был обновлён и перезапущен.\n3. Соединение компроментировано.\nПожалуйста, соединитесь с девелоперами через Настройки, чтобы получать уведомления о серверах.\nМы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения.</string>
|
||||
|
||||
<!-- Privacy settings -->
|
||||
<string name="privacy_and_security">Конфиденциальность</string>
|
||||
<string name="your_privacy">Конфиденциальность</string>
|
||||
@@ -620,7 +563,6 @@
|
||||
<string name="transfer_images_faster">Передавать изображения быстрее</string>
|
||||
<string name="send_link_previews">Отправлять картинки ссылок</string>
|
||||
<string name="full_backup">Резервная копия данных</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">ВЫ</string>
|
||||
<string name="settings_section_title_settings">НАСТРОЙКИ</string>
|
||||
@@ -637,7 +579,6 @@
|
||||
<string name="settings_section_title_messages">СООБЩЕНИЯ</string>
|
||||
<string name="settings_section_title_calls">ЗВОНКИ</string>
|
||||
<string name="settings_section_title_incognito">Режим Инкогнито</string>
|
||||
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">База данных</string>
|
||||
<string name="run_chat_section">ЗАПУСТИТЬ ЧАТ</string>
|
||||
@@ -671,8 +612,8 @@
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Перезапустите приложение, чтобы создать новый профиль.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Используйте самую последнюю версию архива чата и ТОЛЬКО на одном устройстве, иначе вы можете перестать получать сообщения от некоторых контактов.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Остановите чат, чтобы разблокировать операции с архивом чата.</string>
|
||||
<string name="data_section">ДАННЫЕ</string>
|
||||
<string name="delete_files_and_media">Удалить файлы и медиа</string>
|
||||
<string name="delete_files_and_media_for_all_users">Удалить файлы во всех профилях чата</string>
|
||||
<string name="delete_files_and_media_all">Удалить все файлы</string>
|
||||
<string name="delete_files_and_media_question">Удалить файлы и медиа?</string>
|
||||
<string name="delete_files_and_media_desc">Это действие нельзя отменить — все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении.</string>
|
||||
<string name="no_received_app_files">Нет полученных или отправленных файлов</string>
|
||||
@@ -687,7 +628,6 @@
|
||||
<string name="enable_automatic_deletion_message">Это действие нельзя отменить — все сообщения, отправленные или полученные раньше чем выбрано, будут удалены. Это может занять несколько минут.</string>
|
||||
<string name="delete_messages">Удалить сообщения</string>
|
||||
<string name="error_changing_message_deletion">Ошибка при изменении настройки</string>
|
||||
|
||||
<!-- DatabaseEncryptionView.kt -->
|
||||
<string name="save_passphrase_in_keychain">Сохранить пароль в Keystore</string>
|
||||
<string name="database_encrypted">База данных зашифрована!</string>
|
||||
@@ -716,7 +656,6 @@
|
||||
<string name="database_passphrase_will_be_updated">Пароль базы данных будет изменен.</string>
|
||||
<string name="store_passphrase_securely">Пожалуйста, надежно сохраните пароль, вы НЕ сможете его поменять, если потеряете.</string>
|
||||
<string name="store_passphrase_securely_without_recover">Пожалуйста, надежно сохраните пароль, вы НЕ сможете открыть чат, если потеряете его.</string>
|
||||
|
||||
<!-- DatabaseErrorView.kt -->
|
||||
<string name="wrong_passphrase">Неправильный пароль базы данных</string>
|
||||
<string name="encrypted_database">База данных зашифрована</string>
|
||||
@@ -741,11 +680,9 @@
|
||||
<string name="restore_database_alert_confirm">Восстановить</string>
|
||||
<string name="database_restore_error">Ошибка при восстановлении базы данных</string>
|
||||
<string name="restore_passphrase_not_found_desc">Пароль не найден в Keystore, пожалуйста, введите его вручную. Это могло произойти, если вы восстановили данные приложения с помощью инструмента резервного копирования. Если это не так, пожалуйста, свяжитесь с разработчиками.</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Чат остановлен</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Вы можете запустить чат через Настройки приложения или перезапустив приложение.</string>
|
||||
|
||||
<!-- ChatArchiveView.kt -->
|
||||
<string name="chat_archive_header">Архив чата</string>
|
||||
<string name="chat_archive_section">АРХИВ ЧАТА</string>
|
||||
@@ -753,7 +690,6 @@
|
||||
<string name="delete_archive">Удалить архив</string>
|
||||
<string name="archive_created_on_ts">Дата создания <xliff:g id="archive_ts">%1$s</xliff:g></string>
|
||||
<string name="delete_chat_archive_question">Удалить архив чата?</string>
|
||||
|
||||
<!-- Groups -->
|
||||
<string name="group_invitation_item_description">приглашение в группу <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="join_group_question">Вступить в группу?</string>
|
||||
@@ -773,7 +709,6 @@
|
||||
<string name="alert_message_no_group">Эта группа больше не существует.</string>
|
||||
<string name="alert_title_cant_invite_contacts">Нельзя пригласить контакты!</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Вы используете инкогнито профиль для этой группы - чтобы предотвратить раскрытие вашего основного профиля, приглашать контакты не разрешено</string>
|
||||
|
||||
<!-- CIGroupInvitationView.kt -->
|
||||
<string name="you_sent_group_invitation">Вы отправили приглашение в группу</string>
|
||||
<string name="you_are_invited_to_group">Вы приглашены в группу</string>
|
||||
@@ -782,7 +717,6 @@
|
||||
<string name="you_joined_this_group">Вы вступили в эту группу</string>
|
||||
<string name="you_rejected_group_invitation">Вы отклонили приглашение в группу</string>
|
||||
<string name="group_invitation_expired">Приглашение в группу истекло</string>
|
||||
|
||||
<!-- Group event chat items -->
|
||||
<string name="rcv_group_event_member_added">пригласил(а) <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_connected">соединен(а)</string>
|
||||
@@ -799,7 +733,6 @@
|
||||
<string name="snd_group_event_member_deleted">вы удалили <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_user_left">вы покинули группу</string>
|
||||
<string name="snd_group_event_group_profile_updated">профиль группы обновлен</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">поменял(а) адрес для вас</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">смена адреса…</string>
|
||||
@@ -807,12 +740,10 @@
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">смена адреса для %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">вы поменяли адрес</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">смена адреса…</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_member">член группы</string>
|
||||
<string name="group_member_role_admin">админ</string>
|
||||
<string name="group_member_role_owner">владелец</string>
|
||||
|
||||
<!-- GroupMemberStatus -->
|
||||
<string name="group_member_status_removed">удален(а)</string>
|
||||
<string name="group_member_status_left">покинул(а)</string>
|
||||
@@ -825,9 +756,7 @@
|
||||
<string name="group_member_status_connected">соединен(а)</string>
|
||||
<string name="group_member_status_complete">соединение завершено</string>
|
||||
<string name="group_member_status_creator">создатель</string>
|
||||
|
||||
<string name="group_member_status_connecting">соединяется</string>
|
||||
|
||||
<!-- AddGroupMembersView.kt -->
|
||||
<string name="no_contacts_to_add">Нет контактов для добавления</string>
|
||||
<string name="new_member_role">Роль члена группы</string>
|
||||
@@ -841,7 +770,6 @@
|
||||
<string name="no_contacts_selected">Контакты не выбраны</string>
|
||||
<string name="invite_prohibited">Нельзя пригласить контакт!</string>
|
||||
<string name="invite_prohibited_description">Вы пытаетесь пригласить инкогнито контакт в группу, где вы используете свой основной профиль</string>
|
||||
|
||||
<!-- GroupChatInfoView.kt -->
|
||||
<string name="button_add_members">Пригласить членов группы</string>
|
||||
<string name="group_info_section_title_num_members">ЧЛЕНОВ ГРУППЫ: <xliff:g id="num_members">%1$s</xliff:g></string>
|
||||
@@ -861,12 +789,10 @@
|
||||
<string name="error_creating_link_for_group">Ошибка при создании ссылки группы</string>
|
||||
<string name="error_deleting_link_for_group">Ошибка при удалении ссылки группы</string>
|
||||
<string name="only_group_owners_can_change_prefs">Только владельцы группы могут изменять предпочтения группы.</string>
|
||||
|
||||
<!-- For Console chat info section -->
|
||||
<string name="section_title_for_console">ДЛЯ КОНСОЛИ</string>
|
||||
<string name="info_row_local_name">Локальное имя</string>
|
||||
<string name="info_row_database_id">ID базы данных</string>
|
||||
|
||||
<!-- GroupMemberInfoView.kt -->
|
||||
<string name="button_remove_member">Удалить члена группы</string>
|
||||
<string name="button_send_direct_message">Отправить сообщение</string>
|
||||
@@ -886,14 +812,12 @@
|
||||
<string name="info_row_connection">Соединение</string>
|
||||
<string name="conn_level_desc_direct">прямое</string>
|
||||
<string name="conn_level_desc_indirect">непрямое (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
|
||||
<!-- ConnectionStats -->
|
||||
<string name="conn_stats_section_title_servers">СЕРВЕРЫ</string>
|
||||
<string name="receiving_via">Получение через</string>
|
||||
<string name="sending_via">Отправка через</string>
|
||||
<string name="network_status">Состояние сети</string>
|
||||
<string name="switch_receiving_address">Переключить адрес получения</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Создать скрытую группу</string>
|
||||
<string name="group_is_decentralized">Группа полностью децентрализована — она видна только членам.</string>
|
||||
@@ -901,12 +825,10 @@
|
||||
<string name="group_full_name_field">Полное имя:</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Режим Инкогнито здесь не поддерживается - ваш основной профиль будет отправлен членам группы</string>
|
||||
<string name="group_main_profile_sent">Ваш профиль чата будет отправлен членам группы</string>
|
||||
|
||||
<!-- GroupProfileView.kt -->
|
||||
<string name="group_profile_is_stored_on_members_devices">Профиль группы хранится на устройствах членов, а не на серверах.</string>
|
||||
<string name="save_group_profile">Сохранить профиль группы</string>
|
||||
<string name="error_saving_group_profile">Ошибка при сохранении профиля группы</string>
|
||||
|
||||
<!-- AdvancedNetworkSettings.kt -->
|
||||
<string name="network_options_reset_to_defaults">Сбросить настройки</string>
|
||||
<string name="network_option_seconds_label">сек</string>
|
||||
@@ -919,29 +841,24 @@
|
||||
<string name="update_network_settings_question">Обновить настройки сети?</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Обновление настроек приведет к переподключению клиента ко всем серверам.</string>
|
||||
<string name="update_network_settings_confirmation">Обновить</string>
|
||||
|
||||
<!-- Incognito mode -->
|
||||
<string name="incognito">Инкогнито</string>
|
||||
<string name="incognito_random_profile">Случайный профиль</string>
|
||||
<string name="incognito_random_profile_description">Вашему контакту будет отправлен случайный профиль</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Контакту, от которого вы получили эту ссылку, будет отправлен случайный профиль</string>
|
||||
|
||||
<string name="incognito_info_protects">Режим Инкогнито защищает конфиденциальность имени и изображения вашего основного профиля — для каждого нового контакта создается новый случайный профиль.</string>
|
||||
<string name="incognito_info_allows">Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя.</string>
|
||||
<string name="incognito_info_share">Когда вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.</string>
|
||||
<string name="incognito_info_find">Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата.</string>
|
||||
|
||||
<!-- Default themes -->
|
||||
<string name="theme_system">Системная</string>
|
||||
<string name="theme_light">Светлая</string>
|
||||
<string name="theme_dark">Темная</string>
|
||||
|
||||
<!-- Appearance.kt -->
|
||||
<string name="theme">Тема</string>
|
||||
<string name="save_color">Сохранить цвет</string>
|
||||
<string name="reset_color">Сбросить цвета</string>
|
||||
<string name="color_primary">Акцент</string>
|
||||
|
||||
<!-- Preferences.kt -->
|
||||
<string name="chat_preferences_you_allow">Вы разрешаете</string>
|
||||
<string name="chat_preferences_contact_allows">Контакт разрешает</string>
|
||||
@@ -990,5 +907,136 @@
|
||||
<string name="message_deletion_prohibited_in_chat">Необратимое удаление сообщений запрещено в этой группе.</string>
|
||||
<string name="group_members_can_send_voice">Члены группы могут отправлять голосовые сообщения.</string>
|
||||
<string name="voice_messages_are_prohibited">Голосовые сообщения запрещены в этой группе.</string>
|
||||
|
||||
</resources>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Минимальный расход батареи</b>. Вы получите уведомления только когда приложение запущено, без фонового сервиса.</string>
|
||||
<string name="onboarding_notifications_mode_title">Уведомления</string>
|
||||
<string name="onboarding_notifications_mode_off">Когда приложение запущено</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Периодически</string>
|
||||
<string name="onboarding_notifications_mode_service">Мгновенно</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Больше расход батареи</b>! Фоновый сервис постоянно запущен - уведомления будут показаны как только есть новые сообщения.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Меньше расход батареи</b>. Фоновый сервис проверяет новые сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Можно изменить позже в настройках.</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="send_live_message">Отправить живое сообщение</string>
|
||||
<string name="live_message">Живое сообщение!</string>
|
||||
<string name="send_verb">Отправить</string>
|
||||
<string name="scan_code_from_contacts_app">Сканируйте код безопасности из приложения контакта.</string>
|
||||
<string name="delete_after">Удалять через</string>
|
||||
<string name="ttl_sec">%d сек</string>
|
||||
<string name="ttl_s">%dс</string>
|
||||
<string name="ttl_min">%d мин</string>
|
||||
<string name="ttl_month">%d мес.</string>
|
||||
<string name="ttl_months">%d мес.</string>
|
||||
<string name="ttl_m">%dм</string>
|
||||
<string name="ttl_mth">%dмес</string>
|
||||
<string name="ttl_hour">%d час</string>
|
||||
<string name="ttl_hours">%d ч.</string>
|
||||
<string name="ttl_h">%dч</string>
|
||||
<string name="ttl_day">%d день</string>
|
||||
<string name="ttl_week">%d нед.</string>
|
||||
<string name="timed_messages">Исчезающие сообщения</string>
|
||||
<string name="view_security_code">Показать код безопасности</string>
|
||||
<string name="verify_security_code">Подтвердить код безопасности</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Вы и ваш контакт можете отправлять исчезающие сообщения.</string>
|
||||
<string name="only_you_can_send_disappearing">Только вы можете отправлять исчезающие сообщения.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Только ваш контакт может отправлять исчезающие сообщения.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Исчезающие сообщения запрещены в этом чате.</string>
|
||||
<string name="allow_to_send_disappearing">Разрешить посылать исчезающие сообщения.</string>
|
||||
<string name="contact_developers">Пожалуйста, обновите приложение и свяжитесь с разработчиками.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Разрешить вашим контактам отправлять исчезающие сообщения.</string>
|
||||
<string name="failed_to_parse_chat_title">Не удалось открыть чат</string>
|
||||
<string name="failed_to_parse_chats_title">Не удалось открыть чаты</string>
|
||||
<string name="incorrect_code">Неправильный код безопасности!</string>
|
||||
<string name="scan_code">Сканировать код</string>
|
||||
<string name="send_live_message_desc">Отправить живое сообщение — оно будет обновляться для получателей по мере того, как вы его вводите</string>
|
||||
<string name="create_group_link">Создать ссылку группы</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Запретить отправлять исчезающие сообщения.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Исчезающие сообщения запрещены в этой группе.</string>
|
||||
<string name="ttl_w">%dнед</string>
|
||||
<string name="ttl_d">%dд</string>
|
||||
<string name="ttl_weeks">%d нед.</string>
|
||||
<string name="ttl_days">%d дней</string>
|
||||
<string name="to_verify_compare">Чтобы подтвердить безопасность end-to-end шифрования с вашим контактом сравните (или сканируйте) код на ваших устройствах.</string>
|
||||
<string name="is_verified">%s подтверждён</string>
|
||||
<string name="is_not_verified">%s не подтверждён</string>
|
||||
<string name="security_code">Код безопасности</string>
|
||||
<string name="mark_code_verified">Подтвердить</string>
|
||||
<string name="clear_verification">Сбросить подтверждение</string>
|
||||
<string name="allow_disappearing_messages_only_if">Разрешить исчезающие сообщения, только если ваш контакт разрешает их вам.</string>
|
||||
<string name="prohibit_sending_disappearing">Запретить посылать исчезающие сообщения.</string>
|
||||
<string name="group_members_can_send_disappearing">Члены группы могут посылать исчезающие сообщения.</string>
|
||||
<string name="whats_new">Новые функции</string>
|
||||
<string name="new_in_version">Новое в %s</string>
|
||||
<string name="v4_2_security_assessment">Аудит безопасности</string>
|
||||
<string name="v4_2_security_assessment_desc">Безопасность SimpleX Chat была проверена Trail of Bits.</string>
|
||||
<string name="v4_3_voice_messages">Голосовые сообщения</string>
|
||||
<string name="v4_3_voice_messages_desc">Макс. 40 секунд, доставляются мгновенно.</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Окончательное удаление сообщений</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Ваши контакты могут разрешить окончательное удаление сообщений.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Добавить серверы через QR код.</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Улучшенная безопасность</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Скрыть экран приложения.</string>
|
||||
<string name="v4_4_disappearing_messages">Исчезающие сообщения</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Отправленные сообщения будут удалены через заданное время.</string>
|
||||
<string name="v4_3_improved_server_configuration">Улучшенная конфигурация серверов</string>
|
||||
<string name="v4_4_live_messages">\"Живые\" сообщения</string>
|
||||
<string name="v4_4_live_messages_desc">Получатели видят их в то время как вы их набираете.</string>
|
||||
<string name="v4_4_verify_connection_security">Проверить безопасность соединения</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Сравните код безопасности с вашими контактами.</string>
|
||||
<string name="invalid_chat">ошибка чата</string>
|
||||
<string name="accept_feature">Принять</string>
|
||||
<string name="accept_feature_set_1_day">Установить 1 день</string>
|
||||
<string name="invalid_data">неверные данные</string>
|
||||
<string name="v4_2_group_links">Ссылки групп</string>
|
||||
<string name="v4_2_group_links_desc">Админы могут создать ссылки для вступления в группу.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Автоматически принимать запросы контактов</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">С опциональным авто-ответом.</string>
|
||||
<string name="feature_offered_item">предложил(a) %s</string>
|
||||
<string name="feature_offered_item_with_param">предложил(a) %s: %2s</string>
|
||||
<string name="feature_cancelled_item">отменил(a) %s</string>
|
||||
<string name="icon_descr_cancel_live_message">Отменить живое сообщение</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="network_option_ping_count">Количество PING</string>
|
||||
<string name="users_delete_with_connections">Профиль и соединения на сервере</string>
|
||||
<string name="app_version_title">Версия приложения</string>
|
||||
<string name="network_session_mode_user">Профиль чата</string>
|
||||
<string name="network_session_mode_entity">Соединение</string>
|
||||
<string name="users_add">Добавить профиль</string>
|
||||
<string name="error_deleting_user">Ошибка удаления профиля пользователя</string>
|
||||
<string name="files_and_media_section">Файлы и медиа</string>
|
||||
<string name="users_delete_data_only">Только локальные данные профиля</string>
|
||||
<string name="messages_section_title">Сообщения</string>
|
||||
<string name="smp_servers_per_user">Серверы для новых соединений вашего текущего профиля чата</string>
|
||||
<string name="your_chat_profiles_stored_locally">Ваши профили чата хранятся локально, только на вашем устройстве</string>
|
||||
<string name="your_chat_profiles">Ваши профили чата</string>
|
||||
<string name="users_delete_all_chats_deleted">Все чаты и сообщения будут удалены - это нельзя отменить!</string>
|
||||
<string name="app_version_code">Сборка приложения: %s</string>
|
||||
<string name="app_version_name">Версия приложения: v%s</string>
|
||||
<string name="network_session_mode_entity_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого контакта и члена группы</b>.
|
||||
\n<b>Обратите внимание</b>: если у вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.</string>
|
||||
<string name="network_session_mode_user_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого профиля чата, который вы имеете в приложении</b>.</string>
|
||||
<string name="core_build_timestamp">Ядро скомпилировано: %s</string>
|
||||
<string name="core_version">Версия ядра: v%s</string>
|
||||
<string name="users_delete_question">Удалить профиль чата\?</string>
|
||||
<string name="users_delete_profile_for">Удалить профиль чата для</string>
|
||||
<string name="messages_section_description">Эта настройка применяется к сообщениям в вашем текущем профиле чата</string>
|
||||
<string name="network_session_mode_transport_isolation">Отдельные сессии для</string>
|
||||
<string name="update_network_session_mode_question">Обновить режим отдельных сессий\?</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Имя профиля уже используется</string>
|
||||
<string name="failed_to_create_user_title">Ошибка создания профиля!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">У вас уже есть профиль с таким именем. Пожалуйста, выберите другое имя.</string>
|
||||
<string name="failed_to_active_user_title">Ошибка выбора профиля!</string>
|
||||
<string name="v4_5_transport_isolation_descr">По профилю чата или по соединению (БЕТА)</string>
|
||||
<string name="v4_4_french_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Разные имена, аватары и транспортные сессии.</string>
|
||||
<string name="v4_5_italian_interface">Итальянский интерфейс</string>
|
||||
<string name="v4_5_message_draft">Черновик сообщения</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Много профилей чата</string>
|
||||
<string name="v4_5_message_draft_descr">Сохранить последний черновик, вместе с вложениями.</string>
|
||||
<string name="v4_5_private_filenames">Защищенные имена файлов</string>
|
||||
<string name="v4_5_italian_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string>
|
||||
<string name="v4_5_private_filenames_descr">Чтобы защитить ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC.</string>
|
||||
<string name="v4_4_french_interface">Французский интерфейс</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string>
|
||||
<string name="v4_5_reduced_battery_usage">Уменьшенное потребление батареи</string>
|
||||
<string name="v4_5_transport_isolation">Отдельные транспортные сессии</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,226 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="chat_item_ttl_day">1天</string>
|
||||
<string name="about_simplex">关于 SimpleX</string>
|
||||
<string name="all_group_members_will_remain_connected">所有群组成员将保持连接。</string>
|
||||
<string name="about_simplex_chat">关于 <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="above_then_preposition_continuation">以上,然后:</string>
|
||||
<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_week">1周</string>
|
||||
<string name="color_primary">强化</string>
|
||||
<string name="callstatus_accepted">已接受通话</string>
|
||||
<string name="accept">接受</string>
|
||||
<string name="network_enable_socks_info">通过 SOCKS 代理访问服务器在端口9050?允许该选项前必须开始代理。</string>
|
||||
<string name="smp_servers_add">添加服务器…</string>
|
||||
<string name="smp_servers_add_to_another_device">添加另一设备</string>
|
||||
<string name="group_member_role_admin">管理员</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">扫描二维码来添加服务器。</string>
|
||||
<string name="network_settings">高级网络设置</string>
|
||||
<string name="accept_connection_request__question">接受连接请求?</string>
|
||||
<string name="accept_contact_incognito_button">接受隐身聊天</string>
|
||||
<string name="v4_2_group_links_desc">管理员可以创建链接以加入群组。</string>
|
||||
<string name="accept_requests">接受请求</string>
|
||||
<string name="smp_servers_preset_add">添加预设服务器</string>
|
||||
<string name="connect_via_link">通过链接连接</string>
|
||||
<string name="display_name_connection_established">已建立连接</string>
|
||||
<string name="connection_local_display_name">连接 <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="connection_error">连接错误</string>
|
||||
<string name="connection_timeout">连接超时</string>
|
||||
<string name="contact_already_exists">联系人已存在</string>
|
||||
<string name="connection_error_auth">连接错误(AUTH)</string>
|
||||
<string name="answer_call">接听来电</string>
|
||||
<string name="delete_chat_profile_question">删除聊天资料?</string>
|
||||
<string name="delete_files_and_media_all">删除所有文件</string>
|
||||
<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_delete_all_chats_deleted">所有聊天记录和消息将被删除——这一行为无法撤销!</string>
|
||||
<string name="clear_chat_warning">所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</string>
|
||||
<string name="allow_to_send_voice">允许发送语音消息。</string>
|
||||
<string name="allow_voice_messages_question">允许语音消息?</string>
|
||||
<string name="delete_verb">删除</string>
|
||||
<string name="delete_contact_menu_action">删除</string>
|
||||
<string name="delete_group_menu_action">删除</string>
|
||||
<string name="delete_address__question">删除地址?</string>
|
||||
<string name="delete_after">在此后删除</string>
|
||||
<string name="delete_archive">删除档案</string>
|
||||
<string name="deleted_description">已删除</string>
|
||||
<string name="delete_files_and_media_question">删除文件和媒体文件?</string>
|
||||
<string name="full_deletion">为所有人删除</string>
|
||||
<string name="for_me_only">为我删除</string>
|
||||
<string name="delete_files_and_media_for_all_users">为所有聊天资料删除文件</string>
|
||||
<string name="button_delete_group">删除群组</string>
|
||||
<string name="delete_group_question">删除群组?</string>
|
||||
<string name="delete_link">删除链接</string>
|
||||
<string name="delete_link_question">删除链接?</string>
|
||||
<string name="network_session_mode_entity">连接</string>
|
||||
<string name="connection_request_sent">已发送连接请求!</string>
|
||||
<string name="delete_message__question">删除消息?</string>
|
||||
<string name="delete_messages">删除消息</string>
|
||||
<string name="info_row_connection">连接</string>
|
||||
<string name="connect_via_invitation_link">通过邀请链接连接?</string>
|
||||
<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="allow_your_contacts_irreversibly_delete">允许您的联系人不可撤回地删除已发送消息。</string>
|
||||
<string name="chat_preferences_contact_allows">联系人允许</string>
|
||||
<string name="allow_voice_messages_only_if">仅有您的联系人许可后才允许语音消息。</string>
|
||||
<string name="group_info_member_you">您: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">允许您的联系人发送语音消息。</string>
|
||||
<string name="chat_preferences_always">一直</string>
|
||||
<string name="notifications_mode_service">一直开启</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">允许您的联系人发送限时消息。</string>
|
||||
<string name="app_version_code">应用程序构建:%s</string>
|
||||
<string name="all_your_contacts_will_remain_connected">所有联系人会保持连接。</string>
|
||||
<string name="allow_verb">允许</string>
|
||||
<string name="allow_direct_messages">允许直接发送消息给成员。</string>
|
||||
<string name="allow_to_send_disappearing">允许发送限时消息。</string>
|
||||
<string name="delete_address">删除地址</string>
|
||||
<string name="delete_chat_archive_question">删除聊天档案?</string>
|
||||
<string name="users_delete_question">删除聊天资料?</string>
|
||||
<string name="button_delete_contact">删除联系人</string>
|
||||
<string name="delete_contact_question">删除联系人?</string>
|
||||
<string name="rcv_group_event_group_deleted">已删除群组</string>
|
||||
<string name="delete_image">删除图片</string>
|
||||
<string name="allow_disappearing_messages_only_if">仅有您的联系人许可后才允许限时消息。</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">仅有您的联系人许可后才允许不可撤回消息移除。</string>
|
||||
<string name="allow_to_delete_messages">允许不可撤回地删除已发送消息。</string>
|
||||
<string name="users_delete_profile_for">为此删除聊天资料</string>
|
||||
<string name="delete_database">删除数据库</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">在您重启应用程序或者更换密码后安卓密钥库系统用来安全地保存密码——来确保收到通知。</string>
|
||||
<string name="keychain_is_storing_securely">安卓密钥库系统用来安全地保存密码——来确保通知服务运作。</string>
|
||||
<string name="appearance_settings">外观</string>
|
||||
<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="app_version_name">应用程序版本:v%s</string>
|
||||
<string name="notifications_mode_off_desc">仅在运行时应用程序可以接受通知,不会启动后台服务</string>
|
||||
<string name="incognito_random_profile_description">一个随机资料将发送给您的联系人</string>
|
||||
<string name="auth_unavailable">身份验证不可用</string>
|
||||
<string name="auto_accept_images">自动接受图像</string>
|
||||
<string name="attach">附件</string>
|
||||
<string name="icon_descr_audio_call">语音通话</string>
|
||||
<string name="audio_call_no_encryption">语音通话(非端到端加密)</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">自动接受联系人请求</string>
|
||||
<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="turning_off_service_and_periodic">激活电池优化,关闭了后台服务和新消息的定期请求。您可以通过设置重新启用它们。</string>
|
||||
<string name="notifications_mode_service_desc">后台服务一直在运行——一旦有消息,就会显示通知。</string>
|
||||
<string name="icon_descr_audio_off">关闭音频</string>
|
||||
<string name="icon_descr_audio_on">开启音频</string>
|
||||
<string name="icon_descr_asked_to_receive">已要求接收图片</string>
|
||||
<string name="network_session_mode_user_description">一个单独的TCP连接(和SOCKS凭证)将被用于<b>,用于您在应用程序中的每个聊天资料</b> 。</string>
|
||||
<string name="network_session_mode_entity_description">每个联系人和群组成员</b> 将使用单独的 TCP 连接(和 SOCKS 凭证)<b>。
|
||||
\n<b>请注意</b>:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>添加新联系人</b>:为您的联系人创建一次性二维码。</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b> 最适合电池 </b>。您只会在应用程序运行时收到通知,不会使用后台服务。</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b> 适合于电池 </b>。后台服务每 10 分钟检查一次新消息。您可能会错过来电和紧急信息。</string>
|
||||
<string name="bold">加粗</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以不可逆转地删除已发送的消息。</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">您和您的联系人都可以发送限时消息。</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">您和您的联系人都可以发送语音消息。</string>
|
||||
<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="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>
|
||||
<string name="cancel_verb">取消</string>
|
||||
<string name="callstatus_ended">通话结束 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="change_verb">更换</string>
|
||||
<string name="icon_descr_call_ended">通话结束</string>
|
||||
<string name="change_database_passphrase_question">更改数据库密码?</string>
|
||||
<string name="callstatus_error">通话错误</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">为您更改地址</string>
|
||||
<string name="callstatus_in_progress">通话中</string>
|
||||
<string name="icon_descr_call_progress">通话进行中</string>
|
||||
<string name="callstatus_calling">呼叫中…</string>
|
||||
<string name="icon_descr_cancel_live_message">取消实时消息</string>
|
||||
<string name="settings_section_title_calls">通话</string>
|
||||
<string name="call_on_lock_screen">在锁定屏幕上通话:</string>
|
||||
<string name="icon_descr_cancel_image_preview">取消图片预览</string>
|
||||
<string name="feature_cancelled_item">已取消 %s</string>
|
||||
<string name="icon_descr_cancel_file_preview">取消文件预览</string>
|
||||
<string name="cannot_access_keychain">无法访问密钥库来保存数据库密码</string>
|
||||
<string name="cannot_receive_file">无法接收文件</string>
|
||||
<string name="database_initialization_error_title">无法初始化数据库</string>
|
||||
<string name="rcv_group_event_changed_member_role">将 %s 的角色更改为 %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">将您的角色更改为 %s</string>
|
||||
<string name="change_role">改变角色</string>
|
||||
<string name="change_member_role_question">更改群组角色?</string>
|
||||
<string name="icon_descr_cancel_link_preview">取消链接预览</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">为 %s 更改地址…</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">更改地址…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">更改地址…</string>
|
||||
<string name="create_your_profile">创建您的资料</string>
|
||||
<string name="chat_database_deleted">聊天数据库已删除</string>
|
||||
<string name="chat_database_imported">聊天数据库已导入</string>
|
||||
<string name="keychain_error">钥匙串错误</string>
|
||||
<string name="chat_archive_section">聊天档案</string>
|
||||
<string name="chat_archive_header">聊天档案</string>
|
||||
<string name="chat_console">聊天控制台</string>
|
||||
<string name="chat_database_section">聊天数据库</string>
|
||||
<string name="chat_is_stopped_indication">聊天已停止</string>
|
||||
<string name="chat_is_running">聊天进行中</string>
|
||||
<string name="chat_is_stopped">聊天已停止</string>
|
||||
<string name="contact_preferences">联系人偏好设置</string>
|
||||
<string name="your_preferences">您的偏好设置</string>
|
||||
<string name="group_preferences">群组偏好设置</string>
|
||||
<string name="only_group_owners_can_change_prefs">只有群主可以改变群组偏好设置。</string>
|
||||
<string name="save_preferences_question">保存偏好设置?</string>
|
||||
<string name="set_group_preferences">设置群组偏好设置</string>
|
||||
<string name="privacy_redefined">重新定义隐私</string>
|
||||
<string name="v4_3_improved_privacy_and_security">改进的隐私和安全</string>
|
||||
<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="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_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="your_profile_will_be_sent">您的聊天资料将被发送给您的联系人</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>
|
||||
<string name="smp_servers_save">保存服务器</string>
|
||||
<string name="incognito_info_allows">它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。</string>
|
||||
<string name="incognito_info_find">要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。</string>
|
||||
<string name="incognito_info_share">当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。</string>
|
||||
<string name="v4_3_improved_server_configuration">改进的服务器配置</string>
|
||||
<string name="icon_descr_email">电邮</string>
|
||||
<string name="edit_image">编辑图片</string>
|
||||
<string name="button_edit_group_profile">编辑群组资料</string>
|
||||
<string name="error_encrypting_database">加密数据库错误</string>
|
||||
<string name="error_exporting_chat_database">导出聊天数据库错误</string>
|
||||
<string name="error_importing_database">导入聊天数据库错误</string>
|
||||
<string name="error_joining_group">加入群组错误</string>
|
||||
<string name="error_deleting_user">删除用户资料错误</string>
|
||||
<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="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>
|
||||
<string name="settings_notifications_mode_title">通知服务</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -27,6 +27,9 @@
|
||||
<string name="sender_you_pronoun">you</string>
|
||||
<string name="unknown_message_format">unknown message format</string>
|
||||
<string name="invalid_message_format">invalid message format</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="invalid_chat">invalid chat</string>
|
||||
<string name="invalid_data">invalid data</string>
|
||||
|
||||
<!-- PendingContactConnection - ChatModel.kt -->
|
||||
<string name="connection_local_display_name">connection <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
@@ -57,6 +60,13 @@
|
||||
<string name="error_saving_smp_servers">Error saving SMP servers</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Make sure SMP server addresses are in correct format, line separated and are not duplicated.</string>
|
||||
<string name="error_setting_network_config">Error updating network configuration</string>
|
||||
<string name="failed_to_parse_chat_title">Failed to load chat</string>
|
||||
<string name="failed_to_parse_chats_title">Failed to load chats</string>
|
||||
<string name="contact_developers">Please update the app and contact developers.</string>
|
||||
<string name="failed_to_create_user_title">Error creating profile!</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
|
||||
<string name="failed_to_active_user_title">Error switching profile!</string>
|
||||
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Connection timeout</string>
|
||||
@@ -90,6 +100,7 @@
|
||||
<string name="smp_server_test_secure_queue">Secure queue</string>
|
||||
<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>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||
@@ -250,6 +261,8 @@
|
||||
<string name="icon_descr_server_status_pending">Pending</string>
|
||||
<string name="switch_receiving_address_question">Switch receiving address?</string>
|
||||
<string name="switch_receiving_address_desc">This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</string>
|
||||
<string name="view_security_code">View security code</string>
|
||||
<string name="verify_security_code">Verify security code</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Send Message</string>
|
||||
@@ -259,6 +272,11 @@
|
||||
<string name="voice_messages_prohibited">Voice messages prohibited!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Please ask your contact to enable sending voice messages.</string>
|
||||
<string name="only_group_owners_can_enable_voice">Only group owners can enable voice messages.</string>
|
||||
<string name="send_live_message">Send live message</string>
|
||||
<string name="live_message">Live message!</string>
|
||||
<string name="send_live_message_desc">Send a live message - it will update for the recipient(s) as you type it</string>
|
||||
<string name="send_verb">Send</string>
|
||||
<string name="icon_descr_cancel_live_message">Cancel live message</string>
|
||||
|
||||
<!-- General Actions / Responses -->
|
||||
<string name="back">Back</string>
|
||||
@@ -385,9 +403,23 @@
|
||||
<string name="one_time_link">One-time invitation link</string>
|
||||
<string name="your_contact_address">Your contact address</string>
|
||||
|
||||
<!-- ScanCodeView.kt -->
|
||||
<string name="scan_code">Scan code</string>
|
||||
<string name="incorrect_code">Incorrect security code!</string>
|
||||
<string name="scan_code_from_contacts_app">Scan security code from your contact\'s app.</string>
|
||||
|
||||
<!-- VerifyCodeView.kt -->
|
||||
<string name="security_code">Security code</string>
|
||||
<string name="mark_code_verified">Mark verified</string>
|
||||
<string name="clear_verification">Clear verification</string>
|
||||
<string name="to_verify_compare">To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</string>
|
||||
<string name="is_verified">%s is verified</string>
|
||||
<string name="is_not_verified">%s is not verified</string>
|
||||
|
||||
<!-- settings - SettingsView.kt -->
|
||||
<string name="your_settings">Your settings</string>
|
||||
<string name="your_simplex_contact_address">Your <xliff:g id="appName">SimpleX</xliff:g> contact address</string>
|
||||
<string name="your_chat_profiles">Your chat profiles</string>
|
||||
<string name="database_passphrase_and_export">Database passphrase & export</string>
|
||||
<string name="about_simplex_chat">About <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="how_to_use_simplex_chat">How to use it</string>
|
||||
@@ -417,6 +449,7 @@
|
||||
<string name="smp_servers_invalid_address">Invalid server address!</string>
|
||||
<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="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>
|
||||
@@ -452,7 +485,19 @@
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string>
|
||||
<string name="network_session_mode_transport_isolation">Transport isolation</string>
|
||||
<string name="network_session_mode_user">Chat profile</string>
|
||||
<string name="network_session_mode_entity">Connection</string>
|
||||
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
|
||||
<string name="network_session_mode_entity_description">A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</string>
|
||||
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
|
||||
<string name="appearance_settings">Appearance</string>
|
||||
<string name="app_version_title">App version</string>
|
||||
<string name="app_version_name">App version: v%s</string>
|
||||
<string name="app_version_code">App build: %s</string>
|
||||
<string name="core_version">Core version: v%s</string>
|
||||
<string name="core_build_timestamp">Core built at: %s</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
|
||||
<!-- Address Items - UserAddressView.kt -->
|
||||
<string name="create_address">Create address</string>
|
||||
@@ -471,7 +516,7 @@
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Display name:</string>
|
||||
<string name="full_name__field">"Full name:</string>
|
||||
<string name="your_chat_profile">Your chat profile</string>
|
||||
<string name="your_current_profile">Your current profile</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts.\n\n<xliff:g id="appName">SimpleX</xliff:g> servers cannot see your profile.</string>
|
||||
<string name="edit_image">Edit image</string>
|
||||
<string name="delete_image">Delete image</string>
|
||||
@@ -545,6 +590,17 @@
|
||||
<string name="read_more_in_github">Read more in our GitHub repository.</string>
|
||||
<string name="read_more_in_github_with_link">Read more in our <font color="#0088ff">GitHub repository</font>.</string>
|
||||
|
||||
<!-- SetNotificationsMode.kt -->
|
||||
<string name="use_chat">Use chat</string>
|
||||
<string name="onboarding_notifications_mode_title">Private notifications</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">It can be changed later via settings.</string>
|
||||
<string name="onboarding_notifications_mode_off">When app is running</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periodic</string>
|
||||
<string name="onboarding_notifications_mode_service">Instant</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Best for battery</b>. You will receive notifications only when the app is running, background service will NOT be used.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Good for battery</b>. Background service checks for new messages every 10 minutes. You may miss calls and urgent messages.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Uses more battery</b>! Background service is always running – notifications will be shown as soon as the messages are available.</string>
|
||||
|
||||
<!-- MakeConnection -->
|
||||
<string name="paste_the_link_you_received">Paste received link</string>
|
||||
|
||||
@@ -671,8 +727,9 @@
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Restart the app to create a new chat profile.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Stop chat to enable database actions.</string>
|
||||
<string name="data_section">DATA</string>
|
||||
<string name="delete_files_and_media">Delete files \& media</string>
|
||||
<string name="files_and_media_section">Files & media</string>
|
||||
<string name="delete_files_and_media_for_all_users">Delete files for all chat profiles</string>
|
||||
<string name="delete_files_and_media_all">Delete all files</string>
|
||||
<string name="delete_files_and_media_question">Delete files and media?</string>
|
||||
<string name="delete_files_and_media_desc">This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</string>
|
||||
<string name="no_received_app_files">No received or sent files</string>
|
||||
@@ -682,6 +739,8 @@
|
||||
<string name="chat_item_ttl_week">1 week</string>
|
||||
<string name="chat_item_ttl_month">1 month</string>
|
||||
<string name="chat_item_ttl_seconds">%s second(s)</string>
|
||||
<string name="messages_section_title">Messages</string>
|
||||
<string name="messages_section_description">This setting applies to messages in your current chat profile</string>
|
||||
<string name="delete_messages_after">Delete messages after</string>
|
||||
<string name="enable_automatic_deletion_question">Enable automatic message deletion?</string>
|
||||
<string name="enable_automatic_deletion_message">This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</string>
|
||||
@@ -853,6 +912,7 @@
|
||||
<string name="button_leave_group">Leave group</string>
|
||||
<string name="button_edit_group_profile">Edit group profile</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>
|
||||
<string name="delete_link_question">Delete link?</string>
|
||||
<string name="delete_link">Delete link</string>
|
||||
@@ -914,6 +974,7 @@
|
||||
<string name="network_option_tcp_connection_timeout">TCP connection timeout</string>
|
||||
<string name="network_option_protocol_timeout">Protocol timeout</string>
|
||||
<string name="network_option_ping_interval">PING interval</string>
|
||||
<string name="network_option_ping_count">PING count</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">Enable TCP keep-alive</string>
|
||||
<string name="network_options_revert">Revert</string>
|
||||
<string name="network_options_save">Save</string>
|
||||
@@ -921,6 +982,15 @@
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Updating settings will re-connect the client to all servers.</string>
|
||||
<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>
|
||||
|
||||
<!-- Incognito mode -->
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="incognito_random_profile">Your random profile</string>
|
||||
@@ -957,6 +1027,7 @@
|
||||
<string name="group_preferences">Group preferences</string>
|
||||
<string name="set_group_preferences">Set group preferences</string>
|
||||
<string name="your_preferences">Your preferences</string>
|
||||
<string name="timed_messages">Disappearing messages</string>
|
||||
<string name="direct_messages">Direct messages</string>
|
||||
<string name="full_deletion">Delete for everyone</string>
|
||||
<string name="voice_messages">Voice messages</string>
|
||||
@@ -965,12 +1036,21 @@
|
||||
<string name="feature_enabled_for_contact">enabled for contact</string>
|
||||
<string name="feature_off">off</string>
|
||||
<string name="feature_received_prohibited">received, prohibited</string>
|
||||
<string name="accept_feature">Accept</string>
|
||||
<string name="accept_feature_set_1_day">Set 1 day</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Allow your contacts to send disappearing messages.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Allow disappearing messages only if your contact allows them.</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Prohibit sending disappearing messages.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Allow your contacts to irreversibly delete sent messages.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Allow irreversible message deletion only if your contact allows it to you.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Contacts can mark messages for deletion; you will be able to view them.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Allow your contacts to send voice messages.</string>
|
||||
<string name="allow_voice_messages_only_if">Allow voice messages only if your contact allows them.</string>
|
||||
<string name="prohibit_sending_voice_messages">Prohibit sending voice messages.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Both you and your contact can send disappearing messages.</string>
|
||||
<string name="only_you_can_send_disappearing">Only you can send disappearing messages.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Only your contact can send disappearing messages.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Disappearing messages are prohibited in this chat.</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Both you and your contact can irreversibly delete sent messages.</string>
|
||||
<string name="only_you_can_delete_messages">Only you can irreversibly delete messages (your contact can mark them for deletion).</string>
|
||||
<string name="only_your_contact_can_delete">Only your contact can irreversibly delete messages (you can mark them for deletion).</string>
|
||||
@@ -979,17 +1059,78 @@
|
||||
<string name="only_you_can_send_voice">Only you can send voice messages.</string>
|
||||
<string name="only_your_contact_can_send_voice">Only your contact can send voice messages.</string>
|
||||
<string name="voice_prohibited_in_this_chat">Voice messages are prohibited in this chat.</string>
|
||||
<string name="allow_to_send_disappearing">Allow to send disappearing messages.</string>
|
||||
<string name="prohibit_sending_disappearing">Prohibit sending disappearing messages.</string>
|
||||
<string name="allow_direct_messages">Allow sending direct messages to members.</string>
|
||||
<string name="prohibit_direct_messages">Prohibit sending direct messages to members.</string>
|
||||
<string name="allow_to_delete_messages">Allow to irreversibly delete sent messages.</string>
|
||||
<string name="prohibit_message_deletion">Prohibit irreversible message deletion.</string>
|
||||
<string name="allow_to_send_voice">Allow to send voice messages.</string>
|
||||
<string name="prohibit_sending_voice">Prohibit sending voice messages.</string>
|
||||
<string name="group_members_can_send_disappearing">Group members can send disappearing messages.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Disappearing messages are prohibited in this group.</string>
|
||||
<string name="group_members_can_send_dms">Group members can send direct messages.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Direct messages between members are prohibited in this group.</string>
|
||||
<string name="group_members_can_delete">Group members can irreversibly delete sent messages.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">Irreversible message deletion is prohibited in this group.</string>
|
||||
<string name="group_members_can_send_voice">Group members can send voice messages.</string>
|
||||
<string name="voice_messages_are_prohibited">Voice messages are prohibited in this group.</string>
|
||||
<string name="delete_after">Delete after</string>
|
||||
<string name="ttl_sec">%d sec</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_month">%d month</string>
|
||||
<string name="ttl_months">%d months</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="ttl_hour">%d hour</string>
|
||||
<string name="ttl_hours">%d hours</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="ttl_day">%d day</string>
|
||||
<string name="ttl_days">%d days</string>
|
||||
<string name="ttl_d">%dd</string>
|
||||
<string name="ttl_week">%d week</string>
|
||||
<string name="ttl_weeks">%d weeks</string>
|
||||
<string name="ttl_w">%dw</string>
|
||||
<string name="feature_offered_item">offered %s</string>
|
||||
<string name="feature_offered_item_with_param">offered %s: %2s</string>
|
||||
<string name="feature_cancelled_item">cancelled %s</string>
|
||||
|
||||
<!-- WhatsNewView.kt -->
|
||||
<string name="whats_new">What\'s new</string>
|
||||
<string name="new_in_version">New in %s</string>
|
||||
<string name="v4_2_security_assessment">Security assessment</string>
|
||||
<string name="v4_2_security_assessment_desc">SimpleX Chat security was audited by Trail of Bits.</string>
|
||||
<string name="v4_2_group_links">Group links</string>
|
||||
<string name="v4_2_group_links_desc">Admins can create the links to join groups.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Auto-accept contact requests</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">With optional welcome message.</string>
|
||||
<string name="v4_3_voice_messages">Voice messages</string>
|
||||
<string name="v4_3_voice_messages_desc">Max 40 seconds, received instantly.</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Irreversible message deletion</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Your contacts can allow full message deletion.</string>
|
||||
<string name="v4_3_improved_server_configuration">Improved server configuration</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Add servers by scanning QR codes.</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Improved privacy and security</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Hide app screen in the recent apps.</string>
|
||||
<string name="v4_4_disappearing_messages">Disappearing messages</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Sent messages will be deleted after set time.</string>
|
||||
<string name="v4_4_live_messages">Live messages</string>
|
||||
<string name="v4_4_live_messages_desc">Recipients see updates as you type them.</string>
|
||||
<string name="v4_4_verify_connection_security">Verify connection security</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Compare security codes with your contacts.</string>
|
||||
<string name="v4_4_french_interface">French interface</string>
|
||||
<string name="v4_4_french_interface_descr">Thanks to the users – contribute via Weblate!</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Multiple chat profiles</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Different names, avatars and transport isolation.</string>
|
||||
<string name="v4_5_message_draft">Message draft</string>
|
||||
<string name="v4_5_message_draft_descr">Preserve the last message draft, with attachments.</string>
|
||||
<string name="v4_5_transport_isolation">Transport isolation</string>
|
||||
<string name="v4_5_transport_isolation_descr">By chat profile (default) or by connection (BETA).</string>
|
||||
<string name="v4_5_private_filenames">Private filenames</string>
|
||||
<string name="v4_5_private_filenames_descr">To protect timezone, image/voice files use UTC.</string>
|
||||
<string name="v4_5_reduced_battery_usage">Reduced battery usage</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">More improvements are coming soon!</string>
|
||||
<string name="v4_5_italian_interface">Italian interface</string>
|
||||
<string name="v4_5_italian_interface_descr">Thanks to the users – contribute via Weblate!</string>
|
||||
</resources>
|
||||
|
||||
@@ -50,7 +50,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
|
||||
m.tokenStatus = .active
|
||||
} catch {
|
||||
if let cr = error as? ChatResponse, case .chatCmdError(.errorAgent(.NTF(.AUTH))) = cr {
|
||||
if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr {
|
||||
m.tokenStatus = .expired
|
||||
}
|
||||
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")
|
||||
@@ -77,6 +77,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
logger.debug("AppDelegate: applicationWillTerminate")
|
||||
ChatModel.shared.filesToDelete.forEach {
|
||||
removeFile($0)
|
||||
}
|
||||
ChatModel.shared.filesToDelete = []
|
||||
terminateChat()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ struct ContentView: View {
|
||||
@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
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = true
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
||||
@State private var showWhatsNew = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -58,12 +59,21 @@ struct ContentView: View {
|
||||
onAuthorized: { notificationAlertShown = false }
|
||||
)
|
||||
// Local Authentication notice is to be shown on next start after onboarding is complete
|
||||
if (!prefLANoticeShown && prefShowLANotice) {
|
||||
if (!prefLANoticeShown && prefShowLANotice && !chatModel.chats.isEmpty) {
|
||||
prefLANoticeShown = true
|
||||
alertManager.showAlert(laNoticeAlert())
|
||||
} else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
if !showWhatsNew {
|
||||
showWhatsNew = shouldShowWhatsNew()
|
||||
}
|
||||
}
|
||||
}
|
||||
prefShowLANotice = true
|
||||
}
|
||||
.sheet(isPresented: $showWhatsNew) {
|
||||
WhatsNewView()
|
||||
}
|
||||
if chatModel.showCallView, let call = chatModel.activeCall {
|
||||
ActiveCallView(call: call)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import UIKit
|
||||
//import UIKit
|
||||
|
||||
let s = """
|
||||
{
|
||||
@@ -15,6 +15,6 @@ let s = """
|
||||
}
|
||||
"""
|
||||
//let s = "\"2022-04-24T11:59:23.703162Z\""
|
||||
let json = getJSONDecoder()
|
||||
let d = s.data(using: .utf8)!
|
||||
print (try! json.decode(ChatInfo.self, from: d))
|
||||
//let json = getJSONDecoder()
|
||||
//let d = s.data(using: .utf8)!
|
||||
//print (try! json.decode(ChatInfo.self, from: d))
|
||||
|
||||
@@ -16,6 +16,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var onboardingStage: OnboardingStage?
|
||||
@Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get()
|
||||
@Published var currentUser: User?
|
||||
@Published var users: [UserInfo] = []
|
||||
@Published var chatInitialized = false
|
||||
@Published var chatRunning: Bool?
|
||||
@Published var chatDbChanged = false
|
||||
@@ -23,6 +24,8 @@ final class ChatModel: ObservableObject {
|
||||
@Published var chatDbStatus: DBMigrationResult?
|
||||
// list of chat "previews"
|
||||
@Published var chats: [Chat] = []
|
||||
// map of connections network statuses, key is agent connection id
|
||||
@Published var networkStatuses: Dictionary<String, NetworkStatus> = [:]
|
||||
// current chat
|
||||
@Published var chatId: String?
|
||||
@Published var reversedChatItems: [ChatItem] = []
|
||||
@@ -54,10 +57,14 @@ final class ChatModel: ObservableObject {
|
||||
@Published var connReqInv: String?
|
||||
// audio recording and playback
|
||||
@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> = [:]
|
||||
|
||||
var filesToDelete: [String] = []
|
||||
|
||||
static let shared = ChatModel()
|
||||
|
||||
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
|
||||
@@ -101,7 +108,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateContact(_ contact: Contact) {
|
||||
updateChat(.direct(contact: contact), addMissing: contact.directContact)
|
||||
updateChat(.direct(contact: contact), addMissing: contact.directOrUsed)
|
||||
}
|
||||
|
||||
func updateGroup(_ groupInfo: GroupInfo) {
|
||||
@@ -127,17 +134,9 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func updateNetworkStatus(_ id: ChatId, _ status: Chat.NetworkStatus) {
|
||||
if let i = getChatIndex(id) {
|
||||
chats[i].serverInfo.networkStatus = status
|
||||
}
|
||||
}
|
||||
|
||||
func replaceChat(_ id: String, _ chat: Chat) {
|
||||
if let i = getChatIndex(id) {
|
||||
let serverInfo = chats[i].serverInfo
|
||||
chats[i] = chat
|
||||
chats[i].serverInfo = serverInfo
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
chats.insert(chat, at: 0)
|
||||
@@ -163,7 +162,7 @@ final class ChatModel: ObservableObject {
|
||||
addChat(Chat(c), at: i)
|
||||
}
|
||||
}
|
||||
NtfManager.shared.setNtfBadgeCount(totalUnreadCount())
|
||||
NtfManager.shared.setNtfBadgeCount(totalUnreadCountForAllUsers())
|
||||
}
|
||||
|
||||
// func addGroup(_ group: SimpleXChat.Group) {
|
||||
@@ -176,7 +175,7 @@ final class ChatModel: ObservableObject {
|
||||
chats[i].chatItems = [cItem]
|
||||
if case .rcvNew = cItem.meta.itemStatus {
|
||||
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount + 1
|
||||
NtfManager.shared.incNtfBadgeCount()
|
||||
increaseUnreadCounter(user: currentUser!)
|
||||
}
|
||||
if i > 0 {
|
||||
if chatId == nil {
|
||||
@@ -219,7 +218,7 @@ final class ChatModel: ObservableObject {
|
||||
private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool {
|
||||
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
let ci = reversedChatItems[i]
|
||||
withAnimation(.default) {
|
||||
withAnimation {
|
||||
self.reversedChatItems[i] = cItem
|
||||
self.reversedChatItems[i].viewTimestamp = .now
|
||||
// on some occasions the confirmation of message being accepted by the server (tick)
|
||||
@@ -230,9 +229,18 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
withAnimation { reversedChatItems.insert(cItem, at: 0) }
|
||||
withAnimation(itemAnimation()) {
|
||||
reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func itemAnimation() -> Animation? {
|
||||
switch cItem.chatDir {
|
||||
case .directSnd, .groupSnd: return cItem.meta.isLive ? nil : .default
|
||||
default: return .default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
@@ -248,9 +256,6 @@ final class ChatModel: ObservableObject {
|
||||
// remove from current chat
|
||||
if chatId == cInfo.id {
|
||||
if let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
if reversedChatItems[i].isRcvNew {
|
||||
NtfManager.shared.decNtfBadgeCount()
|
||||
}
|
||||
_ = withAnimation {
|
||||
self.reversedChatItems.remove(at: i)
|
||||
}
|
||||
@@ -274,10 +279,44 @@ final class ChatModel: ObservableObject {
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateCurrentUser(_ newProfile: Profile, _ preferences: FullPreferences? = nil) {
|
||||
if let current = currentUser {
|
||||
currentUser?.profile = toLocalProfile(current.profile.profileId, newProfile, "")
|
||||
if let preferences = preferences {
|
||||
currentUser?.fullPreferences = preferences
|
||||
}
|
||||
if let current = currentUser, let i = users.firstIndex(where: { $0.user.userId == current.userId }) {
|
||||
users[i].user = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem {
|
||||
let cItem = ChatItem.liveDummy(chatInfo.chatType)
|
||||
withAnimation {
|
||||
reversedChatItems.insert(cItem, at: 0)
|
||||
}
|
||||
return cItem
|
||||
}
|
||||
|
||||
func removeLiveDummy(animated: Bool = true) {
|
||||
if hasLiveDummy {
|
||||
if animated {
|
||||
withAnimation { _ = reversedChatItems.removeFirst() }
|
||||
} else {
|
||||
_ = reversedChatItems.removeFirst()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var hasLiveDummy: Bool {
|
||||
reversedChatItems.first?.isLiveDummy == true
|
||||
}
|
||||
|
||||
func markChatItemsRead(_ cInfo: ChatInfo) {
|
||||
// update preview
|
||||
_updateChat(cInfo.id) { chat in
|
||||
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
||||
chat.chatStats = ChatStats()
|
||||
}
|
||||
// update current chat
|
||||
@@ -289,10 +328,7 @@ final class ChatModel: ObservableObject {
|
||||
private func markCurrentChatRead(fromIndex i: Int = 0) {
|
||||
var j = i
|
||||
while j < reversedChatItems.count {
|
||||
if case .rcvNew = reversedChatItems[j].meta.itemStatus {
|
||||
reversedChatItems[j].meta.itemStatus = .rcvRead
|
||||
reversedChatItems[j].viewTimestamp = .now
|
||||
}
|
||||
markChatItemRead_(j)
|
||||
j += 1
|
||||
}
|
||||
}
|
||||
@@ -313,8 +349,8 @@ final class ChatModel: ObservableObject {
|
||||
// update preview
|
||||
let markedCount = chat.chatStats.unreadCount - unreadBelow
|
||||
if markedCount > 0 {
|
||||
NtfManager.shared.decNtfBadgeCount(by: markedCount)
|
||||
chat.chatStats.unreadCount -= markedCount
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: markedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +368,7 @@ final class ChatModel: ObservableObject {
|
||||
func clearChat(_ cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
if let chat = getChat(cInfo.id) {
|
||||
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
|
||||
self.decreaseUnreadCounter(user: self.currentUser!, by: chat.chatStats.unreadCount)
|
||||
chat.chatItems = []
|
||||
chat.chatStats = ChatStats()
|
||||
chat.chatInfo = cInfo
|
||||
@@ -347,20 +383,48 @@ final class ChatModel: ObservableObject {
|
||||
// update preview
|
||||
decreaseUnreadCounter(cInfo)
|
||||
// update current chat
|
||||
if chatId == cInfo.id, let j = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
reversedChatItems[j].meta.itemStatus = .rcvRead
|
||||
reversedChatItems[j].viewTimestamp = .now
|
||||
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
markChatItemRead_(i)
|
||||
}
|
||||
}
|
||||
|
||||
private func markChatItemRead_(_ i: Int) {
|
||||
let meta = reversedChatItems[i].meta
|
||||
if case .rcvNew = meta.itemStatus {
|
||||
reversedChatItems[i].meta.itemStatus = .rcvRead
|
||||
reversedChatItems[i].viewTimestamp = .now
|
||||
if meta.itemLive != true, let ttl = meta.itemTimed?.ttl {
|
||||
reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decreaseUnreadCounter(_ cInfo: ChatInfo) {
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1
|
||||
decreaseUnreadCounter(user: currentUser!)
|
||||
}
|
||||
}
|
||||
|
||||
func totalUnreadCount() -> Int {
|
||||
chats.reduce(0, { count, chat in count + chat.chatStats.unreadCount })
|
||||
func increaseUnreadCounter(user: User) {
|
||||
changeUnreadCounter(user: user, by: 1)
|
||||
NtfManager.shared.incNtfBadgeCount()
|
||||
}
|
||||
|
||||
func decreaseUnreadCounter(user: User, by: Int = 1) {
|
||||
changeUnreadCounter(user: user, by: -by)
|
||||
NtfManager.shared.decNtfBadgeCount(by: by)
|
||||
}
|
||||
|
||||
private func changeUnreadCounter(user: User, by: Int) {
|
||||
if let i = users.firstIndex(where: { $0.user.id == user.id }) {
|
||||
users[i].unreadCount += by
|
||||
}
|
||||
}
|
||||
|
||||
func totalUnreadCountForAllUsers() -> Int {
|
||||
chats.filter { $0.chatInfo.ntfsEnabled }.reduce(0, { count, chat in count + chat.chatStats.unreadCount }) +
|
||||
users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount })
|
||||
}
|
||||
|
||||
func getPrevChatItem(_ ci: ChatItem) -> ChatItem? {
|
||||
@@ -441,6 +505,21 @@ final class ChatModel: ObservableObject {
|
||||
while i < maxIx && inView(i) { i += 1 }
|
||||
return reversedChatItems[min(i - 1, maxIx)]
|
||||
}
|
||||
|
||||
func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) {
|
||||
networkStatuses[contact.activeConn.agentConnId] = status
|
||||
}
|
||||
|
||||
func contactNetworkStatus(_ contact: Contact) -> NetworkStatus {
|
||||
networkStatuses[contact.activeConn.agentConnId] ?? .unknown
|
||||
}
|
||||
|
||||
func addTerminalItem(_ item: TerminalItem) {
|
||||
if terminalItems.count >= 500 {
|
||||
terminalItems.remove(at: 0)
|
||||
}
|
||||
terminalItems.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
struct UnreadChatItemCounts {
|
||||
@@ -452,65 +531,61 @@ final class Chat: ObservableObject, Identifiable {
|
||||
@Published var chatInfo: ChatInfo
|
||||
@Published var chatItems: [ChatItem]
|
||||
@Published var chatStats: ChatStats
|
||||
@Published var serverInfo = ServerInfo(networkStatus: .unknown)
|
||||
var created = Date.now
|
||||
|
||||
struct ServerInfo: Decodable {
|
||||
var networkStatus: NetworkStatus
|
||||
}
|
||||
|
||||
enum NetworkStatus: Decodable, Equatable {
|
||||
case unknown
|
||||
case connected
|
||||
case disconnected
|
||||
case error(String)
|
||||
|
||||
var statusString: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "connected"
|
||||
case .error: return "error"
|
||||
default: return "connecting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var statusExplanation: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "You are connected to the server used to receive messages from this contact."
|
||||
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
||||
default: return "Trying to connect to the server used to receive messages from this contact."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
get {
|
||||
switch self {
|
||||
case .unknown: return "circle.dotted"
|
||||
case .connected: return "circle.fill"
|
||||
case .disconnected: return "ellipsis.circle.fill"
|
||||
case .error: return "exclamationmark.circle.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(_ cData: ChatData) {
|
||||
self.chatInfo = cData.chatInfo
|
||||
self.chatItems = cData.chatItems
|
||||
self.chatStats = cData.chatStats
|
||||
}
|
||||
|
||||
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats(), serverInfo: ServerInfo = ServerInfo(networkStatus: .unknown)) {
|
||||
init(chatInfo: ChatInfo, chatItems: [ChatItem] = [], chatStats: ChatStats = ChatStats()) {
|
||||
self.chatInfo = chatInfo
|
||||
self.chatItems = chatItems
|
||||
self.chatStats = chatStats
|
||||
self.serverInfo = serverInfo
|
||||
}
|
||||
|
||||
var id: ChatId { get { chatInfo.id } }
|
||||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
||||
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
||||
}
|
||||
|
||||
enum NetworkStatus: Decodable, Equatable {
|
||||
case unknown
|
||||
case connected
|
||||
case disconnected
|
||||
case error(String)
|
||||
|
||||
var statusString: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "connected"
|
||||
case .error: return "error"
|
||||
default: return "connecting"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var statusExplanation: LocalizedStringKey {
|
||||
get {
|
||||
switch self {
|
||||
case .connected: return "You are connected to the server used to receive messages from this contact."
|
||||
case let .error(err): return "Trying to connect to the server used to receive messages from this contact (error: \(err))."
|
||||
default: return "Trying to connect to the server used to receive messages from this contact."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var imageName: String {
|
||||
get {
|
||||
switch self {
|
||||
case .unknown: return "circle.dotted"
|
||||
case .connected: return "circle.fill"
|
||||
case .disconnected: return "ellipsis.circle.fill"
|
||||
case .error: return "exclamationmark.circle.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
//
|
||||
// ImageUtils.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 24/12/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
import SwiftUI
|
||||
|
||||
func getLoadedFilePath(_ file: CIFile?) -> String? {
|
||||
if let fileName = getLoadedFileName(file) {
|
||||
return getAppFilePath(fileName).path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoadedFileName(_ file: CIFile?) -> String? {
|
||||
if let file = file,
|
||||
file.loaded,
|
||||
let fileName = file.filePath {
|
||||
return fileName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
||||
let loadedFilePath = getLoadedFilePath(file)
|
||||
if let loadedFilePath = loadedFilePath, let fileName = file?.filePath {
|
||||
let filePath = getAppFilePath(fileName)
|
||||
do {
|
||||
let data = try Data(contentsOf: filePath)
|
||||
let img = UIImage(data: data)
|
||||
try img?.setGifFromData(data, levelOfIntegrity: 1.0)
|
||||
return img
|
||||
} catch {
|
||||
return UIImage(contentsOfFile: loadedFilePath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveAnimImage(_ image: UIImage) -> String? {
|
||||
let fileName = generateNewFileName("IMG", "gif")
|
||||
guard let imageData = image.imageData else { return nil }
|
||||
return saveFile(imageData, fileName)
|
||||
}
|
||||
|
||||
func saveImage(_ uiImage: UIImage) -> String? {
|
||||
let hasAlpha = imageHasAlpha(uiImage)
|
||||
let ext = hasAlpha ? "png" : "jpg"
|
||||
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE, hasAlpha: hasAlpha) {
|
||||
let fileName = generateNewFileName("IMG", ext)
|
||||
return saveFile(imageDataResized, fileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cropToSquare(_ image: UIImage) -> UIImage {
|
||||
let size = image.size
|
||||
let side = min(size.width, size.height)
|
||||
let newSize = CGSize(width: side, height: side)
|
||||
var origin = CGPoint.zero
|
||||
if size.width > side {
|
||||
origin.x -= (size.width - side) / 2
|
||||
} else if size.height > side {
|
||||
origin.y -= (size.height - side) / 2
|
||||
}
|
||||
return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size), hasAlpha: imageHasAlpha(image))
|
||||
}
|
||||
|
||||
func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? {
|
||||
var img = image
|
||||
var data = hasAlpha ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
||||
var dataSize = data?.count ?? 0
|
||||
while dataSize != 0 && dataSize > maxDataSize {
|
||||
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
||||
let clippedRatio = min(ratio, 2.0)
|
||||
img = reduceSize(img, ratio: clippedRatio, hasAlpha: hasAlpha)
|
||||
data = hasAlpha ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
||||
dataSize = data?.count ?? 0
|
||||
}
|
||||
logger.debug("resizeImageToDataSize final \(dataSize)")
|
||||
return data
|
||||
}
|
||||
|
||||
func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
|
||||
var img = image
|
||||
let hasAlpha = imageHasAlpha(image)
|
||||
var str = compressImageStr(img, hasAlpha: hasAlpha)
|
||||
var dataSize = str?.count ?? 0
|
||||
while dataSize != 0 && dataSize > maxDataSize {
|
||||
let ratio = sqrt(Double(dataSize) / Double(maxDataSize))
|
||||
let clippedRatio = min(ratio, 2.0)
|
||||
img = reduceSize(img, ratio: clippedRatio, hasAlpha: hasAlpha)
|
||||
str = compressImageStr(img, hasAlpha: hasAlpha)
|
||||
dataSize = str?.count ?? 0
|
||||
}
|
||||
logger.debug("resizeImageToStrSize final \(dataSize)")
|
||||
return str
|
||||
}
|
||||
|
||||
func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? {
|
||||
let ext = hasAlpha ? "png" : "jpg"
|
||||
if let data = hasAlpha ? image.pngData() : image.jpegData(compressionQuality: compressionQuality) {
|
||||
return "data:image/\(ext);base64,\(data.base64EncodedString())"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func reduceSize(_ image: UIImage, ratio: CGFloat, hasAlpha: Bool) -> UIImage {
|
||||
let newSize = CGSize(width: floor(image.size.width / ratio), height: floor(image.size.height / ratio))
|
||||
let bounds = CGRect(origin: .zero, size: newSize)
|
||||
return resizeImage(image, newBounds: bounds, drawIn: bounds, hasAlpha: hasAlpha)
|
||||
}
|
||||
|
||||
private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, hasAlpha: Bool) -> UIImage {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = 1.0
|
||||
format.opaque = !hasAlpha
|
||||
return UIGraphicsImageRenderer(bounds: newBounds, format: format).image { _ in
|
||||
image.draw(in: drawIn)
|
||||
}
|
||||
}
|
||||
|
||||
func imageHasAlpha(_ img: UIImage) -> Bool {
|
||||
if let cgImage = img.cgImage {
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
|
||||
if let context = CGContext(data: nil, width: cgImage.width, height: cgImage.height, bitsPerComponent: 8, bytesPerRow: cgImage.width * 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue) {
|
||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height))
|
||||
if let data = context.data {
|
||||
let data = data.assumingMemoryBound(to: UInt8.self)
|
||||
let size = cgImage.width * cgImage.height
|
||||
var i = 0
|
||||
while i < size {
|
||||
if data[i] < 255 { return true }
|
||||
i += 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func saveFileFromURL(_ url: URL) -> String? {
|
||||
let savedFile: String?
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
do {
|
||||
let fileData = try Data(contentsOf: url)
|
||||
let fileName = uniqueCombine(url.lastPathComponent)
|
||||
savedFile = saveFile(fileData, fileName)
|
||||
} catch {
|
||||
logger.error("FileUtils.saveFileFromURL error: \(error.localizedDescription)")
|
||||
savedFile = nil
|
||||
}
|
||||
} else {
|
||||
logger.error("FileUtils.saveFileFromURL startAccessingSecurityScopedResource returned false")
|
||||
savedFile = nil
|
||||
}
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
return savedFile
|
||||
}
|
||||
|
||||
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
|
||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
|
||||
}
|
||||
|
||||
private func uniqueCombine(_ fileName: String) -> String {
|
||||
func tryCombine(_ fileName: String, _ n: Int) -> String {
|
||||
let ns = fileName as NSString
|
||||
let name = ns.deletingPathExtension
|
||||
let ext = ns.pathExtension
|
||||
let suffix = (n == 0) ? "" : "_\(n)"
|
||||
let f = "\(name)\(suffix).\(ext)"
|
||||
return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f
|
||||
}
|
||||
return tryCombine(fileName, 0)
|
||||
}
|
||||
|
||||
private var tsFormatter: DateFormatter?
|
||||
|
||||
private func getTimestamp() -> String {
|
||||
var df: DateFormatter
|
||||
if let tsFormatter = tsFormatter {
|
||||
df = tsFormatter
|
||||
} else {
|
||||
df = DateFormatter()
|
||||
df.dateFormat = "yyyyMMdd_HHmmss"
|
||||
df.locale = Locale(identifier: "US")
|
||||
df.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
tsFormatter = df
|
||||
}
|
||||
return df.string(from: Date())
|
||||
}
|
||||
|
||||
func dropImagePrefix(_ s: String) -> String {
|
||||
dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,")
|
||||
}
|
||||
|
||||
private func dropPrefix(_ s: String, _ prefix: String) -> String {
|
||||
s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s
|
||||
}
|
||||
@@ -28,7 +28,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
private var granted = false
|
||||
private var prevNtfTime: Dictionary<ChatId, Date> = [:]
|
||||
|
||||
|
||||
// Handle notification when app is in background
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
@@ -38,8 +37,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
let chatModel = ChatModel.shared
|
||||
let action = response.actionIdentifier
|
||||
logger.debug("NtfManager.userNotificationCenter: didReceive: action \(action), categoryIdentifier \(content.categoryIdentifier)")
|
||||
if let userId = content.userInfo["userId"] as? Int64,
|
||||
userId != chatModel.currentUser?.userId {
|
||||
changeActiveUser(userId)
|
||||
}
|
||||
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
|
||||
let chatId = content.userInfo["chatId"] as? String {
|
||||
let chatId = content.userInfo["chatId"] as? String {
|
||||
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
|
||||
Task { await acceptContactRequest(contactRequest) }
|
||||
} else {
|
||||
@@ -83,15 +86,22 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
if UIApplication.shared.applicationState == .active {
|
||||
switch content.categoryIdentifier {
|
||||
case ntfCategoryMessageReceived:
|
||||
let recent = recentInTheSameChat(content)
|
||||
if model.chatId == nil {
|
||||
// in the chat list
|
||||
return recentInTheSameChat(content) ? [] : [.sound, .list]
|
||||
// in the chat list...
|
||||
if model.currentUser?.userId == (content.userInfo["userId"] as? Int64) {
|
||||
// ... of the current user
|
||||
return recent ? [] : [.sound, .list]
|
||||
} else {
|
||||
// ... of different user
|
||||
return recent ? [.banner] : [.sound, .banner, .list]
|
||||
}
|
||||
} else if model.chatId == content.targetContentIdentifier {
|
||||
// in the current chat
|
||||
return recentInTheSameChat(content) ? [] : [.sound, .list]
|
||||
return recent ? [] : [.sound, .list]
|
||||
} else {
|
||||
// in another chat
|
||||
return recentInTheSameChat(content) ? [.banner, .list] : [.sound, .banner, .list]
|
||||
return recent ? [.banner, .list] : [.sound, .banner, .list]
|
||||
}
|
||||
// this notification is deliverd from the notifications server
|
||||
// when the app is in foreground it does not need to be shown
|
||||
@@ -189,20 +199,20 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
center.delegate = self
|
||||
}
|
||||
|
||||
func notifyContactRequest(_ contactRequest: UserContactRequest) {
|
||||
func notifyContactRequest(_ user: User, _ contactRequest: UserContactRequest) {
|
||||
logger.debug("NtfManager.notifyContactRequest")
|
||||
addNotification(createContactRequestNtf(contactRequest))
|
||||
addNotification(createContactRequestNtf(user, contactRequest))
|
||||
}
|
||||
|
||||
func notifyContactConnected(_ contact: Contact) {
|
||||
func notifyContactConnected(_ user: User, _ contact: Contact) {
|
||||
logger.debug("NtfManager.notifyContactConnected")
|
||||
addNotification(createContactConnectedNtf(contact))
|
||||
addNotification(createContactConnectedNtf(user, contact))
|
||||
}
|
||||
|
||||
func notifyMessageReceived(_ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
func notifyMessageReceived(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) {
|
||||
logger.debug("NtfManager.notifyMessageReceived")
|
||||
if cInfo.ntfsEnabled {
|
||||
addNotification(createMessageReceivedNtf(cInfo, cItem))
|
||||
addNotification(createMessageReceivedNtf(user, cInfo, cItem))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -94,8 +94,8 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? =
|
||||
logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
ChatModel.shared.terminalItems.append(.cmd(.now, cmd.obfuscated))
|
||||
ChatModel.shared.terminalItems.append(.resp(.now, resp))
|
||||
ChatModel.shared.addTerminalItem(.cmd(.now, cmd.obfuscated))
|
||||
ChatModel.shared.addTerminalItem(.resp(.now, resp))
|
||||
}
|
||||
return resp
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func apiGetActiveUser() throws -> User? {
|
||||
let r = chatSendCmdSync(.showActiveUser)
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(.error(.noActiveUser)): return nil
|
||||
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,26 @@ func apiCreateActiveUser(_ p: Profile) throws -> User {
|
||||
throw r
|
||||
}
|
||||
|
||||
func listUsers() throws -> [UserInfo] {
|
||||
let r = chatSendCmdSync(.listUsers)
|
||||
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))
|
||||
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))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true))
|
||||
switch r {
|
||||
@@ -189,20 +209,21 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
|
||||
}
|
||||
|
||||
func apiGetChats() throws -> [ChatData] {
|
||||
let r = chatSendCmdSync(.apiGetChats)
|
||||
if case let .apiChats(chats) = r { return chats }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetChats: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChats(userId: userId))
|
||||
if case let .apiChats(_, chats) = r { return chats }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat {
|
||||
let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search))
|
||||
if case let .apiChat(chat) = r { return Chat.init(chat) }
|
||||
if case let .apiChat(_, chat) = r { return Chat.init(chat) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, search: String = "") async throws -> [ChatItem] {
|
||||
let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: pagination, search: search))
|
||||
if case let .apiChat(chat) = r { return chat.chatItems }
|
||||
if case let .apiChat(_, chat) = r { return chat.chatItems }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -219,15 +240,15 @@ func loadChat(chat: Chat, search: String = "") {
|
||||
}
|
||||
}
|
||||
|
||||
func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent) async -> ChatItem? {
|
||||
func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false) async -> ChatItem? {
|
||||
let chatModel = ChatModel.shared
|
||||
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg)
|
||||
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live)
|
||||
let r: ChatResponse
|
||||
if type == .direct {
|
||||
var cItem: ChatItem!
|
||||
let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } })
|
||||
r = await chatSendCmd(cmd, bgTask: false)
|
||||
if case let .newChatItem(aChatItem) = r {
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
cItem = aChatItem.chatItem
|
||||
chatModel.messageDelivery[cItem.id] = endTask
|
||||
return cItem
|
||||
@@ -239,7 +260,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
|
||||
return nil
|
||||
} else {
|
||||
r = await chatSendCmd(cmd, bgDelay: msgDelay)
|
||||
if case let .newChatItem(aChatItem) = r {
|
||||
if case let .newChatItem(_, aChatItem) = r {
|
||||
return aChatItem.chatItem
|
||||
}
|
||||
sendMessageErrorAlert(r)
|
||||
@@ -255,15 +276,15 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
|
||||
)
|
||||
}
|
||||
|
||||
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg), bgDelay: msgDelay)
|
||||
if case let .chatItemUpdated(aChatItem) = r { return aChatItem.chatItem }
|
||||
func apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool = false) async throws -> ChatItem {
|
||||
let r = await chatSendCmd(.apiUpdateChatItem(type: type, id: id, itemId: itemId, msg: msg, live: live), bgDelay: msgDelay)
|
||||
if case let .chatItemUpdated(_, aChatItem) = r { return aChatItem.chatItem }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) {
|
||||
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay)
|
||||
if case let .chatItemDeleted(deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
|
||||
if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -271,7 +292,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) {
|
||||
let r = chatSendCmdSync(.apiGetNtfToken)
|
||||
switch r {
|
||||
case let .ntfToken(token, status, ntfMode): return (token, status, ntfMode)
|
||||
case .chatCmdError(.errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||
case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off)
|
||||
default:
|
||||
logger.debug("apiGetNtfToken response: \(String(describing: r), privacy: .public)")
|
||||
return (nil, nil, .off)
|
||||
@@ -310,18 +331,21 @@ func apiDeleteToken(token: DeviceToken) async throws {
|
||||
}
|
||||
|
||||
func getUserSMPServers() throws -> ([ServerCfg], [String]) {
|
||||
let r = chatSendCmdSync(.getUserSMPServers)
|
||||
if case let .userSMPServers(smpServers, presetServers) = r { return (smpServers, presetServers) }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getUserSMPServers: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId))
|
||||
if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setUserSMPServers(smpServers: [ServerCfg]) async throws {
|
||||
try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setUserSMPServers: no current user") }
|
||||
try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers))
|
||||
}
|
||||
|
||||
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
|
||||
let r = await chatSendCmd(.testSMPServer(smpServer: smpServer))
|
||||
if case let .smpTestResult(testFailure) = r {
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") }
|
||||
let r = await chatSendCmd(.testSMPServer(userId: userId, smpServer: smpServer))
|
||||
if case let .smpTestResult(_, testFailure) = r {
|
||||
if let t = testFailure {
|
||||
return .failure(t)
|
||||
}
|
||||
@@ -331,13 +355,15 @@ func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure>
|
||||
}
|
||||
|
||||
func getChatItemTTL() throws -> ChatItemTTL {
|
||||
let r = chatSendCmdSync(.apiGetChatItemTTL)
|
||||
if case let .chatItemTTL(chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("getChatItemTTL: no current user") }
|
||||
let r = chatSendCmdSync(.apiGetChatItemTTL(userId: userId))
|
||||
if case let .chatItemTTL(_, chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setChatItemTTL(_ chatItemTTL: ChatItemTTL) async throws {
|
||||
try await sendCommandOkResp(.apiSetChatItemTTL(seconds: chatItemTTL.seconds))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("setChatItemTTL: no current user") }
|
||||
try await sendCommandOkResp(.apiSetChatItemTTL(userId: userId, seconds: chatItemTTL.seconds))
|
||||
}
|
||||
|
||||
func getNetworkConfig() async throws -> NetCfg? {
|
||||
@@ -356,15 +382,15 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
|
||||
try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings))
|
||||
}
|
||||
|
||||
func apiContactInfo(contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
|
||||
func apiContactInfo(_ contactId: Int64) async throws -> (ConnectionStats?, Profile?) {
|
||||
let r = await chatSendCmd(.apiContactInfo(contactId: contactId))
|
||||
if case let .contactInfo(_, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
|
||||
if case let .contactInfo(_, _, connStats, customUserProfile) = r { return (connStats, customUserProfile) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (ConnectionStats?) {
|
||||
let r = await chatSendCmd(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
|
||||
func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (ConnectionStats?) {
|
||||
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -376,20 +402,54 @@ func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws
|
||||
try await sendCommandOkResp(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId))
|
||||
}
|
||||
|
||||
func apiGetContactCode(_ contactId: Int64) async throws -> (Contact, String) {
|
||||
let r = await chatSendCmd(.apiGetContactCode(contactId: contactId))
|
||||
if case let .contactCode(_, contact, connectionCode) = r { return (contact, connectionCode) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetGroupMemberCode(_ groupId: Int64, _ groupMemberId: Int64) throws -> (GroupMember, String) {
|
||||
let r = chatSendCmdSync(.apiGetGroupMemberCode(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberCode(_, _, member, connectionCode) = r { return (member, connectionCode) }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiVerifyContact(_ contactId: Int64, connectionCode: String?) -> (Bool, String)? {
|
||||
let r = chatSendCmdSync(.apiVerifyContact(contactId: contactId, connectionCode: connectionCode))
|
||||
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
logger.error("apiVerifyContact error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCode: String?) -> (Bool, String)? {
|
||||
let r = chatSendCmdSync(.apiVerifyGroupMember(groupId: groupId, groupMemberId: groupMemberId, connectionCode: connectionCode))
|
||||
if case let .connectionVerified(_, verified, expectedCode) = r { return (verified, expectedCode) }
|
||||
logger.error("apiVerifyGroupMember error: \(String(describing: r))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiAddContact() async -> String? {
|
||||
let r = await chatSendCmd(.addContact, bgTask: false)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||
logger.error("apiAddContact: no current user")
|
||||
return nil
|
||||
}
|
||||
let r = await chatSendCmd(.apiAddContact(userId: userId), bgTask: false)
|
||||
if case let .invitation(_, connReqInvitation) = r { return connReqInvitation }
|
||||
connectionErrorAlert(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiConnect(connReq: String) async -> ConnReqType? {
|
||||
let r = await chatSendCmd(.connect(connReq: connReq))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else {
|
||||
logger.error("apiConnect: no current user")
|
||||
return nil
|
||||
}
|
||||
let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq))
|
||||
let am = AlertManager.shared
|
||||
switch r {
|
||||
case .sentConfirmation: return .invitation
|
||||
case .sentInvitation: return .contact
|
||||
case let .contactAlreadyExists(contact):
|
||||
case let .contactAlreadyExists(_, contact):
|
||||
let m = ChatModel.shared
|
||||
if let c = m.getContactChat(contact.contactId) {
|
||||
await MainActor.run { m.chatId = c.id }
|
||||
@@ -399,19 +459,19 @@ func apiConnect(connReq: String) async -> ConnReqType? {
|
||||
message: "You are already connected to \(contact.displayName)."
|
||||
)
|
||||
return nil
|
||||
case .chatCmdError(.error(.invalidConnReq)):
|
||||
case .chatCmdError(_, .error(.invalidConnReq)):
|
||||
am.showAlertMsg(
|
||||
title: "Invalid connection link",
|
||||
message: "Please check that you used the correct link or ask your contact to send you another one."
|
||||
)
|
||||
return nil
|
||||
case .chatCmdError(.errorAgent(.SMP(.AUTH))):
|
||||
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))):
|
||||
am.showAlertMsg(
|
||||
title: "Connection error (AUTH)",
|
||||
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
|
||||
)
|
||||
return nil
|
||||
case let .chatCmdError(.errorAgent(.INTERNAL(internalErr))):
|
||||
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
|
||||
if internalErr == "SEUniqueID" {
|
||||
am.showAlertMsg(
|
||||
title: "Already connected?",
|
||||
@@ -458,7 +518,7 @@ func deleteChat(_ chat: Chat) async {
|
||||
|
||||
func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo {
|
||||
let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false)
|
||||
if case let .chatCleared(updatedChatInfo) = r { return updatedChatInfo }
|
||||
if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -473,64 +533,70 @@ func clearChat(_ chat: Chat) async {
|
||||
}
|
||||
|
||||
func apiListContacts() throws -> [Contact] {
|
||||
let r = chatSendCmdSync(.listContacts)
|
||||
if case let .contactsList(contacts) = r { return contacts }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiListContacts: no current user") }
|
||||
let r = chatSendCmdSync(.apiListContacts(userId: userId))
|
||||
if case let .contactsList(_, contacts) = r { return contacts }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiUpdateProfile(profile: Profile) async throws -> Profile? {
|
||||
let r = await chatSendCmd(.apiUpdateProfile(profile: profile))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiUpdateProfile: no current user") }
|
||||
let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile))
|
||||
switch r {
|
||||
case .userProfileNoChange: return nil
|
||||
case let .userProfileUpdated(_, toProfile): return toProfile
|
||||
case let .userProfileUpdated(_, _, toProfile): return toProfile
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiSetContactPrefs(contactId: Int64, preferences: Preferences) async throws -> Contact? {
|
||||
let r = await chatSendCmd(.apiSetContactPrefs(contactId: contactId, preferences: preferences))
|
||||
if case let .contactPrefsUpdated(_, toContact) = r { return toContact }
|
||||
if case let .contactPrefsUpdated(_, _, toContact) = r { return toContact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetContactAlias(contactId: Int64, localAlias: String) async throws -> Contact? {
|
||||
let r = await chatSendCmd(.apiSetContactAlias(contactId: contactId, localAlias: localAlias))
|
||||
if case let .contactAliasUpdated(toContact) = r { return toContact }
|
||||
if case let .contactAliasUpdated(_, toContact) = r { return toContact }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> PendingContactConnection? {
|
||||
let r = await chatSendCmd(.apiSetConnectionAlias(connId: connId, localAlias: localAlias))
|
||||
if case let .connectionAliasUpdated(toConnection) = r { return toConnection }
|
||||
if case let .connectionAliasUpdated(_, toConnection) = r { return toConnection }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateUserAddress() async throws -> String {
|
||||
let r = await chatSendCmd(.createMyAddress)
|
||||
if case let .userContactLinkCreated(connReq) = r { return connReq }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiCreateUserAddress: no current user") }
|
||||
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId))
|
||||
if case let .userContactLinkCreated(_, connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteUserAddress() async throws {
|
||||
let r = await chatSendCmd(.deleteMyAddress)
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiDeleteUserAddress: no current user") }
|
||||
let r = await chatSendCmd(.apiDeleteMyAddress(userId: userId))
|
||||
if case .userContactLinkDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() throws -> UserContactLink? {
|
||||
let r = chatSendCmdSync(.showMyAddress)
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiGetUserAddress: no current user") }
|
||||
let r = chatSendCmdSync(.apiShowMyAddress(userId: userId))
|
||||
switch r {
|
||||
case let .userContactLink(contactLink): return contactLink
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
case let .userContactLink(_, contactLink): return contactLink
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? {
|
||||
let r = await chatSendCmd(.addressAutoAccept(autoAccept: autoAccept))
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("userAddressAutoAccept: no current user") }
|
||||
let r = await chatSendCmd(.apiAddressAutoAccept(userId: userId, autoAccept: autoAccept))
|
||||
switch r {
|
||||
case let .userContactLinkUpdated(contactLink): return contactLink
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
case let .userContactLinkUpdated(_, contactLink): return contactLink
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
@@ -539,8 +605,8 @@ func apiAcceptContactRequest(contactReqId: Int64) async -> Contact? {
|
||||
let r = await chatSendCmd(.apiAcceptContact(contactReqId: contactReqId))
|
||||
let am = AlertManager.shared
|
||||
|
||||
if case let .acceptingContactRequest(contact) = r { return contact }
|
||||
if case .chatCmdError(.errorAgent(.SMP(.AUTH))) = r {
|
||||
if case let .acceptingContactRequest(_, contact) = r { return contact }
|
||||
if case .chatCmdError(_, .errorAgent(.SMP(.AUTH))) = r {
|
||||
am.showAlertMsg(
|
||||
title: "Connection error (AUTH)",
|
||||
message: "Sender may have deleted the connection request."
|
||||
@@ -569,17 +635,17 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws {
|
||||
try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat))
|
||||
}
|
||||
|
||||
func receiveFile(fileId: Int64) async {
|
||||
func receiveFile(user: User, fileId: Int64) async {
|
||||
let inline = privacyTransferImagesInlineGroupDefault.get()
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, inline: inline) {
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(chatItem) }
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
|
||||
}
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
let am = AlertManager.shared
|
||||
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
if case .rcvFileAcceptedSndCancelled = r {
|
||||
am.showAlertMsg(
|
||||
title: "Cannot receive file",
|
||||
@@ -588,7 +654,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
} else if !networkErrorAlert(r) {
|
||||
logger.error("apiReceiveFile error: \(String(describing: r))")
|
||||
switch r {
|
||||
case .chatCmdError(.error(.fileAlreadyReceiving)):
|
||||
case .chatCmdError(_, .error(.fileAlreadyReceiving)):
|
||||
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
|
||||
default:
|
||||
am.showAlertMsg(
|
||||
@@ -603,13 +669,13 @@ func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
func networkErrorAlert(_ r: ChatResponse) -> Bool {
|
||||
let am = AlertManager.shared
|
||||
switch r {
|
||||
case let .chatCmdError(.errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))):
|
||||
am.showAlertMsg(
|
||||
title: "Connection timeout",
|
||||
message: "Please check your network connection with \(serverHostname(addr)) and try again."
|
||||
)
|
||||
return true
|
||||
case let .chatCmdError(.errorAgent(.BROKER(addr, .NETWORK))):
|
||||
case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))):
|
||||
am.showAlertMsg(
|
||||
title: "Connection error",
|
||||
message: "Please check your network connection with \(serverHostname(addr)) and try again."
|
||||
@@ -722,14 +788,15 @@ private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
|
||||
}
|
||||
|
||||
func apiNewGroup(_ p: GroupProfile) throws -> GroupInfo {
|
||||
let r = chatSendCmdSync(.newGroup(groupProfile: p))
|
||||
if case let .groupCreated(groupInfo) = r { return groupInfo }
|
||||
guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("apiNewGroup: no current user") }
|
||||
let r = chatSendCmdSync(.apiNewGroup(userId: userId, groupProfile: p))
|
||||
if case let .groupCreated(_, groupInfo) = r { return groupInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiAddMember(_ groupId: Int64, _ contactId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiAddMember(groupId: groupId, contactId: contactId, memberRole: memberRole))
|
||||
if case let .sentGroupInvitation(_, _, member) = r { return member }
|
||||
if case let .sentGroupInvitation(_, _, _, member) = r { return member }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -742,22 +809,22 @@ enum JoinGroupResult {
|
||||
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult {
|
||||
let r = await chatSendCmd(.apiJoinGroup(groupId: groupId))
|
||||
switch r {
|
||||
case let .userAcceptedGroupSent(groupInfo, _): return .joined(groupInfo: groupInfo)
|
||||
case .chatCmdError(.errorAgent(.SMP(.AUTH))): return .invitationRemoved
|
||||
case .chatCmdError(.errorStore(.groupNotFound)): return .groupNotFound
|
||||
case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo)
|
||||
case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): return .invitationRemoved
|
||||
case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiRemoveMember(_ groupId: Int64, _ memberId: Int64) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiRemoveMember(groupId: groupId, memberId: memberId), bgTask: false)
|
||||
if case let .userDeletedMember(_, member) = r { return member }
|
||||
if case let .userDeletedMember(_, _, member) = r { return member }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiMemberRole(_ groupId: Int64, _ memberId: Int64, _ memberRole: GroupMemberRole) async throws -> GroupMember {
|
||||
let r = await chatSendCmd(.apiMemberRole(groupId: groupId, memberId: memberId, memberRole: memberRole), bgTask: false)
|
||||
if case let .memberRoleUser(_, member, _, _) = r { return member }
|
||||
if case let .memberRoleUser(_, _, member, _, _) = r { return member }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -772,13 +839,19 @@ func leaveGroup(_ groupId: Int64) async {
|
||||
|
||||
func apiLeaveGroup(_ groupId: Int64) async throws -> GroupInfo {
|
||||
let r = await chatSendCmd(.apiLeaveGroup(groupId: groupId), bgTask: false)
|
||||
if case let .leftMemberUser(groupInfo) = r { return groupInfo }
|
||||
if case let .leftMemberUser(_, groupInfo) = r { return groupInfo }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiListMembers(_ groupId: Int64) async -> [GroupMember] {
|
||||
let r = await chatSendCmd(.apiListMembers(groupId: groupId))
|
||||
if case let .groupMembers(group) = r { return group.members }
|
||||
if case let .groupMembers(_, group) = r { return group.members }
|
||||
return []
|
||||
}
|
||||
|
||||
func apiListMembersSync(_ groupId: Int64) -> [GroupMember] {
|
||||
let r = chatSendCmdSync(.apiListMembers(groupId: groupId))
|
||||
if case let .groupMembers(_, group) = r { return group.members }
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -792,13 +865,13 @@ func filterMembersToAdd(_ ms: [GroupMember]) -> [Contact] {
|
||||
|
||||
func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws -> GroupInfo {
|
||||
let r = await chatSendCmd(.apiUpdateGroupProfile(groupId: groupId, groupProfile: groupProfile))
|
||||
if case let .groupUpdated(toGroup) = r { return toGroup }
|
||||
if case let .groupUpdated(_, toGroup) = r { return toGroup }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
|
||||
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
|
||||
if case let .groupLinkCreated(_, connReq) = r { return connReq }
|
||||
if case let .groupLinkCreated(_, _, connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -811,14 +884,20 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
|
||||
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
|
||||
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
|
||||
switch r {
|
||||
case let .groupLink(_, connReq):
|
||||
case let .groupLink(_, _, connReq):
|
||||
return connReq
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .groupLinkNotFound)):
|
||||
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
|
||||
return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func apiGetVersion() throws -> CoreVersionInfo {
|
||||
let r = chatSendCmdSync(.showVersion)
|
||||
if case let .versionInfo(info) = r { return info }
|
||||
throw r
|
||||
}
|
||||
|
||||
func initializeChat(start: Bool, dbKey: String? = nil) throws {
|
||||
logger.debug("initializeChat")
|
||||
let m = ChatModel.shared
|
||||
@@ -846,20 +925,17 @@ func startChat() throws {
|
||||
let m = ChatModel.shared
|
||||
try setNetworkConfig(getNetCfg())
|
||||
let justStarted = try apiStartChat()
|
||||
m.users = try listUsers()
|
||||
if justStarted {
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
|
||||
m.chatItemTTL = try getChatItemTTL()
|
||||
let chats = try apiGetChats()
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCount())
|
||||
try getUserChatData()
|
||||
NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers())
|
||||
try refreshCallInvitations()
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode) = apiGetNtfToken()
|
||||
if let token = m.deviceToken {
|
||||
registerToken(token: token)
|
||||
}
|
||||
withAnimation {
|
||||
m.onboardingStage = m.onboardingStage == .step2_CreateProfile
|
||||
m.onboardingStage = m.onboardingStage == .step2_CreateProfile && m.users.count == 1
|
||||
? .step3_SetNotificationsMode
|
||||
: .onboardingComplete
|
||||
}
|
||||
@@ -869,6 +945,30 @@ func startChat() throws {
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
}
|
||||
|
||||
func changeActiveUser(_ userId: Int64) {
|
||||
do {
|
||||
try changeActiveUser_(userId)
|
||||
} catch let error {
|
||||
logger.error("Unable to set active user: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func changeActiveUser_(_ userId: Int64) throws {
|
||||
let m = ChatModel.shared
|
||||
m.currentUser = try apiSetActiveUser(userId)
|
||||
m.users = try listUsers()
|
||||
try getUserChatData()
|
||||
}
|
||||
|
||||
func getUserChatData() throws {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
|
||||
m.chatItemTTL = try getChatItemTTL()
|
||||
let chats = try apiGetChats()
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
}
|
||||
|
||||
class ChatReceiver {
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
@@ -909,28 +1009,36 @@ class ChatReceiver {
|
||||
func processReceivedMsg(_ res: ChatResponse) async {
|
||||
let m = ChatModel.shared
|
||||
await MainActor.run {
|
||||
m.terminalItems.append(.resp(.now, res))
|
||||
m.addTerminalItem(.resp(.now, res))
|
||||
logger.debug("processReceivedMsg: \(res.responseType)")
|
||||
switch res {
|
||||
case let .newContactConnection(connection):
|
||||
m.updateContactConnection(connection)
|
||||
case let .contactConnectionDeleted(connection):
|
||||
m.removeChat(connection.id)
|
||||
case let .contactConnected(contact, _):
|
||||
if contact.directContact {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
m.updateNetworkStatus(contact.id, .connected)
|
||||
NtfManager.shared.notifyContactConnected(contact)
|
||||
case let .newContactConnection(user, connection):
|
||||
if active(user) {
|
||||
m.updateContactConnection(connection)
|
||||
}
|
||||
case let .contactConnecting(contact):
|
||||
if contact.directContact {
|
||||
case let .contactConnectionDeleted(user, connection):
|
||||
if active(user) {
|
||||
m.removeChat(connection.id)
|
||||
}
|
||||
case let .contactConnected(user, contact, _):
|
||||
if active(user) && contact.directOrUsed {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
}
|
||||
case let .receivedContactRequest(contactRequest):
|
||||
if contact.directOrUsed {
|
||||
NtfManager.shared.notifyContactConnected(user, contact)
|
||||
}
|
||||
m.setContactNetworkStatus(contact, .connected)
|
||||
case let .contactConnecting(user, contact):
|
||||
if active(user) && contact.directOrUsed {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
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)
|
||||
@@ -939,15 +1047,15 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
chatInfo: cInfo,
|
||||
chatItems: []
|
||||
))
|
||||
NtfManager.shared.notifyContactRequest(contactRequest)
|
||||
NtfManager.shared.notifyContactRequest(user, contactRequest)
|
||||
}
|
||||
case let .contactUpdated(toContact):
|
||||
let cInfo = ChatInfo.direct(contact: toContact)
|
||||
if m.hasChat(toContact.id) {
|
||||
case let .contactUpdated(user, toContact):
|
||||
if active(user) && m.hasChat(toContact.id) {
|
||||
let cInfo = ChatInfo.direct(contact: toContact)
|
||||
m.updateChatInfo(cInfo)
|
||||
}
|
||||
case let .contactsMerged(intoContact, mergedContact):
|
||||
if m.hasChat(mergedContact.id) {
|
||||
case let .contactsMerged(user, intoContact, mergedContact):
|
||||
if active(user) && m.hasChat(mergedContact.id) {
|
||||
if m.chatId == mergedContact.id {
|
||||
m.chatId = intoContact.id
|
||||
}
|
||||
@@ -957,95 +1065,130 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
updateContactsStatus(contactRefs, status: .disconnected)
|
||||
case let .contactSubError(contact, chatError):
|
||||
case let .contactSubError(user, contact, chatError):
|
||||
if active(user) {
|
||||
m.updateContact(contact)
|
||||
}
|
||||
processContactSubError(contact, chatError)
|
||||
case let .contactSubSummary(contactSubscriptions):
|
||||
case let .contactSubSummary(user, contactSubscriptions):
|
||||
for sub in contactSubscriptions {
|
||||
if active(user) {
|
||||
m.updateContact(sub.contact)
|
||||
}
|
||||
if let err = sub.contactError {
|
||||
processContactSubError(sub.contact, err)
|
||||
} else {
|
||||
m.updateContact(sub.contact)
|
||||
m.updateNetworkStatus(sub.contact.id, .connected)
|
||||
m.setContactNetworkStatus(sub.contact, .connected)
|
||||
}
|
||||
}
|
||||
case let .newChatItem(aChatItem):
|
||||
case let .newChatItem(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
m.addChatItem(cInfo, cItem)
|
||||
if active(user) {
|
||||
m.addChatItem(cInfo, cItem)
|
||||
} else if cItem.isRcvNew && cInfo.ntfsEnabled {
|
||||
m.increaseUnreadCounter(user: user)
|
||||
}
|
||||
if let file = cItem.file,
|
||||
let mc = cItem.content.msgContent,
|
||||
file.fileSize <= MAX_IMAGE_SIZE {
|
||||
file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV {
|
||||
let acceptImages = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
if (mc.isImage && acceptImages)
|
||||
|| (mc.isVoice && ((file.fileSize > MAX_VOICE_MESSAGE_SIZE_INLINE_SEND && acceptImages) || cInfo.chatType == .group)) {
|
||||
Task {
|
||||
await receiveFile(fileId: file.fileId) // TODO check inlineFileMode != IFMSent
|
||||
await receiveFile(user: user, fileId: file.fileId) // TODO check inlineFileMode != IFMSent
|
||||
}
|
||||
}
|
||||
}
|
||||
if cItem.showNotification {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
case let .chatItemStatusUpdated(aChatItem):
|
||||
case let .chatItemStatusUpdated(user, aChatItem):
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
var res = false
|
||||
if !cItem.isDeletedContent {
|
||||
res = m.upsertChatItem(cInfo, cItem)
|
||||
if !cItem.isDeletedContent && (!active(user) || m.upsertChatItem(cInfo, cItem)) {
|
||||
NtfManager.shared.notifyMessageReceived(user, cInfo, cItem)
|
||||
}
|
||||
if res {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
} else if let endTask = m.messageDelivery[cItem.id] {
|
||||
if let endTask = m.messageDelivery[cItem.id] {
|
||||
switch cItem.meta.itemStatus {
|
||||
case .sndSent: endTask()
|
||||
case .sndErrorAuth: endTask()
|
||||
case .sndError: endTask()
|
||||
default: break
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
case let .chatItemUpdated(aChatItem):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .chatItemDeleted(deletedChatItem, toChatItem, _):
|
||||
case let .chatItemUpdated(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .chatItemDeleted(user, deletedChatItem, toChatItem, _):
|
||||
if !active(user) {
|
||||
if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled {
|
||||
m.decreaseUnreadCounter(user: user)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let toChatItem = toChatItem {
|
||||
_ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem)
|
||||
} else {
|
||||
m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem)
|
||||
}
|
||||
case let .receivedGroupInvitation(groupInfo, _, _):
|
||||
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
case let .userAcceptedGroupSent(groupInfo, hostContact):
|
||||
case let .receivedGroupInvitation(user, groupInfo, _, _):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
}
|
||||
case let .userAcceptedGroupSent(user, groupInfo, hostContact):
|
||||
if !active(user) { return }
|
||||
|
||||
m.updateGroup(groupInfo)
|
||||
if let hostContact = hostContact {
|
||||
m.dismissConnReqView(hostContact.activeConn.id)
|
||||
m.removeChat(hostContact.activeConn.id)
|
||||
}
|
||||
case let .joinedGroupMemberConnecting(groupInfo, _, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .deletedMemberUser(groupInfo, _): // TODO update user member
|
||||
m.updateGroup(groupInfo)
|
||||
case let .deletedMember(groupInfo, _, deletedMember):
|
||||
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
||||
case let .leftMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .groupDeleted(groupInfo, _): // TODO update user member
|
||||
m.updateGroup(groupInfo)
|
||||
case let .userJoinedGroup(groupInfo):
|
||||
m.updateGroup(groupInfo)
|
||||
case let .joinedGroupMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .connectedToGroupMember(groupInfo, member):
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
case let .groupUpdated(toGroup):
|
||||
m.updateGroup(toGroup)
|
||||
case let .rcvFileStart(aChatItem):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .rcvFileComplete(aChatItem):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .sndFileStart(aChatItem, _):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .sndFileComplete(aChatItem, _):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .deletedMemberUser(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
case let .deletedMember(user, groupInfo, _, deletedMember):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, deletedMember)
|
||||
}
|
||||
case let .leftMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .groupDeleted(user, groupInfo, _): // TODO update user member
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
case let .userJoinedGroup(user, groupInfo):
|
||||
if active(user) {
|
||||
m.updateGroup(groupInfo)
|
||||
}
|
||||
case let .joinedGroupMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .connectedToGroupMember(user, groupInfo, member):
|
||||
if active(user) {
|
||||
_ = m.upsertGroupMember(groupInfo, member)
|
||||
}
|
||||
case let .groupUpdated(user, toGroup):
|
||||
if active(user) {
|
||||
m.updateGroup(toGroup)
|
||||
}
|
||||
case let .rcvFileStart(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .rcvFileComplete(user, aChatItem):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileStart(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
case let .sndFileComplete(user, aChatItem, _):
|
||||
chatItemSimpleUpdate(user, aChatItem)
|
||||
let cItem = aChatItem.chatItem
|
||||
let mc = cItem.content.msgContent
|
||||
if aChatItem.chatInfo.chatType == .direct,
|
||||
@@ -1069,7 +1212,7 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
// logger.debug("reportNewIncomingVoIPPushPayload success for \(contact.id)")
|
||||
// }
|
||||
// }
|
||||
case let .callOffer(contact, callType, offer, sharedKey, _):
|
||||
case let .callOffer(_, contact, callType, offer, sharedKey, _):
|
||||
withCall(contact) { call in
|
||||
call.callState = .offerReceived
|
||||
call.peerMedia = callType.media
|
||||
@@ -1087,16 +1230,16 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
relay: useRelay
|
||||
)
|
||||
}
|
||||
case let .callAnswer(contact, answer):
|
||||
case let .callAnswer(_, contact, answer):
|
||||
withCall(contact) { call in
|
||||
call.callState = .answerReceived
|
||||
m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates)
|
||||
}
|
||||
case let .callExtraInfo(contact, extraInfo):
|
||||
case let .callExtraInfo(_, contact, extraInfo):
|
||||
withCall(contact) { _ in
|
||||
m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates)
|
||||
}
|
||||
case let .callEnded(contact):
|
||||
case let .callEnded(_, contact):
|
||||
if let invitation = m.callInvitations.removeValue(forKey: contact.id) {
|
||||
CallController.shared.reportCallRemoteEnded(invitation: invitation)
|
||||
}
|
||||
@@ -1120,32 +1263,38 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
}
|
||||
}
|
||||
|
||||
func chatItemSimpleUpdate(_ aChatItem: AChatItem) {
|
||||
func active(_ user: User) -> Bool {
|
||||
user.id == ChatModel.shared.currentUser?.id
|
||||
}
|
||||
|
||||
func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) {
|
||||
let m = ChatModel.shared
|
||||
let cInfo = aChatItem.chatInfo
|
||||
let cItem = aChatItem.chatItem
|
||||
if m.upsertChatItem(cInfo, cItem) {
|
||||
NtfManager.shared.notifyMessageReceived(cInfo, cItem)
|
||||
let notify = { NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) }
|
||||
if !active(user) {
|
||||
notify()
|
||||
} else if m.upsertChatItem(cInfo, cItem) {
|
||||
notify()
|
||||
}
|
||||
}
|
||||
|
||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: Chat.NetworkStatus) {
|
||||
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) {
|
||||
let m = ChatModel.shared
|
||||
for c in contactRefs {
|
||||
m.updateNetworkStatus(c.id, status)
|
||||
m.networkStatuses[c.agentConnId] = status
|
||||
}
|
||||
}
|
||||
|
||||
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
||||
let m = ChatModel.shared
|
||||
m.updateContact(contact)
|
||||
var err: String
|
||||
switch chatError {
|
||||
case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network"
|
||||
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
|
||||
default: err = String(describing: chatError)
|
||||
}
|
||||
m.updateNetworkStatus(contact.id, .error(err))
|
||||
m.setContactNetworkStatus(contact, .error(err))
|
||||
}
|
||||
|
||||
func refreshCallInvitations() throws {
|
||||
@@ -1177,3 +1326,15 @@ private struct UserResponse: Decodable {
|
||||
var user: User?
|
||||
var error: String?
|
||||
}
|
||||
|
||||
struct RuntimeError: Error {
|
||||
let message: String
|
||||
|
||||
init(_ message: String) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
public var localizedDescription: String {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ struct SimpleXApp: App {
|
||||
enteredBackground = ProcessInfo.processInfo.systemUptime
|
||||
}
|
||||
doAuthenticate = false
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCount())
|
||||
NtfManager.shared.setNtfBadgeCount(chatModel.totalUnreadCountForAllUsers())
|
||||
case .active:
|
||||
if chatModel.chatRunning == true {
|
||||
ChatReceiver.shared.start()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user