Compare commits
237 Commits
v4.4.0-bet
...
v4.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9a8d333f7 | ||
|
|
73f8c543e3 | ||
|
|
14945a9296 | ||
|
|
155ffd16ec | ||
|
|
1eb1e52912 | ||
|
|
af173ee5c4 | ||
|
|
06a2f7e4da | ||
|
|
958299784d | ||
|
|
49b6979ff0 | ||
|
|
3c493db613 | ||
|
|
86cc85b3a5 | ||
|
|
0427d2e578 | ||
|
|
c90d911d2a | ||
|
|
2473d14baa | ||
|
|
a36f2147d8 | ||
|
|
3837e92556 | ||
|
|
76505afff2 | ||
|
|
89c9a01b20 | ||
|
|
68517cf852 | ||
|
|
fbbad55a0f | ||
|
|
bcca27bfdb | ||
|
|
c55a7692c5 | ||
|
|
a8aa829e4c | ||
|
|
d5af03ce18 | ||
|
|
101ef7a81a | ||
|
|
71daeed81a | ||
|
|
d44324eb4d | ||
|
|
93d2ef66cf | ||
|
|
8a78943e94 | ||
|
|
d0f0013755 | ||
|
|
f22ee1a6cf | ||
|
|
4a58ca60ac | ||
|
|
b206868730 | ||
|
|
bd4c10b224 | ||
|
|
46d15d1811 | ||
|
|
ea64be55e1 | ||
|
|
c2cd58f63d | ||
|
|
f21fc76ced | ||
|
|
13bd51b97d | ||
|
|
a1ed0a84b8 | ||
|
|
4815e447fa | ||
|
|
d80cad57b6 | ||
|
|
a58be6ebb6 | ||
|
|
dfa0272065 | ||
|
|
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 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -42,6 +42,7 @@ stack.yaml.lock
|
||||
|
||||
# Temporary test files
|
||||
tests/tmp
|
||||
tests/tmp*
|
||||
logs/
|
||||
|
||||
|
||||
|
||||
73
README.md
73
README.md
@@ -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)
|
||||
[Feb 04, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
|
||||
|
||||
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
[Jan 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).
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.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 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)
|
||||
[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 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
[All updates](./blog)
|
||||
|
||||
@@ -146,10 +148,11 @@ What is already implemented:
|
||||
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
|
||||
9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details.
|
||||
10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings.
|
||||
11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections.
|
||||
|
||||
We plan to add soon:
|
||||
|
||||
1. Message queue rotation. Currently the queues created between two users are used until the contact is deleted, providing a long-term pairwise identifiers of the conversation. We are planning to add queue rotation to make these identifiers termporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
1. Automatic message queue rotation. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days).
|
||||
2. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`.
|
||||
3. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time.
|
||||
|
||||
@@ -191,17 +194,24 @@ 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.
|
||||
- ✅ Preserve message drafts.
|
||||
- 🏗 File server to optimize for efficient and private sending of large files.
|
||||
- 🏗 Improved audio & video calls.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 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.
|
||||
- 🏗 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.
|
||||
- Local app files encryption.
|
||||
- Video messages.
|
||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- 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.
|
||||
@@ -209,28 +219,50 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
|
||||
- keep all your contacts and groups even if you lose the domain.
|
||||
- the server doesn't have information about your contacts and groups.
|
||||
- Channels server for large groups and broadcast channels.
|
||||
- Hosting server for large groups, communities and public channels.
|
||||
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
|
||||
- High capacity multi-node SMP relays.
|
||||
|
||||
## 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 +282,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 81
|
||||
versionName "4.4-beta.2"
|
||||
versionCode 99
|
||||
versionName "4.5.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
|
||||
@@ -22,10 +22,12 @@ var TransformOperation;
|
||||
TransformOperation["Decrypt"] = "decrypt";
|
||||
})(TransformOperation || (TransformOperation = {}));
|
||||
let activeCall;
|
||||
let answerTimeout = 30000;
|
||||
const processCommand = (function () {
|
||||
const defaultIceServers = [
|
||||
{ urls: ["stun:stun.simplex.im:443"] },
|
||||
{ urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||
{ urls: ["turn:turn.simplex.im:443?transport=udp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||
{ urls: ["turn:turn.simplex.im:443?transport=tcp"], username: "private", credential: "yleob6AVkiNI87hpR94Z" },
|
||||
];
|
||||
function getCallConfig(encodedInsertableStreams, iceServers, relay) {
|
||||
return {
|
||||
@@ -100,9 +102,16 @@ const processCommand = (function () {
|
||||
const iceCandidates = getIceCandidates(pc, config);
|
||||
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
|
||||
await setupMediaStreams(call);
|
||||
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
|
||||
pc.addEventListener("connectionstatechange", connectionStateChange);
|
||||
return call;
|
||||
async function connectionStateChange() {
|
||||
// "failed" means the second party did not answer in time (15 sec timeout in Chrome WebView)
|
||||
// See https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/p2p/base/p2p_constants.cc;l=70)
|
||||
if (pc.connectionState !== "failed")
|
||||
connectionHandler();
|
||||
}
|
||||
async function connectionHandler() {
|
||||
sendMessageToNative({
|
||||
resp: {
|
||||
type: "connection",
|
||||
@@ -115,6 +124,7 @@ const processCommand = (function () {
|
||||
},
|
||||
});
|
||||
if (pc.connectionState == "disconnected" || pc.connectionState == "failed") {
|
||||
clearConnectionTimeout();
|
||||
pc.removeEventListener("connectionstatechange", connectionStateChange);
|
||||
if (activeCall) {
|
||||
setTimeout(() => sendMessageToNative({ resp: { type: "ended" } }), 0);
|
||||
@@ -122,6 +132,7 @@ const processCommand = (function () {
|
||||
endCall();
|
||||
}
|
||||
else if (pc.connectionState == "connected") {
|
||||
clearConnectionTimeout();
|
||||
const stats = (await pc.getStats());
|
||||
for (const stat of stats.values()) {
|
||||
const { type, state } = stat;
|
||||
@@ -141,6 +152,12 @@ const processCommand = (function () {
|
||||
}
|
||||
}
|
||||
}
|
||||
function clearConnectionTimeout() {
|
||||
if (connectionTimeout) {
|
||||
clearTimeout(connectionTimeout);
|
||||
connectionTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
function serialize(x) {
|
||||
return LZString.compressToBase64(JSON.stringify(x));
|
||||
|
||||
@@ -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,7 @@ 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()
|
||||
@@ -405,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")
|
||||
@@ -479,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) {
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.net.Uri
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -15,10 +13,12 @@ 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.*
|
||||
@@ -26,6 +26,7 @@ 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.*
|
||||
|
||||
/*
|
||||
@@ -35,6 +36,7 @@ import kotlin.time.*
|
||||
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)
|
||||
@@ -42,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)
|
||||
@@ -81,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 }
|
||||
@@ -120,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
|
||||
@@ -139,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) {
|
||||
@@ -157,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
|
||||
@@ -168,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
|
||||
@@ -181,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
|
||||
@@ -212,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 {
|
||||
@@ -248,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
|
||||
@@ -256,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
|
||||
@@ -264,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
|
||||
)
|
||||
@@ -302,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)
|
||||
@@ -353,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) {
|
||||
@@ -388,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 {
|
||||
@@ -419,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,
|
||||
@@ -557,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
|
||||
@@ -629,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"
|
||||
@@ -643,6 +731,7 @@ class ContactSubStatus(
|
||||
@Serializable
|
||||
data class Connection(
|
||||
val connId: Long,
|
||||
val agentConnId: String,
|
||||
val connStatus: ConnStatus,
|
||||
val connLevel: Int,
|
||||
val viaGroupLink: Boolean,
|
||||
@@ -651,7 +740,7 @@ data class Connection(
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -769,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
|
||||
@@ -1167,6 +1257,7 @@ data class ChatItem (
|
||||
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))
|
||||
@@ -1253,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(),
|
||||
@@ -1274,6 +1366,35 @@ data class ChatItem (
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1340,6 +1461,22 @@ data class CIMeta (
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1404,6 +1541,7 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("sndGroupFeature") class SndGroupFeature(val groupFeature: GroupFeature, val preference: GroupPreference, val param: Int? = null): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvChatFeatureRejected") class RcvChatFeatureRejected(val feature: ChatFeature): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("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
|
||||
@@ -1427,11 +1565,12 @@ sealed class CIContent: ItemContent {
|
||||
is SndGroupFeature -> featureText(groupFeature, preference.enable.text, param)
|
||||
is RcvChatFeatureRejected -> "${feature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(R.string.feature_received_prohibited)}"
|
||||
is InvalidJSON -> "invalid data"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun featureText(feature: Feature, enabled: String, param: Int?): String =
|
||||
if (feature.hasParam && param != null) {
|
||||
if (feature.hasParam) {
|
||||
"${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
|
||||
} else {
|
||||
"${feature.text}: $enabled"
|
||||
@@ -1439,11 +1578,11 @@ sealed class CIContent: ItemContent {
|
||||
|
||||
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
|
||||
allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
|
||||
"offered ${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
|
||||
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
allowed != FeatureAllowed.NO ->
|
||||
"offered ${feature.text}"
|
||||
String.format(generalGetString(R.string.feature_offered_item), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
else ->
|
||||
"cancelled ${feature.text}"
|
||||
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, TimedMessagesPreference.ttlText(param))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -138,7 +78,7 @@ fun TerminalLayout(
|
||||
SendMsgView(
|
||||
composeState = composeState,
|
||||
showVoiceRecordIcon = false,
|
||||
recState = mutableStateOf(RecordingState.NotStarted),
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
needToAllowVoiceToContact = false,
|
||||
@@ -147,8 +87,8 @@ fun TerminalLayout(
|
||||
sendMessage = sendCommand,
|
||||
sendLiveMessage = null,
|
||||
updateLiveMessage = null,
|
||||
::onMessageChange,
|
||||
textStyle
|
||||
onMessageChange = ::onMessageChange,
|
||||
textStyle = textStyle
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -174,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(
|
||||
@@ -185,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,13 +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.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
|
||||
) ?: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,10 +297,10 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
|
||||
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
|
||||
InfoText(call.callState.text)
|
||||
|
||||
val connInfo =
|
||||
if (call.connectionInfo == null) ""
|
||||
else " (${call.connectionInfo.text})"
|
||||
InfoText(call.encryptionStatus + connInfo)
|
||||
val connInfo = call.connectionInfo
|
||||
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
|
||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
||||
InfoText(call.encryptionStatus + connInfoText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +480,10 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Video,
|
||||
peerMedia = CallMediaType.Video,
|
||||
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp", null)
|
||||
)
|
||||
),
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
@@ -501,7 +504,10 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Audio,
|
||||
peerMedia = CallMediaType.Audio,
|
||||
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null),
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp", null)
|
||||
)
|
||||
),
|
||||
dismiss = {},
|
||||
toggleAudio = {},
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package chat.simplex.app.views.call
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import androidx.compose.ui.text.toUpperCase
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.Contact
|
||||
import chat.simplex.app.model.User
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
data class Call(
|
||||
val contact: Contact,
|
||||
@@ -61,39 +65,39 @@ enum class CallState {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
|
||||
@Serializable
|
||||
sealed class WCallCommand {
|
||||
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
|
||||
@Serializable @SerialName("start") class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("end") object End: WCallCommand()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class WCallResponse {
|
||||
@Serializable @SerialName("capabilities") class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("offer") class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("answer") class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("ice") class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("capabilities") data class Capabilities(val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val capabilities: CallCapabilities): WCallResponse()
|
||||
@Serializable @SerialName("answer") data class Answer(val answer: String, val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
||||
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
||||
@Serializable @SerialName("error") class Error(val message: String): WCallResponse()
|
||||
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
|
||||
}
|
||||
|
||||
@Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
|
||||
@Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: String)
|
||||
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: String)
|
||||
@Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
|
||||
@Serializable class RcvCallInvitation(val 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,19 +107,32 @@ sealed class WCallResponse {
|
||||
CallMediaType.Audio -> R.string.incoming_audio_call
|
||||
})
|
||||
}
|
||||
@Serializable class CallCapabilities(val encryption: Boolean)
|
||||
@Serializable class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||
val text: String @Composable get() = when {
|
||||
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
|
||||
stringResource(R.string.call_connection_peer_to_peer)
|
||||
localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay ->
|
||||
stringResource(R.string.call_connection_via_relay)
|
||||
else ->
|
||||
"${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}"
|
||||
@Serializable data class CallCapabilities(val encryption: Boolean)
|
||||
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||
val text: String @Composable get() {
|
||||
val local = localCandidate?.candidateType
|
||||
val remote = remoteCandidate?.candidateType
|
||||
return when {
|
||||
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
|
||||
stringResource(R.string.call_connection_peer_to_peer)
|
||||
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
|
||||
stringResource(R.string.call_connection_via_relay)
|
||||
else ->
|
||||
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
||||
}
|
||||
}
|
||||
|
||||
val protocolText: String get() {
|
||||
val local = localCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
||||
val localRelay = localCandidate?.relayProtocol?.uppercase(Locale.ROOT) ?: "unknown"
|
||||
val remote = remoteCandidate?.protocol?.uppercase(Locale.ROOT) ?: "unknown"
|
||||
val localText = if (localRelay == local || localCandidate?.relayProtocol == null) local else "$local ($localRelay)"
|
||||
return if (local == remote) localText else "$localText / $remote"
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
|
||||
@Serializable data class RTCIceCandidate(val candidateType: RTCIceCandidateType?, val protocol: String?, val relayProtocol: String?)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
|
||||
@Serializable data class RTCIceServer(val urls: List<String>, val username: String? = null, val credential: String? = null)
|
||||
|
||||
@@ -150,7 +167,7 @@ enum class VideoCamera {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ConnectionState(
|
||||
data class ConnectionState(
|
||||
val connectionState: String,
|
||||
val iceConnectionState: String,
|
||||
val iceGatheringState: String,
|
||||
@@ -158,20 +175,22 @@ class ConnectionState(
|
||||
)
|
||||
|
||||
// the servers are expected in this format:
|
||||
// stun:stun.simplex.im:443
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443
|
||||
// stun:stun.simplex.im:443?transport=tcp
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
|
||||
fun parseRTCIceServer(str: String): RTCIceServer? {
|
||||
var s = replaceScheme(str, "stun:")
|
||||
s = replaceScheme(s, "turn:")
|
||||
s = replaceScheme(s, "turns:")
|
||||
val u = runCatching { URI(s) }.getOrNull()
|
||||
if (u != null) {
|
||||
val scheme = u.scheme
|
||||
val host = u.host
|
||||
val port = u.port
|
||||
if (u.path == "" && (scheme == "stun" || scheme == "turn")) {
|
||||
if (u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns")) {
|
||||
val userInfo = u.userInfo?.split(":")
|
||||
val query = if (u.query == null || u.query == "") "" else "?${u.query}"
|
||||
return RTCIceServer(
|
||||
urls = listOf("$scheme:$host:$port"),
|
||||
urls = listOf("$scheme:$host:$port$query"),
|
||||
username = userInfo?.getOrNull(0),
|
||||
credential = userInfo?.getOrNull(1)
|
||||
)
|
||||
|
||||
@@ -54,10 +54,14 @@ fun ChatInfoView(
|
||||
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,
|
||||
@@ -149,6 +153,7 @@ fun ChatInfoLayout(
|
||||
chat: Chat,
|
||||
contact: Contact,
|
||||
connStats: ConnectionStats?,
|
||||
contactNetworkStatus: NetworkStatus,
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
connectionCode: String?,
|
||||
@@ -200,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()) {
|
||||
@@ -314,7 +319,7 @@ fun LocalAliasEditor(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
|
||||
private fun NetworkStatusRow(networkStatus: NetworkStatus) {
|
||||
Row(
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -346,14 +351,14 @@ private fun NetworkStatusRow(networkStatus: Chat.NetworkStatus) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private 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)
|
||||
}
|
||||
@@ -446,14 +451,14 @@ 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 = {},
|
||||
|
||||
@@ -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)
|
||||
@@ -204,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) }
|
||||
@@ -226,9 +235,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
},
|
||||
acceptFeature = { contact, feature ->
|
||||
acceptFeature = { contact, feature, param ->
|
||||
withApi {
|
||||
chatModel.controller.allowFeatureToContact(contact, feature)
|
||||
chatModel.controller.allowFeatureToContact(contact, feature, param)
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
@@ -287,7 +296,7 @@ fun ChatLayout(
|
||||
joinGroup: (Long) -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
addMembers: (GroupInfo) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
|
||||
@@ -503,7 +512,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
receiveFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
|
||||
setFloatingButton: (@Composable () -> Unit) -> Unit,
|
||||
onComposed: () -> Unit,
|
||||
@@ -529,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 }
|
||||
@@ -568,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))
|
||||
}
|
||||
}
|
||||
@@ -1026,7 +1035,7 @@ fun PreviewChatLayout() {
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
@@ -1085,7 +1094,7 @@ fun PreviewGroupChatLayout() {
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
addMembers = { _ -> },
|
||||
markRead = { _, _ -> },
|
||||
changeNtfsState = { _, _ -> },
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
@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.*
|
||||
@@ -20,7 +21,8 @@ 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.filled.AttachFile
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.outlined.Reply
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
@@ -32,28 +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.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
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
|
||||
@@ -67,7 +66,8 @@ sealed class ComposeContextItem {
|
||||
data class LiveMessage(
|
||||
val chatItem: ChatItem,
|
||||
val typedMsg: String,
|
||||
val sentMsg: String
|
||||
val sentMsg: String,
|
||||
val sent: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@@ -103,6 +103,9 @@ data class ComposeState(
|
||||
}
|
||||
hasContent && !inProgress
|
||||
}
|
||||
val endLiveDisabled: Boolean
|
||||
get() = liveMessage != null && message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
|
||||
|
||||
val linkPreviewAllowed: Boolean
|
||||
get() =
|
||||
when (preview) {
|
||||
@@ -128,6 +131,9 @@ data class ComposeState(
|
||||
}
|
||||
}
|
||||
|
||||
val empty: Boolean
|
||||
get() = message.isEmpty() && preview is ComposePreview.NoPreview
|
||||
|
||||
companion object {
|
||||
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
|
||||
save = { json.encodeToString(serializer(), it.value) },
|
||||
@@ -148,15 +154,14 @@ sealed class RecordingState {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -177,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 ->
|
||||
@@ -238,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? ->
|
||||
@@ -248,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(
|
||||
@@ -349,9 +343,12 @@ fun ComposeView(
|
||||
}
|
||||
recState.value = RecordingState.NotStarted
|
||||
textStyle.value = smallFont
|
||||
chosenContent.value = emptyList()
|
||||
chosenAudio.value = null
|
||||
chosenFile.value = null
|
||||
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? {
|
||||
@@ -421,15 +418,17 @@ fun ComposeView(
|
||||
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 (cs.liveMessage != null) {
|
||||
sent = updateMessage(cs.liveMessage.chatItem, 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()
|
||||
@@ -437,35 +436,33 @@ fun ComposeView(
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
is ComposePreview.ImagePreview -> {
|
||||
chosenContent.value.forEachIndexed { index, it ->
|
||||
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 (chosenContent.value.lastIndex == index) msgText else "", preview.images[index]))
|
||||
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText 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())
|
||||
AudioPlayer.stop(chosenAudioVal.first.toFile().absolutePath)
|
||||
msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", chosenAudioVal.second / 1000))
|
||||
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 chosenFileVal = chosenFile.value
|
||||
if (chosenFileVal != null) {
|
||||
val file = saveFileFromUri(context, chosenFileVal)
|
||||
if (file != null) {
|
||||
files.add((file))
|
||||
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
|
||||
}
|
||||
val file = saveFileFromUri(context, preview.uri)
|
||||
if (file != null) {
|
||||
files.add((file))
|
||||
msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,7 +477,7 @@ fun ComposeView(
|
||||
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
|
||||
)
|
||||
}
|
||||
if (sent == null && chosenContent.value.isNotEmpty()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -509,7 +506,6 @@ 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))
|
||||
}
|
||||
@@ -532,7 +528,6 @@ fun ComposeView(
|
||||
|
||||
fun cancelImages() {
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
|
||||
chosenContent.value = emptyList()
|
||||
}
|
||||
|
||||
fun cancelVoice() {
|
||||
@@ -544,12 +539,10 @@ fun ComposeView(
|
||||
AudioPlayer.stop(filePath)
|
||||
filePath?.let { File(it).delete() }
|
||||
}
|
||||
chosenAudio.value = null
|
||||
}
|
||||
|
||||
fun cancelFile() {
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
|
||||
chosenFile.value = null
|
||||
}
|
||||
|
||||
fun truncateToWords(s: String): String {
|
||||
@@ -567,13 +560,16 @@ fun ComposeView(
|
||||
}
|
||||
|
||||
suspend fun sendLiveMessage() {
|
||||
val typedMsg = composeState.value.message
|
||||
val sentMsg = truncateToWords(typedMsg)
|
||||
if (composeState.value.liveMessage == null) {
|
||||
val ci = sendMessageAsync(sentMsg, live = true)
|
||||
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 = sentMsg))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +586,7 @@ fun ComposeView(
|
||||
if (sentMsg != null) {
|
||||
val ci = sendMessageAsync(sentMsg, live = true)
|
||||
if (ci != null) {
|
||||
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg))
|
||||
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))
|
||||
@@ -672,7 +668,7 @@ fun ComposeView(
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -695,13 +691,35 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
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 && composeState.value.liveMessage != null) {
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -721,6 +739,10 @@ fun ComposeView(
|
||||
},
|
||||
sendLiveMessage = ::sendLiveMessage,
|
||||
updateLiveMessage = ::updateLiveMessage,
|
||||
cancelLiveMessage = {
|
||||
composeState.value = composeState.value.copy(liveMessage = null)
|
||||
chatModel.removeLiveDummy()
|
||||
},
|
||||
onMessageChange = ::onMessageChange,
|
||||
textStyle = textStyle
|
||||
)
|
||||
|
||||
@@ -6,8 +6,10 @@ 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.*
|
||||
@@ -60,21 +62,26 @@ fun SendMsgView(
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
allowVoiceToContact: () -> Unit,
|
||||
sendMessage: () -> Unit,
|
||||
sendLiveMessage: ( suspend () -> Unit)? = null,
|
||||
sendLiveMessage: (suspend () -> Unit)? = null,
|
||||
updateLiveMessage: (suspend () -> Unit)? = null,
|
||||
cancelLiveMessage: (() -> Unit)? = null,
|
||||
onMessageChange: (String) -> Unit,
|
||||
textStyle: MutableState<TextStyle>
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 8.dp)) {
|
||||
val cs = composeState.value
|
||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
||||
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
||||
NativeKeyboard(composeState, textStyle, onMessageChange)
|
||||
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) }
|
||||
@@ -106,7 +113,10 @@ fun SendMsgView(
|
||||
else ->
|
||||
RecordVoiceView(recState, stopRecOnNextClick)
|
||||
}
|
||||
if (sendLiveMessage != null && updateLiveMessage != null && (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)) {
|
||||
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) {
|
||||
@@ -116,15 +126,24 @@ fun SendMsgView(
|
||||
}
|
||||
}
|
||||
}
|
||||
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 color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
|
||||
if (composeState.value.liveMessage == null &&
|
||||
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) }
|
||||
SendTextButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled(), sendMessage) { showDropdown = true }
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showDropdown,
|
||||
@@ -133,7 +152,7 @@ fun SendMsgView(
|
||||
) {
|
||||
ItemAction(
|
||||
generalGetString(R.string.send_live_message),
|
||||
Icons.Filled.MoreHoriz,
|
||||
Icons.Filled.Bolt,
|
||||
onClick = {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
showDropdown = false
|
||||
@@ -141,7 +160,7 @@ fun SendMsgView(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
SendTextButton(icon, color, sendButtonSize, sendButtonAlpha, cs.sendEnabled(), sendMessage)
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +172,7 @@ fun SendMsgView(
|
||||
private fun NativeKeyboard(
|
||||
composeState: MutableState<ComposeState>,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
onMessageChange: (String) -> Unit
|
||||
) {
|
||||
val cs = composeState.value
|
||||
@@ -163,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) {
|
||||
@@ -184,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/*"))
|
||||
@@ -230,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(
|
||||
@@ -241,6 +262,16 @@ 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) }
|
||||
@@ -312,7 +343,7 @@ private fun VoiceButtonWithoutPermission(onClick: () -> Unit) {
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.size(34.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
@@ -323,7 +354,9 @@ private fun LockToCurrentOrientationUntilDispose() {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context as Activity
|
||||
activity.requestedOrientation = when (activity.display?.rotation) {
|
||||
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
|
||||
@@ -334,7 +367,6 @@ private fun LockToCurrentOrientationUntilDispose() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun StopRecordButton(onClick: () -> Unit) {
|
||||
IconButton(onClick, Modifier.size(36.dp)) {
|
||||
@@ -357,7 +389,7 @@ private fun RecordVoiceButton(interactionSource: MutableInteractionSource) {
|
||||
stringResource(R.string.icon_descr_record_voice_message),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.size(34.dp)
|
||||
.padding(4.dp)
|
||||
)
|
||||
}
|
||||
@@ -369,9 +401,24 @@ private fun ProgressIndicator() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendTextButton(
|
||||
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,
|
||||
backgroundColor: Color,
|
||||
sizeDp: Animatable<Float, AnimationVector1D>,
|
||||
alpha: Animatable<Float, AnimationVector1D>,
|
||||
enabled: Boolean,
|
||||
@@ -400,7 +447,7 @@ private fun SendTextButton(
|
||||
.padding(4.dp)
|
||||
.alpha(alpha.value)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor)
|
||||
.background(if (enabled) MaterialTheme.colors.primary else HighOrLowlight)
|
||||
.padding(3.dp)
|
||||
)
|
||||
}
|
||||
@@ -421,15 +468,12 @@ private fun StartLiveMessageButton(onClick: () -> Unit) {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.MoreHoriz,
|
||||
Icons.Filled.Bolt,
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = Color.White,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.primary)
|
||||
.padding(1.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -457,9 +501,10 @@ private fun startLiveMessage(
|
||||
sendButtonAlpha.snapTo(1f)
|
||||
}
|
||||
scope.launch {
|
||||
delay(3000)
|
||||
while (composeState.value.liveMessage != null) {
|
||||
delay(3000)
|
||||
update()
|
||||
delay(3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,7 +566,7 @@ fun PreviewSendMsgView() {
|
||||
SendMsgView(
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
showVoiceRecordIcon = false,
|
||||
recState = mutableStateOf(RecordingState.NotStarted),
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
@@ -549,7 +594,7 @@ fun PreviewSendMsgViewEditing() {
|
||||
SendMsgView(
|
||||
composeState = remember { mutableStateOf(composeStateEditing) },
|
||||
showVoiceRecordIcon = false,
|
||||
recState = mutableStateOf(RecordingState.NotStarted),
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
@@ -572,12 +617,12 @@ 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,
|
||||
recState = mutableStateOf(RecordingState.NotStarted),
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
needToAllowVoiceToContact = false,
|
||||
|
||||
@@ -427,8 +427,7 @@ 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),
|
||||
|
||||
@@ -66,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.clickable
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
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.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
@@ -29,7 +20,7 @@ fun CIFeaturePreferenceView(
|
||||
contact: Contact?,
|
||||
feature: ChatFeature,
|
||||
allowed: FeatureAllowed,
|
||||
acceptFeature: (Contact, ChatFeature) -> Unit
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
@@ -39,17 +30,20 @@ fun CIFeaturePreferenceView(
|
||||
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(R.string.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) } },
|
||||
onClick = { if (accept(it)) { acceptFeature(contact, feature, param) } },
|
||||
shouldConsumeEvent = ::accept
|
||||
)
|
||||
} else {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
|
||||
if (meta.itemEdited) res += iconSpace
|
||||
if (meta.itemTimed != null) {
|
||||
res += iconSpace
|
||||
val ttl = meta.itemTimed?.ttl
|
||||
val ttl = meta.itemTimed.ttl
|
||||
if (ttl != chatTTL) {
|
||||
res += TimedMessagesPreference.shortTtlText(ttl)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ fun ChatItemView(
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature) -> Unit
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -54,6 +54,7 @@ fun ChatItemView(
|
||||
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
|
||||
@@ -97,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)
|
||||
@@ -133,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
|
||||
@@ -149,7 +150,9 @@ fun ChatItemView(
|
||||
}
|
||||
)
|
||||
}
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +238,7 @@ fun ChatItemView(
|
||||
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
|
||||
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
|
||||
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
|
||||
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,7 +330,7 @@ fun PreviewChatItemView() {
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _ -> }
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -346,7 +350,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _ -> }
|
||||
acceptFeature = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,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)
|
||||
@@ -241,6 +242,7 @@ fun CIMarkdownText(
|
||||
}
|
||||
|
||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
const val MAX_SAFE_WIDTH_HEIGHT = 100_000
|
||||
|
||||
@Composable
|
||||
fun PriorityLayout(
|
||||
@@ -259,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)
|
||||
|
||||
@@ -66,7 +66,7 @@ private fun typing(w: FontWeight = FontWeight.Light): AnnotatedString =
|
||||
|
||||
@Composable
|
||||
fun MarkdownText (
|
||||
text: String,
|
||||
text: CharSequence,
|
||||
formattedText: List<FormattedText>? = null,
|
||||
sender: String? = null,
|
||||
meta: CIMeta? = null,
|
||||
@@ -78,6 +78,7 @@ fun MarkdownText (
|
||||
senderBold: Boolean = false,
|
||||
modifier: Modifier = Modifier,
|
||||
linkMode: SimplexLinkMode,
|
||||
inlineContent: Map<String, InlineTextContent>? = null,
|
||||
onLinkLongClick: (link: String) -> Unit = {}
|
||||
) {
|
||||
val textLayoutDirection = remember (text) {
|
||||
@@ -132,13 +133,14 @@ fun MarkdownText (
|
||||
if (formattedText == null) {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
appendSender(this, sender, senderBold)
|
||||
append(text)
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,16 +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.*
|
||||
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
|
||||
@@ -21,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
|
||||
@@ -67,6 +82,43 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
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) {
|
||||
@@ -91,15 +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,
|
||||
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 {
|
||||
@@ -187,7 +254,7 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
Modifier.padding(top = 52.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ChatStatusImage(chat)
|
||||
ChatStatusImage(contactNetworkStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,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,
|
||||
@@ -221,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)
|
||||
@@ -241,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.media.*
|
||||
import android.media.AudioManager.AudioPlaybackCallback
|
||||
@@ -14,8 +15,6 @@ 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 {
|
||||
fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String
|
||||
@@ -26,6 +25,7 @@ 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"
|
||||
}
|
||||
private var recorder: MediaRecorder? = null
|
||||
private var progressJob: Job? = null
|
||||
@@ -50,8 +50,10 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
rec.setAudioEncodingBitRate(16000)
|
||||
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
|
||||
rec.setMaxFileSize(recordedBytesLimit)
|
||||
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||
val path = getAppFilePath(SimplexApp.context, uniqueCombine(SimplexApp.context, getAppFilePath(SimplexApp.context, "voice_${timestamp}.m4a")))
|
||||
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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -21,7 +21,7 @@ enum class OnboardingStage {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateProfile(chatModel: ChatModel) {
|
||||
fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
@@ -34,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 {
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -51,8 +51,6 @@ fun PrivacySettingsView(
|
||||
SectionView(stringResource(R.string.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.ImageAspectRatio, stringResource(R.string.transfer_images_faster), chatModel.controller.appPrefs.privacyTransferImagesInline)
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
SectionDivider()
|
||||
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,30 @@
|
||||
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))
|
||||
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
|
||||
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
|
||||
}
|
||||
}
|
||||
953
apps/android/app/src/main/res/values-cs/strings.xml
Normal file
953
apps/android/app/src/main/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,953 @@
|
||||
<?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">Chyba příjmu požadavku od kontaktu</string>
|
||||
<string name="error_changing_address">Chuba změny adresy</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">Poslat</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">Zrušit živou zprávu</string>
|
||||
<string name="back">Zpět</string>
|
||||
<string name="cancel_verb">Zrušit</string>
|
||||
<string name="reset_verb">Obnovit</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">bez podrobností</string>
|
||||
<string name="add_contact">Jednorázový zvací odkaz</string>
|
||||
<string name="copied">Zkopírováno do schránky</string>
|
||||
<string name="add_contact_or_create_group">Začít novou konverzaci</string>
|
||||
<string name="create_group">Vytvořit tajnou skupinu</string>
|
||||
<string name="to_share_with_your_contact">(sdílet s kontaktem)</string>
|
||||
<string name="only_stored_on_members_devices">(uloženo pouze členy skupiny)</string>
|
||||
<string name="toast_permission_denied">Oprávnění zamítnuto!</string>
|
||||
<string name="use_camera_button">Použít fotoaparát</string>
|
||||
<string name="from_gallery_button">Z Galerie</string>
|
||||
<string name="choose_file">Vybrat soubor</string>
|
||||
<string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string>
|
||||
<string name="chat_help_tap_button">Klepněte na tlačítko</string>
|
||||
<string name="above_then_preposition_continuation">nad, potom:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Přidat nový kontakt</b>: vytvořit jednorázový kód QR.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Skenovat QR kód</b>: připojení ke kontaktu, který vám ukáže QR kód.</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scan displayed QR code from the app, via <b>Scan QR code</b>.</string>
|
||||
<string name="clear_chat_question">Vyčistit konverzaci\?</string>
|
||||
<string name="clear_verb">Čistý</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="your_profile_will_be_sent">Váš profil v chatu bude odeslán vašemu kontaktu</string>
|
||||
<string name="create_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="one_time_link">Vytvořit jednorázový zvací odkaz</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">Onion hostitelé nebudou použiti.</string>
|
||||
<string name="network_session_mode_user">Profil chatu</string>
|
||||
<string name="network_session_mode_entity">Připojení</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="create_address">Vytvořit adresu</string>
|
||||
<string name="accept_automatically">Automaticky</string>
|
||||
<string name="section_title_welcome_message">UVÍTACÍ ZPRÁVA</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">Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odesílané pomocí <b>2vrstvého end-to-end šifrování</b>.</string>
|
||||
<string name="onboarding_notifications_mode_title">Soukromá oznámení</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Pravidelný</string>
|
||||
<string name="ignore">Ignorovat</string>
|
||||
<string name="call_already_ended">Hovor již skončil!</string>
|
||||
<string name="icon_descr_video_call">videohovor</string>
|
||||
<string name="icon_descr_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="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">Odstranit přístupovou frázi z úložiště klíčů\?</string>
|
||||
<string name="notifications_will_be_hidden">Oznámení budou doručována pouze do doby, než se aplikace zastaví!</string>
|
||||
<string name="remove_passphrase">Odstranit</string>
|
||||
<string name="update_database">Aktualizovat</string>
|
||||
<string name="current_passphrase">Aktuální přístupová fráze…</string>
|
||||
<string name="update_database_passphrase">Aktualizovat přístupovou frázi databáze</string>
|
||||
<string name="enter_correct_current_passphrase">Zadejte prosím správnou aktuální přístupovou frázi.</string>
|
||||
<string name="database_is_not_encrypted">Váš databáze konverzace není zašifrována - nastavte přístupovou frázi pro její ochranu.</string>
|
||||
<string name="keychain_is_storing_securely">K bezpečnému uložení heslové fráze slouží úložiště klíčů Android - umožňuje fungování služby oznámení.</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Upozornění</b>: pokud přístupovou frázi ztratíte, NEBUDE možné ji obnovit ani změnit.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">Databáze bude 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">Vytvořit frontu</string>
|
||||
<string name="smp_server_test_secure_queue">Zabezpečit frontu</string>
|
||||
<string name="service_notifications">Okamžitá oznámení!</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>V nastavení ji lze vypnout</b> - oznámení se budou zobrazovat pokud aplikace běží.</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">Potvrdit</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">Nová přístupová fráze…</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">Odesílatel možná smazal požadavek připojení</string>
|
||||
<string name="error_smp_test_server_auth">Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo</string>
|
||||
<string name="smp_server_test_delete_queue">Odstranit frontu</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">Pokud se nemůžete setkat osobně, ukažte <b>ve videohovoru QR kód</b> nebo sdílejte odkaz.</string>
|
||||
<string name="scan_code">Skenovat kód</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 Onion hostitelé.</string>
|
||||
<string name="update_network_session_mode_question">Aktualizovat režim izolace\?</string>
|
||||
<string name="app_version_code">Sestavení aplikace: %s</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit. O své kontakty nepřijdete, pokud ji později smažete.</string>
|
||||
<string name="share_link">Sdílet odkaz</string>
|
||||
<string name="delete_address">Odstranit adresu</string>
|
||||
<string name="full_name__field">Celé jméno:</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">Pro zachování soukromí má aplikace místo push oznámení <b><xliff:g id="appName">SimpleX</xliff:g> službu na pozadí</b> - denně spotřebuje několik procent baterie.</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">Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv.</string>
|
||||
<string name="send_live_message_desc">Poslat živou zprávu - zpráva se bude aktualizovat pro příjemce během psaní.</string>
|
||||
<string name="share_one_time_link">Vytvořit jednorázovou pozvánku</string>
|
||||
<string name="scan_QR_code">Skenovat QR kód</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">( skenovat nebo vložit ze schránky)</string>
|
||||
<string name="edit_image">Upravit obrázek</string>
|
||||
<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="send_link_previews">Odesílání náhledů odkazů</string>
|
||||
<string name="full_backup">Zálohování dat aplikace</string>
|
||||
<string name="confirm_new_passphrase">Potvrdit novou heslovou frázi…</string>
|
||||
<string name="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">ty</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">ŽIVĚ</string>
|
||||
<string name="description_via_group_link_incognito">inkognito přes skupinový odkaz</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů SMP jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
|
||||
<string name="failed_to_create_user_title">Chyba při vytváření profilu!</string>
|
||||
<string name="failed_to_create_user_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">Chyba mazání kontaktu</string>
|
||||
<string name="error_deleting_group">Chyba mazání skupiny</string>
|
||||
<string name="error_deleting_contact_request">Chyba mazání žádosti kontaktu</string>
|
||||
<string name="error_deleting_pending_contact_connection">Chyba mazání probíhajícího připojení kontaktu</string>
|
||||
<string name="error_smp_test_failed_at_step">Test selhal v kroku %s.</string>
|
||||
<string name="error_smp_test_certificate">Je možné, že otisk certifikátu v adrese serveru je nesprávný.</string>
|
||||
<string name="smp_server_test_connect">Připojit</string>
|
||||
<string name="smp_server_test_disconnect">Odpojit</string>
|
||||
<string name="error_deleting_user">Chyba mazání uživatelského profilu</string>
|
||||
<string name="icon_descr_instant_notifications">Okamžitá oznámení</string>
|
||||
<string name="service_notifications_disabled">Okamžitá oznámení jsou vypnutá!</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é konverzace</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">Pouze majitelé skupin mohou povolit zasílání hlasových zpráv.</string>
|
||||
<string name="send_live_message">Odeslat živou zprávu</string>
|
||||
<string name="live_message">Živé zprávy!</string>
|
||||
<string name="connect_via_link_or_qr">Připojit se prostřednictvím odkazu / QR kódu</string>
|
||||
<string name="thank_you_for_installing_simplex">Děkujeme za instalaci <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Můžete se <font color="#0088ff">připojit k <xliff:g id="appNameFull">SimpleX Chat</xliff:g> vývojářům a položit jim případné dotazy a získat aktualizace</font>.</string>
|
||||
<string name="to_connect_via_link_title">K připojení prostřednictvím odkazu</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Pokud jste dostal <xliff:g id="appName">SimpleX Chat</xliff:g> zvací odkaz, Můžete ho otevřít v prohlížeči:</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Pokud zvolíte odmítnutí, odesílatel NEBUDE upozorněn.</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobilní telefon: tap <b>Otevřete v mobilní aplikaci</b>, potom klikněte <b>Připojit</b>.</string>
|
||||
<string name="reject_contact_button">Odmítnout</string>
|
||||
<string name="clear_chat_button">Čistá konverzace</string>
|
||||
<string name="clear_chat_menu_action">Čistý</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">Pokud se nemůžete setkat osobně, můžete <b>skenovat QR kód ve videohovoru</b> nebo může váš kontakt sdílet pozvánku.</string>
|
||||
<string name="connect_via_link">Připojte se prostřednictvím odkazu</string>
|
||||
<string name="connect_button">Připojit</string>
|
||||
<string name="paste_button">Vložit</string>
|
||||
<string name="this_string_is_not_a_connection_link">Tento řetězec není odkazem na připojení!</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">Můžete se také připojit kliknutím na odkaz. Pokud se otevře v prohlížeči, klikněte na tlačítko <b>Otevřít v mobilní aplikaci</b>.</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 hostitelé 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">Onion 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 Onion 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">Oddělit TCP připojení (a SOCKS pověření) bude použito <b>pro všechny kontakty a členy skupin</b>. <b>Upozornění</b>: Pokud máte mnoho připojení, může být spotřeba baterie a provoz podstatně vyšší a některá připojení mohou selhat.</string>
|
||||
<string name="appearance_settings">Vzhled</string>
|
||||
<string name="app_version_title">Verze aplikace</string>
|
||||
<string name="app_version_name">Verze aplikace: v%s</string>
|
||||
<string name="core_version">Verze jádra: v%s</string>
|
||||
<string name="core_build_timestamp">Jádro sestaveno na: %s</string>
|
||||
<string name="delete_address__question">Smazat adresu\?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string>
|
||||
<string name="contact_requests">Žádosti o kontakt</string>
|
||||
<string name="display_name__field">Zobrazované jméno:</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">K ochraně soukromí, místo uživatelských ID používaných všemi ostatními platformami, <xliff:g id="appName">SimpleX</xliff:g> má identifikátory pro fronty zpráv, zvlášť pro každý z vašich kontaktů.</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Mnoho lidí se ptalo: <i>if <xliff:g id="appName">SimpleX</xliff:g> nemá žádné identifikátory uživatelů, jak může doručovat zprávy\?</i></string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Vy kontrolujete přez který server(y) <b>přijímat</b> zprávy, vaše kontakty - servery které používáte pro konverzaci.</string>
|
||||
<string name="read_more_in_github">Další informace najdete v našem repozitáři GitHub.</string>
|
||||
<string name="read_more_in_github_with_link">Další informace najdete v našem <font color="#0088ff">úložišti GitHub</font>.</string>
|
||||
<string name="use_chat">Konverzovat</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Lze změnit později v nastavení.</string>
|
||||
<string name="onboarding_notifications_mode_off">Když je aplikace spuštěná</string>
|
||||
<string name="onboarding_notifications_mode_service">Okamžitý</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Nejlepší pro baterii</b>. Budete přijímat oznámení pouze když aplikace běží, služba na pozadí NEBUDE použita.</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><Dobré pro baterii</b>. Služba na pozadí kontroluje nové zprávy každých 10 minut. Můžete zmeškat hovory a naléhavé zprávy.</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">Chyba změny nastavení</string>
|
||||
<string name="save_passphrase_in_keychain">Uložit přístupovou frázi do úložiště klíčů</string>
|
||||
<string name="database_encrypted">Databáze zašifrována!</string>
|
||||
<string name="error_encrypting_database">Chyba šifrování databáze</string>
|
||||
<string name="encrypt_database">Šifrovat</string>
|
||||
<string name="encrypted_with_random_passphrase">Databáze je zašifrována pomocí náhodné přístupové fráze, můžete ji změnit.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">K bezpečnému uložení přístupové fráze se použije úložiště klíčů Android, po restartování aplikace nebo změně přístupové fráze - umožní přijímání oznámení.</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení.</string>
|
||||
<string name="encrypt_database_question">Šifrovat databázi\?</string>
|
||||
<string name="change_database_passphrase_question">Změnit přístupovou frázi databáze\?</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">připojen</string>
|
||||
<string name="group_member_status_complete">kompletní</string>
|
||||
<string name="group_member_status_creator">tvůrce</string>
|
||||
<string name="group_member_status_connecting">připojování</string>
|
||||
<string name="no_contacts_to_add">Žádné kontakty k přidání</string>
|
||||
<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>
|
||||
@@ -49,7 +49,7 @@
|
||||
<string name="simplex_link_mode_browser_warning">Das Öffnen des Links über den Browser kann die Privatsphäre und Sicherheit der Verbindung reduzieren. SimpleX-Links, denen nicht vertraut wird, werden Rot sein.</string>
|
||||
<!-- SimpleXAPI.kt -->
|
||||
<string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string>
|
||||
<string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerk-Konfiguration.</string>
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Verbindungszeitüberschreitung</string>
|
||||
@@ -90,10 +90,10 @@
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Um Ihre Privatsphäre zu schützen kann statt der Push-Benachrichtigung der <b><xliff:g id="appName">SimpleX</xliff:g> Hintergrunddienst genutzt werden</b> – dieser benötigt ein paar Prozent Akkuleistung am Tag.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Diese können über die Einstellungen deaktiviert werden</b> – Solange die App abläuft werden Benachrichtigungen weiterhin angezeigt.</string>
|
||||
<string name="turn_off_battery_optimization">Um diese Funktion zu nutzen, ist es nötig, die Einstellung <b>Akkuoptimierung</b> für <xliff:g id="appName">SimpleX</xliff:g> im nächsten Dialog zu <b>deaktivieren</b>. Ansonsten werden die Benachrichtigungen deaktiviert.</string>
|
||||
<string name="turning_off_service_and_periodic">Die Akkuoptimierung ist aktiv, der Hintergrunddienst und die regelmäßige Nachfrage nach neuen Nachrichten ist abgeschaltet. Sie können diese Funktion in den Einstellungen wieder aktivieren.</string>
|
||||
<string name="periodic_notifications">Regelmäßige Benachrichtigungen</string>
|
||||
<string name="periodic_notifications_disabled">Regelmäßige Benachrichtigungen sind deaktiviert!</string>
|
||||
<string name="periodic_notifications_desc">Die App holt regelmäßig neue Nachrichten ab — dies benötigt ein paar Prozent Akkuleistung am Tag. Die App nutzt keine Push-Benachrichtigungen — es werden keine Daten von Ihrem Gerät an Server gesendet.</string>
|
||||
<string name="turning_off_service_and_periodic">Die Akkuoptimierung ist aktiv, der Hintergrunddienst und die periodische Nachfrage nach neuen Nachrichten ist abgeschaltet. Sie können diese Funktion in den Einstellungen wieder aktivieren.</string>
|
||||
<string name="periodic_notifications">Periodische Benachrichtigungen</string>
|
||||
<string name="periodic_notifications_disabled">Periodische Benachrichtigungen sind deaktiviert!</string>
|
||||
<string name="periodic_notifications_desc">Die App holt periodisch neue Nachrichten ab — dies benötigt ein paar Prozent Akkuleistung am Tag. Die App nutzt keine Push-Benachrichtigungen — es werden keine Daten von Ihrem Gerät an Server gesendet.</string>
|
||||
<string name="enter_passphrase_notification_title">Passwort wird benötigt</string>
|
||||
<string name="enter_passphrase_notification_desc">Geben Sie bitte das Datenbank-Passwort ein, um Benachrichtigungen zu erhalten.</string>
|
||||
<string name="database_initialization_error_title">Die Datenbank kann nicht initialisiert werden</string>
|
||||
@@ -110,7 +110,7 @@
|
||||
<string name="settings_notification_preview_mode_title">Vorschau anzeigen</string>
|
||||
<string name="settings_notification_preview_title">Benachrichtigungsvorschau</string>
|
||||
<string name="notifications_mode_off">Wird ausgeführt, wenn die App geöffnet ist</string>
|
||||
<string name="notifications_mode_periodic">Startet regelmäßig</string>
|
||||
<string name="notifications_mode_periodic">Startet periodisch</string>
|
||||
<string name="notifications_mode_service">Immer aktiv</string>
|
||||
<string name="notifications_mode_off_desc">Die App kann Benachrichtigungen nur empfangen, wenn sie ausgeführt wird, es wird kein Hintergrunddienst gestartet.</string>
|
||||
<string name="notifications_mode_periodic_desc">Überprüft alle 10 Minuten auf neue Nachrichten für bis zu einer Minute.</string>
|
||||
@@ -230,8 +230,8 @@
|
||||
<string name="icon_descr_send_message">Nachricht senden</string>
|
||||
<string name="icon_descr_record_voice_message">Nehme Sprachnachricht auf</string>
|
||||
<string name="allow_voice_messages_question">Sprachnachrichten erlauben?</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, damit Sie sie senden können.</string>
|
||||
<string name="voice_messages_prohibited">Sprachnachrichten unzulässig!</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, damit Sie sie versenden können.</string>
|
||||
<string name="voice_messages_prohibited">Sprachnachrichten nicht erlaubt!</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren.</string>
|
||||
<string name="only_group_owners_can_enable_voice">Sprachnachrichten können nur von Gruppen-Eigentümern aktiviert werden.</string>
|
||||
<!-- General Actions / Responses -->
|
||||
@@ -328,11 +328,12 @@
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Zeigen Sie Ihrem Kontakt den QR-Code aus der App zum Scannen.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs anzeigen</b> oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird\nan Ihren Kontakt gesendet.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird
|
||||
\nan Ihren Kontakt gesendet</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.</string>
|
||||
<string name="share_invitation_link">Einladungslink teilen</string>
|
||||
<string name="paste_connection_link_below_to_connect">Fügen Sie den erhaltenen Link in das Feld unten ein, um sich mit Ihrem Kontakt zu verbinden.</string>
|
||||
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet.</string>
|
||||
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet</string>
|
||||
<!-- PasteToConnect.kt -->
|
||||
<string name="connect_via_link">Über einen Link verbinden</string>
|
||||
<string name="connect_button">Verbinden</string>
|
||||
@@ -361,7 +362,7 @@
|
||||
<string name="smp_servers_add">Füge Server hinzu…</string>
|
||||
<string name="smp_servers_test_server">Teste Server</string>
|
||||
<string name="smp_servers_test_servers">Teste alle Server</string>
|
||||
<string name="smp_servers_save">Sichere alle Server</string>
|
||||
<string name="smp_servers_save">Alle Server speichern</string>
|
||||
<string name="smp_servers_test_failed">Server Test ist fehlgeschlagen!</string>
|
||||
<string name="smp_servers_test_some_failed">Einige Server haben den Test nicht bestanden:</string>
|
||||
<string name="smp_servers_scan_qr">Scannen Sie den QR-Code des Servers</string>
|
||||
@@ -386,10 +387,10 @@
|
||||
<string name="how_to_use_your_servers">Wie Sie Ihre Server nutzen</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Gespeicherte WebRTC ICE-Server werden entfernt.</string>
|
||||
<string name="your_ICE_servers">Ihre ICE-Server</string>
|
||||
<string name="configure_ICE_servers">Konfigurieren Sie ICE-Server</string>
|
||||
<string name="configure_ICE_servers">ICE-Server konfigurieren</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE-Server (einer pro Zeile)</string>
|
||||
<string name="error_saving_ICE_servers">Fehler beim Speichern der ICE-Server</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht kopiert sind.</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string>
|
||||
<string name="save_servers_button">Speichern</string>
|
||||
<string name="network_and_servers">Netzwerk & Server</string>
|
||||
<string name="network_settings">Erweiterte Netzwerkeinstellungen</string>
|
||||
@@ -426,7 +427,7 @@
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Angezeigter Name:</string>
|
||||
<string name="full_name__field">"Vollständiger Name:</string>
|
||||
<string name="your_chat_profile">Mein Chat-Profil</string>
|
||||
<string name="your_current_profile">Mein aktuelles Chat-Profil</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
|
||||
<string name="edit_image">Bild bearbeiten</string>
|
||||
<string name="delete_image">Bild löschen</string>
|
||||
@@ -558,7 +559,6 @@
|
||||
<string name="your_privacy">Meine Privatsphäre</string>
|
||||
<string name="protect_app_screen">App-Bildschirm schützen</string>
|
||||
<string name="auto_accept_images">Bilder automatisch akzeptieren</string>
|
||||
<string name="transfer_images_faster">Bilder schneller übertragen</string>
|
||||
<string name="send_link_previews">Link-Vorschau senden</string>
|
||||
<string name="full_backup">App-Datensicherung</string>
|
||||
<!-- Settings sections -->
|
||||
@@ -578,7 +578,7 @@
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
<string name="settings_section_title_incognito">Inkognito Modus</string>
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Meine Chat-Datenbank</string>
|
||||
<string name="your_chat_database">Chat-Datenbank</string>
|
||||
<string name="run_chat_section">CHAT STARTEN</string>
|
||||
<string name="chat_is_running">Der Chat läuft</string>
|
||||
<string name="chat_is_stopped">Der Chat ist beendet</string>
|
||||
@@ -598,20 +598,19 @@
|
||||
<string name="error_stopping_chat">Fehler beim Beenden des Chats</string>
|
||||
<string name="error_exporting_chat_database">Fehler beim Exportieren der Chat-Datenbank</string>
|
||||
<string name="import_database_question">Chat-Datenbank importieren?</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT.
|
||||
\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="import_database_confirmation">Importieren</string>
|
||||
<string name="error_deleting_database">Fehler beim Löschen der Chat-Datenbank</string>
|
||||
<string name="error_importing_database">Fehler beim Importieren der Chat-Datenbank</string>
|
||||
<string name="chat_database_imported">Chat-Datenbank importiert</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden.</string>
|
||||
<string name="delete_chat_profile_question">Chat-Profil löschen?</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string>
|
||||
<string name="chat_database_deleted">Chat-Datenbank gelöscht</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Starten Sie die App neu, um ein neues Chat-Profil zu erstellen.</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Chat beenden, um Datenbankaktionen zu erlauben.</string>
|
||||
<string name="data_section">DATEN</string>
|
||||
<string name="delete_files_and_media">Dateien \& Medien löschen</string>
|
||||
<string name="delete_files_and_media_question">Dateien und Medien löschen?</string>
|
||||
<string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string>
|
||||
<string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string>
|
||||
@@ -882,28 +881,163 @@
|
||||
<string name="allow_your_contacts_irreversibly_delete">Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Erlauben Sie Ihre Kontakten Sprachnachrichten zu senden.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Erlauben Sie Ihre Kontakten Sprachnachrichten zu versenden.</string>
|
||||
<string name="allow_voice_messages_only_if">Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt.</string>
|
||||
<string name="prohibit_sending_voice_messages">Das Senden von Sprachnachrichten verbieten.</string>
|
||||
<string name="prohibit_sending_voice_messages">Das Senden von Sprachnachrichten nicht erlauben.</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">Sowohl Ihr Kontakt, als auch Sie können Nachrichten unwiederbringlich löschen.</string>
|
||||
<string name="only_you_can_delete_messages">Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren).</string>
|
||||
<string name="only_your_contact_can_delete">Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).</string>
|
||||
<string name="message_deletion_prohibited">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden.</string>
|
||||
<string name="only_you_can_send_voice">Nur Sie können Sprachnachrichten senden.</string>
|
||||
<string name="only_your_contact_can_send_voice">Nur Ihr Kontakt kann Sprachnachrichten senden.</string>
|
||||
<string name="voice_prohibited_in_this_chat">In diesem Chat sind Sprachnachrichten untersagt.</string>
|
||||
<string name="allow_direct_messages">Das Senden von Direktnachrichten an Mitglieder erlauben.</string>
|
||||
<string name="prohibit_direct_messages">Das Senden von Direktnachrichten an Mitglieder verbieten.</string>
|
||||
<string name="allow_to_delete_messages">Unwiederbringliches Löschen von gesendeten Nachrichten erlauben.</string>
|
||||
<string name="prohibit_message_deletion">Unwiederbringliches Löschen von Nachrichten verbieten.</string>
|
||||
<string name="allow_to_send_voice">Senden von Sprachnachrichten erlauben.</string>
|
||||
<string name="prohibit_sending_voice">Senden von Sprachnachrichten untersagen.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten versenden.</string>
|
||||
<string name="only_you_can_send_voice">Nur Sie können Sprachnachrichten versenden.</string>
|
||||
<string name="only_your_contact_can_send_voice">Nur Ihr Kontakt kann Sprachnachrichten versenden.</string>
|
||||
<string name="voice_prohibited_in_this_chat">In diesem Chat sind Sprachnachrichten nicht erlaubt.</string>
|
||||
<string name="allow_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder erlauben.</string>
|
||||
<string name="prohibit_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben.</string>
|
||||
<string name="allow_to_delete_messages">Unwiederbringliches löschen von gesendeten Nachrichten erlauben.</string>
|
||||
<string name="prohibit_message_deletion">Unwiederbringliches löschen von Nachrichten nicht erlauben.</string>
|
||||
<string name="allow_to_send_voice">Das Senden von Sprachnachrichten erlauben.</string>
|
||||
<string name="prohibit_sending_voice">Das Senden von Sprachnachrichten nicht erlauben.</string>
|
||||
<string name="group_members_can_send_dms">Gruppenmitglieder können Direktnachrichten versenden.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht möglich.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">In dieser Gruppe sind Direktnachrichten zwischen Mitgliedern nicht erlaubt.</string>
|
||||
<string name="group_members_can_delete">Gruppenmitglieder können gesendete Nachrichten unwiederbringlich löschen.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten verboten.</string>
|
||||
<string name="group_members_can_send_voice">Gruppenmitglieder können Sprachnachrichten senden.</string>
|
||||
<string name="voice_messages_are_prohibited">In dieser Gruppe sind Sprachnachrichten untersagt.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string>
|
||||
<string name="group_members_can_send_voice">Gruppenmitglieder können Sprachnachrichten versenden.</string>
|
||||
<string name="voice_messages_are_prohibited">In dieser Gruppe sind Sprachnachrichten nicht erlaubt.</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="view_security_code">Schauen Sie sich den Sicherheitscode an</string>
|
||||
<string name="onboarding_notifications_mode_service">Sofort</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>Gute Option für die Batterieausdauer</b>. Der Hintergrundservice überprüft alle 10 Minuten nach neuen Nachrichten. Sie können eventuell Anrufe und dringende Nachrichten verpassen.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>Beste Option für die Batterieausdauer</b>. Sie empfangen Benachrichtigungen nur solange die App abläuft. Der Hintergrundservice wird nicht genutzt!</string>
|
||||
<string name="send_verb">Senden</string>
|
||||
<string name="is_verified">%s wurde erfolgreich überprüft</string>
|
||||
<string name="clear_verification">Überprüfung zurücknehmen</string>
|
||||
<string name="onboarding_notifications_mode_off">Solange die App abläuft</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Kann später über die Einstellungen geändert werden.</string>
|
||||
<string name="delete_after">Löschen nach</string>
|
||||
<string name="ttl_hour">%d Stunde</string>
|
||||
<string name="ttl_hours">%d Stunden</string>
|
||||
<string name="ttl_m">%dm</string>
|
||||
<string name="ttl_min">%d min</string>
|
||||
<string name="ttl_month">%d Monat</string>
|
||||
<string name="ttl_months">%d Monate</string>
|
||||
<string name="ttl_mth">%dmth</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="ttl_sec">%d s</string>
|
||||
<string name="ttl_d">%dd</string>
|
||||
<string name="ttl_day">%d Tag</string>
|
||||
<string name="ttl_days">%d Tage</string>
|
||||
<string name="ttl_w">%dw</string>
|
||||
<string name="ttl_week">%d Woche</string>
|
||||
<string name="ttl_weeks">%d Wochen</string>
|
||||
<string name="timed_messages">Verschwindende Nachrichten</string>
|
||||
<string name="incorrect_code">Falscher Sicherheitscode!</string>
|
||||
<string name="scan_code">Code scannen</string>
|
||||
<string name="mark_code_verified">Als überprüft markieren</string>
|
||||
<string name="scan_code_from_contacts_app">Scannen Sie den Sicherheitscode von der App Ihres Kontakts.</string>
|
||||
<string name="security_code">Sicherheitscode</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periodisch</string>
|
||||
<string name="allow_to_send_disappearing">Erlauben Sie das Senden von verschwindenden Nachrichten.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">In diesem Chat sind verschwindende Nachrichten nicht erlaubt.</string>
|
||||
<string name="only_you_can_send_disappearing">Nur Sie können verschwindende Nachrichten senden.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Nur Ihr Kontakt kann verschwindende Nachrichten senden.</string>
|
||||
<string name="failed_to_parse_chat_title">Fehler beim Laden des Chats</string>
|
||||
<string name="failed_to_parse_chats_title">Fehler beim Laden der Chats</string>
|
||||
<string name="contact_developers">Bitte aktualisieren Sie die App und nehmen Sie Kontakt mit den Entwicklern auf.</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>Benötigt mehr Leistung Ihrer Batterie</b>! Der Hintergrundservice läuft die ganze Zeit ab. Benachrichtigungen werden Ihnen sofort angezeigt, nachdem Sie neue Nachrichten erhalten haben.</string>
|
||||
<string name="create_group_link">Gruppenlink erstellen</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten.</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Das Senden von verschwindenden Nachrichten verbieten.</string>
|
||||
<string name="disappearing_messages_are_prohibited">In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt.</string>
|
||||
<string name="group_members_can_send_disappearing">Gruppenmitglieder können verschwindende Nachrichten senden.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Fügen Sie Server durch Scannen der QR Codes hinzu.</string>
|
||||
<string name="v4_4_disappearing_messages">Verschwindende Nachrichten</string>
|
||||
<string name="accept_feature">Übernehmen</string>
|
||||
<string name="accept_feature_set_1_day">Einen Tag festlegen</string>
|
||||
<string name="invalid_chat">Ungültiger Chat</string>
|
||||
<string name="live_message">Live Nachricht!</string>
|
||||
<string name="send_live_message_desc">Eine Live Nachricht senden - der/die Empfänger sieht/sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</string>
|
||||
<string name="send_live_message">Live Nachricht senden</string>
|
||||
<string name="verify_security_code">Sicherheitscode überprüfen</string>
|
||||
<string name="is_not_verified">%s wurde noch nicht überprüft</string>
|
||||
<string name="to_verify_compare">Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen.</string>
|
||||
<string name="onboarding_notifications_mode_title">Private Benachrichtigungen</string>
|
||||
<string name="use_chat">Chat verwenden</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Ihr Kontakt und Sie können beide verschwindende Nachrichten senden.</string>
|
||||
<string name="ttl_h">%dh</string>
|
||||
<string name="v4_2_group_links">Gruppen-Links</string>
|
||||
<string name="new_in_version">Neu in %s</string>
|
||||
<string name="prohibit_sending_disappearing">Das Senden von verschwindenden Nachrichten verbieten.</string>
|
||||
<string name="v4_2_security_assessment">Sicherheits-Gutachten</string>
|
||||
<string name="v4_2_security_assessment_desc">Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft.</string>
|
||||
<string name="whats_new">Was ist neu</string>
|
||||
<string name="v4_2_group_links_desc">Administratoren können Links für den Beitritt zu Gruppen erzeugen.</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">Kontaktanfragen automatisch annehmen</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Vergleichen Sie die Sicherheitscodes mit Ihren Kontakten.</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">App-Bildschirm in aktuellen Anwendungen verbergen.</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Verbesserte Privatsphäre und Sicherheit</string>
|
||||
<string name="v4_3_improved_server_configuration">Verbesserte Serverkonfiguration</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Unwiederbringliches löschen einer Nachricht</string>
|
||||
<string name="v4_4_live_messages">Live Nachrichten</string>
|
||||
<string name="v4_3_voice_messages_desc">Max. 40 Sekunden, sofort erhalten.</string>
|
||||
<string name="v4_4_live_messages_desc">Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht.</string>
|
||||
<string name="v4_3_voice_messages">Sprachnachrichten</string>
|
||||
<string name="allow_disappearing_messages_only_if">Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</string>
|
||||
<string name="invalid_data">Ungültige Daten</string>
|
||||
<string name="v4_4_verify_connection_security">Sicherheit der Verbindung überprüfen</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Mit optionaler Begrüßungsmeldung.</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Ihre Kontakte können die unwiederbringliche Löschung von Nachrichten erlauben.</string>
|
||||
<string name="icon_descr_cancel_live_message">Livenachricht abbrechen</string>
|
||||
<string name="feature_offered_item">beginne %s</string>
|
||||
<string name="feature_offered_item_with_param">beginne %s: %2s</string>
|
||||
<string name="feature_cancelled_item">beende %s</string>
|
||||
<string name="core_simplexmq_version">simplexmq Version: v%s (%2s)</string>
|
||||
<string name="delete_files_and_media_all">Alle Dateien löschen</string>
|
||||
<string name="messages_section_title">Nachrichten</string>
|
||||
<string name="app_version_code">App Build: %s</string>
|
||||
<string name="app_version_title">App Version</string>
|
||||
<string name="app_version_name">App Version: v%s</string>
|
||||
<string name="core_build_timestamp">Core übersetzt am: %s</string>
|
||||
<string name="core_version">Core Version: v%s</string>
|
||||
<string name="users_add">Profil hinzufügen</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="users_delete_profile_for">Chat-Profil löschen für</string>
|
||||
<string name="network_option_ping_count">PING Zähler</string>
|
||||
<string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string>
|
||||
<string name="smp_servers_per_user">Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil</string>
|
||||
<string name="files_and_media_section">Dateien & Medien</string>
|
||||
<string name="network_session_mode_transport_isolation">Transport-Isolation</string>
|
||||
<string name="users_delete_question">Chat-Profil löschen\?</string>
|
||||
<string name="error_deleting_user">Fehler beim Löschen des Benutzerprofils</string>
|
||||
<string name="your_chat_profiles">Meine Chat-Profile</string>
|
||||
<string name="network_session_mode_entity">Verbindung</string>
|
||||
<string name="network_session_mode_user">Chat-Profil</string>
|
||||
<string name="delete_files_and_media_for_all_users">Dateien für alle Chat-Profile löschen</string>
|
||||
<string name="network_session_mode_entity_description"><b>Für jeden Kontakt und jedes Gruppenmitglied</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.
|
||||
\n
|
||||
\n<b>Bitte beachten Sie</b>: Wenn Sie viele Verbindung haben, kann der Batterieverbrauch und die Datennutzung wesentlich höher sein und einige Verbindungen können scheitern.</string>
|
||||
<string name="network_session_mode_user_description"><b>Für jedes von Ihnen in der App genutzte Chat-Profil</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.</string>
|
||||
<string name="users_delete_data_only">Nur lokale Profildaten</string>
|
||||
<string name="users_delete_with_connections">Profil und Serververbindungen</string>
|
||||
<string name="messages_section_description">Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil</string>
|
||||
<string name="your_chat_profiles_stored_locally">Ihre Chat-Profile werden nur lokal auf Ihrem Endgerät gespeichert</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Doppelter Anzeigename!</string>
|
||||
<string name="failed_to_create_user_title">Fehler beim Erstellen des Profils!</string>
|
||||
<string name="failed_to_active_user_title">Fehler beim Umschalten des Profils!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Sie haben schon ein Chat-Profil mit dem gleichen Anzeigenamen. Bitte wählen Sie einen anderen Namen aus.</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Mehrere Chat-Profile</string>
|
||||
<string name="v4_5_reduced_battery_usage">Reduzierter Batterieverbrauch</string>
|
||||
<string name="v4_5_private_filenames">Neutrale Dateinamen</string>
|
||||
<string name="v4_5_message_draft_descr">Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren.</string>
|
||||
<string name="v4_5_transport_isolation_descr">Per Chat-Profil (Voreinstellung) oder per Verbindung (BETA).</string>
|
||||
<string name="v4_5_italian_interface">Italienische Bedienoberfläche</string>
|
||||
<string name="v4_4_french_interface">Französische Bedienoberfläche</string>
|
||||
<string name="v4_5_message_draft">Nachrichtenentwurf</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Unterschiedliche Namen, Avatare und Transport-Isolation.</string>
|
||||
<string name="v4_5_transport_isolation">Transport-Isolation</string>
|
||||
<string name="v4_5_italian_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
|
||||
<string name="v4_4_french_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
|
||||
<string name="v4_5_private_filenames_descr">Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen.</string>
|
||||
</resources>
|
||||
2
apps/android/app/src/main/res/values-es/strings.xml
Normal file
2
apps/android/app/src/main/res/values-es/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -21,7 +21,7 @@
|
||||
<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">marquer comme 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>
|
||||
@@ -143,7 +143,7 @@
|
||||
<string name="group_preview_join_as">rejoindre en tant que %s</string>
|
||||
<string name="group_preview_you_are_invited">vous êtes invité·e au groupe</string>
|
||||
<string name="chat_with_developers">Discuter avec les développeurs</string>
|
||||
<string name="tap_to_start_new_chat">Appuyez pour commencer un nouveau chat</string>
|
||||
<string name="tap_to_start_new_chat">Appuyez ici pour démarrer une nouvelle discussion</string>
|
||||
<string name="you_have_no_chats">Vous n\'avez aucune discussion</string>
|
||||
<string name="images_limit_title">Trop d’images !</string>
|
||||
<string name="share_file">Partager le fichier…</string>
|
||||
@@ -296,7 +296,7 @@
|
||||
<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 toutes vos questions et pour recevoir des informations sur les mises à jour</font>.</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>
|
||||
@@ -328,14 +328,14 @@
|
||||
<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> des messages de vos contacts.</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 le code QR du serveur</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>
|
||||
@@ -359,8 +359,8 @@
|
||||
<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 les supprimez par la suite.</string>
|
||||
<string name="your_chat_profile">Votre profil de chat</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>
|
||||
@@ -380,8 +380,8 @@
|
||||
<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 – tout le monde peut faire fonctionner les serveurs.</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Pour protéger la vie privée, au lieu d\'ID d\'utilisateur utilisés par toutes les autres plateformes, <xliff:g id="appName">SimpleX</xliff:g> possède des identifiants pour les files d\'attente de messages, distincts pour chacun de vos contacts.</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>
|
||||
@@ -461,13 +461,13 @@
|
||||
<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">Les gens peuvent se connecter à vous uniquement via les liens que vous partagez.</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 demande : <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="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>
|
||||
@@ -519,7 +519,6 @@
|
||||
<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="data_section">DONNÉES</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>
|
||||
@@ -624,7 +623,6 @@
|
||||
<string name="privacy_and_security">Vie privée et sécurité</string>
|
||||
<string name="protect_app_screen">Protéger l\'écran de l\'app</string>
|
||||
<string name="auto_accept_images">Images auto-acceptées</string>
|
||||
<string name="transfer_images_faster">Transfert d\'images plus rapide</string>
|
||||
<string name="full_backup">Sauvegarde des données de l\'app</string>
|
||||
<string name="settings_section_title_you">VOUS</string>
|
||||
<string name="settings_section_title_help">AIDE</string>
|
||||
@@ -643,7 +641,6 @@
|
||||
<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">"Supprimer les fichiers 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>
|
||||
@@ -778,7 +775,7 @@
|
||||
<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 pour suppression).</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>
|
||||
@@ -786,7 +783,7 @@
|
||||
<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">Envoyé via</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>
|
||||
@@ -862,9 +859,9 @@
|
||||
<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 pour suppression).</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 peuvent 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>
|
||||
@@ -879,9 +876,9 @@
|
||||
<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 pouvez tous deux supprimer de manière irréversible les messages envoyés.</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 pouvez tous deux supprimer de manière irréversible les messages envoyés.</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>
|
||||
@@ -892,4 +889,81 @@
|
||||
<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>
|
||||
<string name="v4_4_french_interface">Interface en français</string>
|
||||
<string name="v4_5_transport_isolation_descr">Par profil de chat (par défaut) ou par connexion (BETA).</string>
|
||||
<string name="v4_5_italian_interface">Interface en italien</string>
|
||||
<string name="v4_5_message_draft">Brouillon de message</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Plus d\'améliorations à venir !</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Différents profils de chat</string>
|
||||
<string name="v4_5_message_draft_descr">Conserver le brouillon du dernier message, avec les pièces jointes.</string>
|
||||
<string name="v4_5_reduced_battery_usage">Réduction de la consommation de batterie</string>
|
||||
<string name="v4_5_private_filenames">Noms de fichiers privés</string>
|
||||
<string name="v4_5_italian_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
<string name="v4_4_french_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
<string name="v4_5_private_filenames_descr">Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC.</string>
|
||||
<string name="v4_5_transport_isolation">Isolation du transport</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">Différents noms, avatars et mode d\'isolation de transport.</string>
|
||||
</resources>
|
||||
147
apps/android/app/src/main/res/values-hi/strings.xml
Normal file
147
apps/android/app/src/main/res/values-hi/strings.xml
Normal file
@@ -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>
|
||||
2
apps/android/app/src/main/res/values-hr/strings.xml
Normal file
2
apps/android/app/src/main/res/values-hr/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
969
apps/android/app/src/main/res/values-it/strings.xml
Normal file
969
apps/android/app/src/main/res/values-it/strings.xml
Normal file
@@ -0,0 +1,969 @@
|
||||
<?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="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>
|
||||
86
apps/android/app/src/main/res/values-ja/strings.xml
Normal file
86
apps/android/app/src/main/res/values-ja/strings.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="chat_item_ttl_day">1日</string>
|
||||
<string name="chat_item_ttl_week">1週間</string>
|
||||
<string name="callstatus_accepted">受けた通話</string>
|
||||
<string name="smp_servers_preset_add">既存サーバを追加</string>
|
||||
<string name="group_member_role_admin">管理者</string>
|
||||
<string name="v4_2_group_links_desc">管理者はグループの参加リンクを発行できます。</string>
|
||||
<string name="network_settings">ネットワーク詳細設定</string>
|
||||
<string name="chat_item_ttl_month">1ヶ月</string>
|
||||
<string name="about_simplex">SimpleXについて</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>について</string>
|
||||
<string name="color_primary">アクセント色</string>
|
||||
<string name="accept_contact_button">承諾</string>
|
||||
<string name="accept_connection_request__question">繋がりを承諾しますか?</string>
|
||||
<string name="accept">承諾</string>
|
||||
<string name="accept_feature">承諾</string>
|
||||
<string name="accept_call_on_lock_screen">承諾</string>
|
||||
<string name="accept_contact_incognito_button">シークレットモードで承諾</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">QRコードでサーバを追加</string>
|
||||
<string name="smp_servers_add_to_another_device">別の端末に追加</string>
|
||||
<string name="users_add">プロフィールを追加</string>
|
||||
<string name="smp_servers_add">サーバを追加…</string>
|
||||
<string name="network_enable_socks_info">SOCKSプロキシ(ポート9050)経由で接続しますか?(※設定する前にプロキシ起動が必要※)</string>
|
||||
<string name="users_delete_all_chats_deleted">全チャットとメッセージが削除されます(※元に戻せません※)!</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">送信相手からの音声メッセージを許可する。</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">あなたと連絡相手が音声メッセージを送信できます。</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>電池省エネに良い</b>:バックグラウンド機能で10分毎に新着メッセージを確認します。通話と緊急メッセージを見逃す可能性があります。</string>
|
||||
<string name="icon_descr_audio_off">音声オフ</string>
|
||||
<string name="attach">添付する</string>
|
||||
<string name="app_version_code">アプリ・ビルド番号: %s</string>
|
||||
<string name="all_your_contacts_will_remain_connected">あなたの連絡先が繋がったまま継続します。</string>
|
||||
<string name="accept_requests">リクエストを承諾</string>
|
||||
<string name="accept_automatically">自動的に</string>
|
||||
<string name="icon_descr_audio_on">音声オン</string>
|
||||
<string name="integrity_msg_bad_hash">メッセージのハッシュ値問題</string>
|
||||
<string name="integrity_msg_bad_id">メッセージIDの問題</string>
|
||||
<string name="allow_verb">許可</string>
|
||||
<string name="allow_voice_messages_question">音声メッセージを許可しますか?</string>
|
||||
<string name="back">戻る</string>
|
||||
<string name="appearance_settings">見た目</string>
|
||||
<string name="app_version_title">アプリのバージョン</string>
|
||||
<string name="app_version_name">アプリのバージョン: v%s</string>
|
||||
<string name="network_session_mode_user_description"><b>アプリ内の各チャットプロフィールに、</b>.連絡先毎にそれぞれのTCP接続(とSOCKS資格情報)が使われます。</string>
|
||||
<string name="network_session_mode_entity_description"><b>各連絡先とグループに、</b>それぞれのTCP接続(とSOCKS資格情報)が使われます。
|
||||
\n<b>※注意※</b> 接続が多かったら、電池とデータの使用量が増えて、切断する可能性もあります。</string>
|
||||
<string name="bold">太文字</string>
|
||||
<string name="icon_descr_audio_call">音声通話</string>
|
||||
<string name="settings_audio_video_calls">音声とビデオ通話</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>※注意※</b>:喪失したら、パスフレーズの回復・変更ができません。</string>
|
||||
<string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。</string>
|
||||
<string name="allow_voice_messages_only_if">送信相手も音声メッセージを許可する時のみに許可する。</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">あなたと連絡相手が消えるメッセージを送信できます。</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">連絡先を自動的に承諾</string>
|
||||
<string name="chat_preferences_always">常に</string>
|
||||
<string name="notifications_mode_service">常にオン</string>
|
||||
<string name="clear_chat_warning">全てのメッセージが削除されます(※注意:元に戻せません!※)。削除されるのは片方あなたのメッセージのみ</string>
|
||||
<string name="allow_disappearing_messages_only_if">送信相手も消えるメッセージ機能を許可する時のみに許可する。</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">送信相手も永久メッセージ削除を許可する時のみに許可する。</string>
|
||||
<string name="allow_to_delete_messages">送信済みメッセージの永久削除を許可</string>
|
||||
<string name="allow_to_send_disappearing">消えるメッセージの送信を許可</string>
|
||||
<string name="allow_direct_messages">メンバーへのダイレクトメッセージを許可</string>
|
||||
<string name="allow_to_send_voice">音声メッセージの送信を許可</string>
|
||||
<string name="notifications_mode_off_desc">アクティブの時のみに通知が出ます。バックグラウンド通知サービスは起動されません。</string>
|
||||
<string name="keychain_is_storing_securely">Androidキーストアはパスフレーズの保管に使われます。通知機能に必要です。</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">再起動時とパスフレーズ変更時にAndroidキーストアがパスフレーズの保管に使われます。通知機能に必要です。</string>
|
||||
<string name="answer_call">通話に応答</string>
|
||||
<string name="settings_section_title_icon">アプリのアイコン</string>
|
||||
<string name="full_backup">アプリデータのバックアップ</string>
|
||||
<string name="incognito_random_profile_from_contact_description">このリンクの送信元にランダムなプロフィール(ダミー)が送られます。</string>
|
||||
<string name="incognito_random_profile_description">連絡先にランダムなプロフィール(ダミー)が送られます。</string>
|
||||
<string name="audio_call_no_encryption">音声通話 (エンドツーエンド暗号化なし)</string>
|
||||
<string name="icon_descr_asked_to_receive">画像受信を依頼しました。</string>
|
||||
<string name="auth_unavailable">認証不可能</string>
|
||||
<string name="auto_accept_images">画像を自動的に受信</string>
|
||||
<string name="notifications_mode_service_desc">バックグラウンド機能が常にオンで、メッセージが到着次第に通知が出ます。</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>新しい連絡先を追加</b>:使い捨てのQRコードを発行</string>
|
||||
<string name="turning_off_service_and_periodic">電池省エネをオンに、バックグラウンド機能と定期的な受信依頼をオフにします。設定メニューにて変更できます。</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>電池消費が最少</b>:アプリがアクティブ時のみに通知が出ます。</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>設定メニューにてオフにできます。</b> アプリがアクティブ時に通知が出ます。</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">あなたと連絡相手が送信済みメッセージを永久削除できます。</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QRコードを読み込み</b>:連絡相手のQRコードをスキャンすると繋がります。</string>
|
||||
</resources>
|
||||
617
apps/android/app/src/main/res/values-nl/strings.xml
Normal file
617
apps/android/app/src/main/res/values-nl/strings.xml
Normal file
@@ -0,0 +1,617 @@
|
||||
<?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>
|
||||
<string name="invalid_connection_link">Ongeldige verbindingslink</string>
|
||||
<string name="service_notifications_disabled">Onmiddelijke meldingen zijn uitgeschakeld!</string>
|
||||
<string name="service_notifications">Onmiddellijke berichten!</string>
|
||||
<string name="notification_preview_new_message">nieuw bericht</string>
|
||||
<string name="icon_descr_image_snd_complete">Afbeelding verzonden</string>
|
||||
<string name="live_message">Live bericht!</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Als je een uitnodigingslink voor <xliff:g id="appName">SimpleX Chat</xliff:g> hebt ontvangen, kun je deze in je browser openen:</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">Dit kan later worden gewijzigd via instellingen.</string>
|
||||
<string name="join_group_question">Deelnemen aan groep\?</string>
|
||||
<string name="icon_descr_add_members">Nodig leden uit</string>
|
||||
<string name="no_contacts_selected">Geen contacten geselecteerd</string>
|
||||
<string name="v4_4_live_messages">Live berichten</string>
|
||||
<string name="icon_descr_instant_notifications">Onmiddellijke meldingen</string>
|
||||
<string name="notification_new_contact_request">Nieuw contactverzoek</string>
|
||||
<string name="auth_log_in_using_credential">Log in met uw inloggegevens</string>
|
||||
<string name="message_delivery_error_desc">Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd.</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Bericht wordt verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="large_file">Groot bestand!</string>
|
||||
<string name="mark_read">Markeer gelezen</string>
|
||||
<string name="mark_unread">Markeer als ongelezen</string>
|
||||
<string name="mute_chat">Stom</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Als u elkaar niet persoonlijk kunt ontmoeten, kunt u <b> de QR-code scannen in het videogesprek </b>, of uw contactpersoon kan een uitnodigingslink delen.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Als je elkaar niet persoonlijk kunt ontmoeten, <b>toon je de QR-code in het videogesprek</b> of deel je de link.</string>
|
||||
<string name="invalid_QR_code">Ongeldige QR-code</string>
|
||||
<string name="icon_descr_more_button">Meer</string>
|
||||
<string name="incorrect_code">Onjuiste beveiligingscode!</string>
|
||||
<string name="mark_code_verified">Markeer geverifieerd</string>
|
||||
<string name="how_to_use_simplex_chat">Hoe te gebruiken</string>
|
||||
<string name="markdown_help">Markdown-hulp</string>
|
||||
<string name="markdown_in_messages">Markdown in berichten</string>
|
||||
<string name="network_settings_title">Netwerkinstellingen</string>
|
||||
<string name="how_to_use_markdown">Markdown gebruiken</string>
|
||||
<string name="italic">cursief</string>
|
||||
<string name="how_it_works">Hoe het werkt</string>
|
||||
<string name="callstatus_missed">gemiste oproep</string>
|
||||
<string name="how_simplex_works">Hoe <xliff:g id="appName">SimpleX</xliff:g> werkt</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">Veel mensen vroegen: <i>als <xliff:g id="appName">SimpleX</xliff:g> geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?</i></string>
|
||||
<string name="incoming_audio_call">Inkomende audio-oproep</string>
|
||||
<string name="incoming_video_call">Inkomend videogesprek</string>
|
||||
<string name="ignore">Negeren</string>
|
||||
<string name="status_no_e2e_encryption">geen e2e-encryptie</string>
|
||||
<string name="import_database_question">Chatdatabase importeren\?</string>
|
||||
<string name="chat_item_ttl_none">nooit</string>
|
||||
<string name="no_received_app_files">Geen ontvangen of verzonden bestanden</string>
|
||||
<string name="alert_title_group_invitation_expired">Uitnodiging verlopen!</string>
|
||||
<string name="rcv_group_event_member_left">Verlaten</string>
|
||||
<string name="group_member_status_left">Verlaten</string>
|
||||
<string name="group_member_status_invited">uitgenodigd</string>
|
||||
<string name="button_leave_group">Groep verlaten</string>
|
||||
<string name="info_row_local_name">Lokale naam</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Incognitomodus wordt hier niet ondersteund - uw hoofdprofiel wordt naar groepsleden verzonden</string>
|
||||
<string name="users_delete_data_only">Alleen lokale profielgegevens</string>
|
||||
<string name="message_deletion_prohibited_in_chat">Het onomkeerbaar verwijderen van berichten is verboden in deze groep.</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">App-scherm verbergen in de recente apps.</string>
|
||||
<string name="settings_section_title_incognito">Incognito modus</string>
|
||||
<string name="messages_section_title">Berichten</string>
|
||||
<string name="new_passphrase">Nieuwe wachtwoordzin…</string>
|
||||
<string name="keychain_error">Keychain fout</string>
|
||||
<string name="join_group_button">Word lid van</string>
|
||||
<string name="leave_group_question">Groep verlaten\?</string>
|
||||
<string name="new_member_role">Nieuwe ledenrol</string>
|
||||
<string name="no_contacts_to_add">Geen contacten om toe te voegen</string>
|
||||
<string name="incognito_info_allows">Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chatprofiel.</string>
|
||||
<string name="theme_light">Licht</string>
|
||||
<string name="chat_preferences_no">Nee</string>
|
||||
<string name="v4_5_multiple_chat_profiles">Meerdere chatprofielen</string>
|
||||
<string name="v4_5_italian_interface">Italiaanse interface</string>
|
||||
<string name="v4_5_message_draft">Concept bericht</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string>
|
||||
<string name="button_add_members">Nodig leden uit</string>
|
||||
<string name="notification_display_mode_hidden_desc">Verberg contact en bericht</string>
|
||||
<string name="turn_off_battery_optimization">Om het te gebruiken <b xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">batterijoptimalisatie uitschakelen</b> voor <xliff:g xmlns:xliff=\"urn:oasis: names:tc:xliff:document:1.2\" id=\"appName\">SimpleX</xliff:g> in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld.</string>
|
||||
<string name="onboarding_notifications_mode_service">Onmiddellijk</string>
|
||||
<string name="rcv_group_event_member_added">uitgenodigd <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="group_invitation_item_description">uitnodiging voor groep <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="invite_to_group_button">Uitnodigen voor groep</string>
|
||||
<string name="join_group_incognito_button">Doe incognito mee</string>
|
||||
<string name="group_preview_join_as">lid worden als %s</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Het kan gebeuren wanneer:
|
||||
\n1. De berichten op de server verlopen als ze 30 dagen niet zijn ontvangen,
|
||||
\n2. De server die u gebruikt om de berichten van deze contactpersoon te ontvangen, is bijgewerkt en opnieuw opgestart.
|
||||
\n3. De verbinding is verbroken.
|
||||
\nMaak verbinding met de ontwikkelaars via Instellingen om de updates over de servers te ontvangen.
|
||||
\nWe zullen serverredundantie toevoegen om verloren berichten te voorkomen.</string>
|
||||
<string name="joining_group">Deel nemen aan groep</string>
|
||||
<string name="leave_group_button">Verlaten</string>
|
||||
<string name="group_member_role_member">lid</string>
|
||||
<string name="image_descr_link_preview">link voorbeeldafbeelding</string>
|
||||
<string name="member_info_section_title_member">LID</string>
|
||||
<string name="settings_section_title_messages">BERICHTEN</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobiel: tik op <b>Openen in mobiele app</b> en tik vervolgens op <b>Verbinden</b> in de app.</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Lid wordt uit de groep verwijderd - dit kan niet ongedaan worden gemaakt!</string>
|
||||
<string name="message_delivery_error_title">Fout bij bezorging van bericht</string>
|
||||
<string name="delete_message_mark_deleted_warning">Bericht wordt gemarkeerd voor verwijdering. De ontvanger(s) kunnen dit bericht onthullen.</string>
|
||||
<string name="network_status">Netwerk status</string>
|
||||
<string name="image_descr">Afbeelding</string>
|
||||
<string name="image_saved">Afbeelding opgeslagen in Galerij</string>
|
||||
<string name="import_database_confirmation">Importeren</string>
|
||||
<string name="import_database">Database importeren</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Verbeterde privacy en veiligheid</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">De afbeelding wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later!</string>
|
||||
<string name="incognito_info_protects">De incognitomodus beschermt de privacy van uw hoofdprofielnaam en -afbeelding - voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt.</string>
|
||||
<string name="new_database_archive">Nieuw database-archief</string>
|
||||
<string name="no_details">geen details</string>
|
||||
<string name="conn_level_desc_indirect">indirect (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
<string name="notification_preview_mode_message">Bericht tekst</string>
|
||||
<string name="settings_notification_preview_title">Meldingsvoorbeeld</string>
|
||||
<string name="settings_notifications_mode_title">Meldingsservice</string>
|
||||
<string name="notifications">Meldingen</string>
|
||||
<string name="invalid_contact_link">Ongeldige link!</string>
|
||||
<string name="smp_servers_invalid_address">Ongeldig serveradres!</string>
|
||||
<string name="install_simplex_chat_for_terminal">Installeer <xliff:g id="appNameFull">SimpleX Chat</xliff:g> voor terminal</string>
|
||||
<string name="how_to">Hoe</string>
|
||||
<string name="how_to_use_your_servers">Hoe u uw servers gebruikt</string>
|
||||
<string name="network_and_servers">Netwerk & servers</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE-servers (één per lijn)</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Zorg ervoor dat WebRTC ICE-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="network_disable_socks_info">Als u bevestigt, kunnen de berichtenservers uw IP-adres zien en uw provider - met welke servers u verbinding maakt.</string>
|
||||
<string name="network_use_onion_hosts_no">Nee</string>
|
||||
<string name="immune_to_spam_and_abuse">Immuun voor spam en misbruik</string>
|
||||
<string name="make_private_connection">Maak een privéverbinding</string>
|
||||
<string name="message_deletion_prohibited">Het onomkeerbaar verwijderen van berichten is verboden in deze chat.</string>
|
||||
<string name="new_in_version">Nieuw in %s</string>
|
||||
<string name="v4_3_voice_messages_desc">Max 40 seconden, direct ontvangen.</string>
|
||||
<string name="v4_3_improved_server_configuration">Verbeterde serverconfiguratie</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Onomkeerbare berichtverwijdering</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">uitgenodigd via je groepslink</string>
|
||||
<string name="incognito">Incognito</string>
|
||||
<string name="icon_descr_call_missed">Gemiste oproep</string>
|
||||
<string name="description_via_contact_address_link_incognito">incognito via link naar contactadres</string>
|
||||
<string name="description_via_group_link_incognito">incognito via groepslink</string>
|
||||
<string name="description_via_one_time_link_incognito">incognito via eenmalige link</string>
|
||||
<string name="invalid_chat">ongeldige chat</string>
|
||||
<string name="invalid_data">onjuiste data</string>
|
||||
<string name="invalid_message_format">ongeldig berichtformaat</string>
|
||||
<string name="display_name_invited_to_connect">uitgenodigd om te verbinden</string>
|
||||
<string name="live">LIVE</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Zorg ervoor dat SMP-serveradressen de juiste indeling hebben, regelgescheiden zijn en niet gedupliceerd zijn.</string>
|
||||
<string name="marked_deleted_description">gemarkeerd als verwijderd</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</string>
|
||||
<string name="image_descr_profile_image">profielfoto</string>
|
||||
<string name="privacy_redefined">Privacy opnieuw gedefinieerd</string>
|
||||
<string name="privacy_and_security">Privacy en beveiliging</string>
|
||||
<string name="network_error_desc">Controleer uw netwerkverbinding met <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> en probeer het opnieuw.</string>
|
||||
<string name="error_smp_test_certificate">Mogelijk is de certificaatvingerafdruk in het serveradres onjuist</string>
|
||||
<string name="periodic_notifications">Periodieke meldingen</string>
|
||||
<string name="auth_open_chat_console">Chatconsole openen</string>
|
||||
<string name="toast_permission_denied">Geen toestemming!</string>
|
||||
<string name="icon_descr_profile_image_placeholder">profielafbeelding tijdelijke aanduiding</string>
|
||||
<string name="one_time_link">Eenmalige uitnodigingslink</string>
|
||||
<string name="paste_button">Plakken</string>
|
||||
<string name="smp_servers_preset_address">Vooraf ingesteld serveradres</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts worden niet gebruikt.</string>
|
||||
<string name="onboarding_notifications_mode_title">Privé meldingen</string>
|
||||
<string name="paste_the_link_you_received">Plak de ontvangen link</string>
|
||||
<string name="onboarding_notifications_mode_periodic">Periodiek</string>
|
||||
<string name="open_verb">Open</string>
|
||||
<string name="open_simplex_chat_to_accept_call">Open <xliff:g id="appNameFull">SimpleX Chat</xliff:g> om de oproep te accepteren</string>
|
||||
<string name="call_connection_peer_to_peer">peer-to-peer</string>
|
||||
<string name="notifications_will_be_hidden">Meldingen worden alleen afgeleverd totdat de app stopt!</string>
|
||||
<string name="restore_passphrase_not_found_desc">Wachtwoordzin niet gevonden in Keystore, voer deze handmatig in. Dit kan zijn gebeurd als u de gegevens van de app hebt hersteld met een back-uptool. Als dit niet het geval is, neem dan contact op met de ontwikkelaars.</string>
|
||||
<string name="users_delete_with_connections">Profiel- en serververbindingen</string>
|
||||
<string name="chat_preferences_off">uit</string>
|
||||
<string name="chat_preferences_on">aan</string>
|
||||
<string name="only_you_can_send_disappearing">Alleen jij kunt verdwijnende berichten verzenden.</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Alleen uw contactpersoon kan verdwijnende berichten verzenden.</string>
|
||||
<string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</string>
|
||||
<string name="feature_offered_item_with_param">aangeboden %s: %2s</string>
|
||||
<string name="old_database_archive">Oud database-archief</string>
|
||||
<string name="enter_correct_current_passphrase">Voer de juiste huidige wachtwoordzin in.</string>
|
||||
<string name="group_member_role_owner">eigenaar</string>
|
||||
<string name="network_option_ping_count">PING-telling</string>
|
||||
<string name="network_option_ping_interval">PING-interval</string>
|
||||
<string name="v4_5_message_draft_descr">Bewaar het laatste berichtconcept, met bijlagen.</string>
|
||||
<string name="v4_5_private_filenames">Privé bestandsnamen</string>
|
||||
<string name="images_limit_desc">Er kunnen slechts 10 afbeeldingen tegelijk worden verzonden</string>
|
||||
<string name="enter_passphrase_notification_title">Wachtwoordzin is nodig</string>
|
||||
<string name="feature_off">uit</string>
|
||||
<string name="add_contact">Eenmalige uitnodigingslink</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts zijn vereist voor verbinding.</string>
|
||||
<string name="only_group_owners_can_change_prefs">Alleen groepseigenaren kunnen groepsvoorkeuren wijzigen.</string>
|
||||
<string name="only_stored_on_members_devices">(alleen opgeslagen door groepsleden)</string>
|
||||
<string name="paste_connection_link_below_to_connect">Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contactpersoon.</string>
|
||||
<string name="smp_servers_preset_server">Vooraf ingestelde server</string>
|
||||
<string name="periodic_notifications_disabled">Periodieke meldingen zijn uitgeschakeld!</string>
|
||||
<string name="icon_descr_server_status_pending">In behandeling</string>
|
||||
<string name="only_group_owners_can_enable_voice">Alleen groepseigenaren kunnen spraakberichten inschakelen.</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Vraag uw contactpersoon om het verzenden van spraakberichten in te schakelen.</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="network_use_onion_hosts_required_desc">Onion hosts zijn vereist voor verbinding.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts worden gebruikt indien beschikbaar.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code – iedereen kan de servers draaien.</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string>
|
||||
<string name="only_your_contact_can_delete">Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string>
|
||||
<string name="only_you_can_send_voice">Alleen jij kunt spraakberichten verzenden.</string>
|
||||
<string name="only_your_contact_can_send_voice">Alleen uw contactpersoon kan spraakberichten verzenden.</string>
|
||||
<string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string>
|
||||
<string name="feature_offered_item">aangeboden %s</string>
|
||||
<string name="store_passphrase_securely_without_recover">Sla de wachtwoordzin veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de chat.</string>
|
||||
<string name="store_passphrase_securely">Bewaar de wachtwoordzin veilig, u kunt deze NIET wijzigen als u deze kwijtraakt.</string>
|
||||
<string name="open_chat">Chat openen</string>
|
||||
<string name="restore_database_alert_desc">Voer het vorige wachtwoord in na het herstellen van de databaseback-up. Deze actie kan niet ongedaan gemaakt worden.</string>
|
||||
<string name="icon_descr_call_pending_sent">Oproep in behandeling</string>
|
||||
<string name="simplex_link_mode_browser_warning">Het openen van de link in de browser kan de privacy en beveiliging van de verbinding verminderen. Niet-vertrouwde SimpleX-links worden rood weergegeven.</string>
|
||||
<string name="contact_developers">Werk de app bij en neem contact op met de ontwikkelaars.</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Alleen client-apparaten slaan gebruikersprofielen, contacten, groepen en berichten op die zijn verzonden met <b>2-laags end-to-end-codering</b>.</string>
|
||||
</resources>
|
||||
2
apps/android/app/src/main/res/values-pl/strings.xml
Normal file
2
apps/android/app/src/main/res/values-pl/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
2
apps/android/app/src/main/res/values-pt-rBR/strings.xml
Normal file
2
apps/android/app/src/main/res/values-pt-rBR/strings.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -423,7 +423,7 @@
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Имя профиля:</string>
|
||||
<string name="full_name__field">"Полное имя:</string>
|
||||
<string name="your_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>
|
||||
@@ -560,7 +560,6 @@
|
||||
<string name="your_privacy">Конфиденциальность</string>
|
||||
<string name="protect_app_screen">Защитить экран приложения</string>
|
||||
<string name="auto_accept_images">Автоприем изображений</string>
|
||||
<string name="transfer_images_faster">Передавать изображения быстрее</string>
|
||||
<string name="send_link_previews">Отправлять картинки ссылок</string>
|
||||
<string name="full_backup">Резервная копия данных</string>
|
||||
<!-- Settings sections -->
|
||||
@@ -612,8 +611,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>
|
||||
@@ -964,4 +963,79 @@
|
||||
<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>
|
||||
259
apps/android/app/src/main/res/values-zh-rCN/strings.xml
Normal file
259
apps/android/app/src/main/res/values-zh-rCN/strings.xml
Normal file
@@ -0,0 +1,259 @@
|
||||
<?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>
|
||||
<string name="confirm_new_passphrase">确认新口令…</string>
|
||||
<string name="group_member_status_complete">完整的</string>
|
||||
<string name="group_member_status_connected">连接的</string>
|
||||
<string name="chat_preferences">聊天偏好</string>
|
||||
<string name="settings_section_title_chats">聊天</string>
|
||||
<string name="clear_verb">清空</string>
|
||||
<string name="clear_chat_menu_action">清空</string>
|
||||
<string name="clear_chat_button">清除聊天记录</string>
|
||||
<string name="clear_chat_question">清除聊天记录?</string>
|
||||
<string name="chat_with_developers">与开发者聊天</string>
|
||||
<string name="clear_contacts_selection_button">清除</string>
|
||||
<string name="colored">有色</string>
|
||||
<string name="callstate_connected">连接的</string>
|
||||
<string name="connect_button">连接</string>
|
||||
<string name="connect_via_link_verb">连接</string>
|
||||
<string name="choose_file">选择文件</string>
|
||||
<string name="network_session_mode_user">聊天资料</string>
|
||||
<string name="v4_5_transport_isolation_descr">按聊天资料(默认)或按连接(BETA)。</string>
|
||||
<string name="smp_servers_check_address">检查服务器地址并再试一次。</string>
|
||||
<string name="clear_verification">清晰的验证</string>
|
||||
<string name="icon_descr_close_button">关闭键</string>
|
||||
<string name="configure_ICE_servers">配置ICE服务器</string>
|
||||
<string name="confirm_verb">确认</string>
|
||||
<string name="auth_confirm_credential">确认您的证书</string>
|
||||
<string name="server_connected">连接的</string>
|
||||
<string name="icon_descr_server_status_connected">连接的</string>
|
||||
<string name="smp_server_test_connect">连接</string>
|
||||
<string name="notification_contact_connected">连接的</string>
|
||||
<string name="server_connecting">连接的</string>
|
||||
<string name="group_member_status_connecting">连接的</string>
|
||||
<string name="notifications_mode_periodic_desc">每10分钟检查一次新消息,最长1分钟</string>
|
||||
<string name="rcv_group_event_member_connected">连接的</string>
|
||||
<string name="v4_4_verify_connection_security_desc">与你的联系人比较安全码</string>
|
||||
</resources>
|
||||
21
apps/android/app/src/main/res/values-zh-rTW/strings.xml
Normal file
21
apps/android/app/src/main/res/values-zh-rTW/strings.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="chat_item_ttl_week">1 個星期</string>
|
||||
<string name="accept_requests">接受請求</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="about_simplex">關於 SimpleX</string>
|
||||
<string name="accept_call_on_lock_screen">接受</string>
|
||||
<string name="accept_feature">接受</string>
|
||||
<string name="chat_item_ttl_day">1 天</string>
|
||||
<string name="chat_item_ttl_month">1 個月</string>
|
||||
<string name="accept_contact_button">接受</string>
|
||||
<string name="about_simplex_chat">關於<xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="accept_connection_request__question">接受新連線要求\?</string>
|
||||
<string name="callstatus_accepted">已接受電話</string>
|
||||
<string name="network_enable_socks_info">要在端口 9050 啟動 SOCKS 代理伺服器嗎\? 在啟用這個選項之前,必須先啟動代理伺服器。</string>
|
||||
<string name="group_member_role_admin">管理員</string>
|
||||
<string name="above_then_preposition_continuation">以上,然後:</string>
|
||||
<string name="smp_servers_preset_add">加入預設伺服器</string>
|
||||
<string name="smp_servers_add">新增伺服器…</string>
|
||||
<string name="accept">接受</string>
|
||||
</resources>
|
||||
@@ -28,6 +28,8 @@
|
||||
<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>
|
||||
@@ -61,6 +63,10 @@
|
||||
<string name="failed_to_parse_chat_title">Failed to load chat</string>
|
||||
<string name="failed_to_parse_chats_title">Failed to load chats</string>
|
||||
<string name="contact_developers">Please update the app and contact developers.</string>
|
||||
<string name="failed_to_create_user_title">Error creating profile!</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Duplicate display name!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">You already have a chat profile with the same display name. Please choose another name.</string>
|
||||
<string name="failed_to_active_user_title">Error switching profile!</string>
|
||||
|
||||
<!-- API Error Responses - SimpleXAPI.kt -->
|
||||
<string name="connection_timeout">Connection timeout</string>
|
||||
@@ -94,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>
|
||||
@@ -269,6 +276,7 @@
|
||||
<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>
|
||||
@@ -411,6 +419,7 @@
|
||||
<!-- 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>
|
||||
@@ -440,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>
|
||||
@@ -475,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>
|
||||
@@ -494,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>
|
||||
@@ -651,7 +673,6 @@
|
||||
<string name="your_privacy">Your privacy</string>
|
||||
<string name="protect_app_screen">Protect app screen</string>
|
||||
<string name="auto_accept_images">Auto-accept images</string>
|
||||
<string name="transfer_images_faster">Transfer images faster</string>
|
||||
<string name="send_link_previews">Send link previews</string>
|
||||
<string name="full_backup">App data backup</string>
|
||||
|
||||
@@ -705,8 +726,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>
|
||||
@@ -716,6 +738,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>
|
||||
@@ -949,6 +973,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>
|
||||
@@ -956,6 +981,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>
|
||||
@@ -1001,6 +1035,8 @@
|
||||
<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>
|
||||
@@ -1055,4 +1091,45 @@
|
||||
<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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -310,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,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
|
||||
@@ -363,11 +402,29 @@ final class ChatModel: ObservableObject {
|
||||
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? {
|
||||
@@ -448,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 {
|
||||
@@ -459,62 +531,18 @@ 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 } }
|
||||
@@ -523,3 +551,41 @@ final class Chat: ObservableObject, Identifiable {
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,9 @@ func saveAnimImage(_ image: UIImage) -> String? {
|
||||
}
|
||||
|
||||
func saveImage(_ uiImage: UIImage) -> String? {
|
||||
if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE) {
|
||||
let ext = imageHasAlpha(uiImage) ? "png" : "jpg"
|
||||
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)
|
||||
}
|
||||
@@ -67,19 +68,18 @@ func cropToSquare(_ image: UIImage) -> UIImage {
|
||||
} 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))
|
||||
return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size), hasAlpha: imageHasAlpha(image))
|
||||
}
|
||||
|
||||
func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? {
|
||||
func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? {
|
||||
var img = image
|
||||
let usePng = imageHasAlpha(image)
|
||||
var data = usePng ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
||||
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)
|
||||
data = usePng ? img.pngData() : img.jpegData(compressionQuality: 0.85)
|
||||
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)")
|
||||
@@ -88,45 +88,61 @@ func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64) -> Data? {
|
||||
|
||||
func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? {
|
||||
var img = image
|
||||
var str = compressImageStr(img)
|
||||
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)
|
||||
str = compressImageStr(img)
|
||||
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) -> String? {
|
||||
let ext = imageHasAlpha(image) ? "png" : "jpg"
|
||||
if let data = imageHasAlpha(image) ? image.pngData() : image.jpegData(compressionQuality: compressionQuality) {
|
||||
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) -> UIImage {
|
||||
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)
|
||||
return resizeImage(image, newBounds: bounds, drawIn: bounds, hasAlpha: hasAlpha)
|
||||
}
|
||||
|
||||
private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect) -> UIImage {
|
||||
private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, hasAlpha: Bool) -> UIImage {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = 1.0
|
||||
format.opaque = !imageHasAlpha(image)
|
||||
format.opaque = !hasAlpha
|
||||
return UIGraphicsImageRenderer(bounds: newBounds, format: format).image { _ in
|
||||
image.draw(in: drawIn)
|
||||
}
|
||||
}
|
||||
|
||||
func imageHasAlpha(_ img: UIImage) -> Bool {
|
||||
let alpha = img.cgImage?.alphaInfo
|
||||
return alpha == .first || alpha == .last || alpha == .premultipliedFirst || alpha == .premultipliedLast || alpha == .alphaOnly
|
||||
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? {
|
||||
@@ -149,8 +165,7 @@ func saveFileFromURL(_ url: URL) -> String? {
|
||||
}
|
||||
|
||||
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
|
||||
let fileName = uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
|
||||
return fileName
|
||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
|
||||
}
|
||||
|
||||
private func uniqueCombine(_ fileName: String) -> String {
|
||||
@@ -175,6 +190,7 @@ private func getTimestamp() -> String {
|
||||
df = DateFormatter()
|
||||
df.dateFormat = "yyyyMMdd_HHmmss"
|
||||
df.locale = Locale(identifier: "US")
|
||||
df.timeZone = TimeZone(secondsFromGMT: 0)
|
||||
tsFormatter = df
|
||||
}
|
||||
return df.string(from: Date())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -227,7 +248,7 @@ func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int6
|
||||
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)
|
||||
@@ -257,13 +278,13 @@ private func sendMessageErrorAlert(_ r: ChatResponse) {
|
||||
|
||||
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 }
|
||||
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? {
|
||||
@@ -358,13 +384,13 @@ func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) a
|
||||
|
||||
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) throws -> (ConnectionStats?) {
|
||||
let r = chatSendCmdSync(.apiGroupMemberInfo(groupId: groupId, groupMemberId: groupMemberId))
|
||||
if case let .groupMemberInfo(_, _, connStats_) = r { return (connStats_) }
|
||||
if case let .groupMemberInfo(_, _, _, connStats_) = r { return (connStats_) }
|
||||
throw r
|
||||
}
|
||||
|
||||
@@ -378,44 +404,52 @@ func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws
|
||||
|
||||
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) }
|
||||
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) }
|
||||
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) }
|
||||
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) }
|
||||
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 }
|
||||
@@ -425,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?",
|
||||
@@ -484,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
|
||||
}
|
||||
|
||||
@@ -499,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
|
||||
}
|
||||
}
|
||||
@@ -565,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."
|
||||
@@ -595,17 +635,16 @@ 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 {
|
||||
let inline = privacyTransferImagesInlineGroupDefault.get()
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, inline: inline) {
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(chatItem) }
|
||||
func receiveFile(user: User, fileId: Int64) async {
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId) {
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
|
||||
}
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool? = nil) 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",
|
||||
@@ -614,7 +653,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(
|
||||
@@ -629,13 +668,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."
|
||||
@@ -748,14 +787,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
|
||||
}
|
||||
|
||||
@@ -768,22 +808,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
|
||||
}
|
||||
|
||||
@@ -798,19 +838,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 }
|
||||
if case let .groupMembers(_, group) = r { return group.members }
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -824,13 +864,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
|
||||
}
|
||||
|
||||
@@ -843,14 +883,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
|
||||
@@ -878,20 +924,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
|
||||
}
|
||||
@@ -901,6 +944,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
|
||||
@@ -941,28 +1008,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.directOrUsed {
|
||||
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.directOrUsed {
|
||||
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)
|
||||
@@ -971,15 +1046,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
|
||||
}
|
||||
@@ -989,21 +1064,30 @@ 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_AUTO_RCV {
|
||||
@@ -1011,73 +1095,99 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
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,
|
||||
@@ -1101,7 +1211,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
|
||||
@@ -1119,16 +1229,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)
|
||||
}
|
||||
@@ -1152,32 +1262,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 {
|
||||
@@ -1209,3 +1325,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()
|
||||
|
||||
@@ -228,8 +228,9 @@ struct ActiveCallOverlay: View {
|
||||
Text(call.callState.text)
|
||||
HStack {
|
||||
Text(call.encryptionStatus)
|
||||
if let connInfo = call.connectionInfo?.text {
|
||||
Text("(") + Text(connInfo) + Text(")")
|
||||
if let connInfo = call.connectionInfo {
|
||||
// Text("(") + Text(connInfo.text) + Text(", \(connInfo.protocolText))")
|
||||
Text("(") + Text(connInfo.text) + Text(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ struct IncomingCallView: View {
|
||||
private func incomingCall(_ invitation: RcvCallInvitation) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
if m.users.count > 1 {
|
||||
ProfileImage(imageStr: invitation.user.image, color: .white)
|
||||
.frame(width: 24, height: 24)
|
||||
}
|
||||
Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
|
||||
Text(invitation.callTypeText)
|
||||
}
|
||||
@@ -82,6 +86,8 @@ struct IncomingCallView: View {
|
||||
struct IncomingCallView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData
|
||||
return IncomingCallView()
|
||||
let m = ChatModel()
|
||||
m.users = [UserInfo.sampleData, UserInfo.sampleData]
|
||||
return IncomingCallView().environmentObject(m)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,22 +362,37 @@ struct ConnectionInfo: Codable, Equatable {
|
||||
var remoteCandidate: RTCIceCandidate?
|
||||
|
||||
var text: LocalizedStringKey {
|
||||
get {
|
||||
if localCandidate?.candidateType == .host && remoteCandidate?.candidateType == .host {
|
||||
return "peer-to-peer"
|
||||
} else if localCandidate?.candidateType == .relay && remoteCandidate?.candidateType == .relay {
|
||||
return "via relay"
|
||||
} else {
|
||||
let unknown = NSLocalizedString("unknown", comment: "connection info")
|
||||
return "\(localCandidate?.candidateType?.rawValue ?? unknown) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)"
|
||||
}
|
||||
let local = localCandidate?.candidateType
|
||||
let remote = remoteCandidate?.candidateType
|
||||
if local == .host && remote == .host {
|
||||
return "peer-to-peer"
|
||||
} else if local == .relay && remote == .relay {
|
||||
return "via relay"
|
||||
} else {
|
||||
let unknown = NSLocalizedString("unknown", comment: "connection info")
|
||||
return "\(local?.rawValue ?? unknown) / \(remote?.rawValue ?? unknown)"
|
||||
}
|
||||
}
|
||||
|
||||
var protocolText: String {
|
||||
let unknown = NSLocalizedString("unknown", comment: "connection info")
|
||||
let local = localCandidate?.protocol?.uppercased() ?? unknown
|
||||
let localRelay = localCandidate?.relayProtocol?.uppercased() ?? unknown
|
||||
let remote = remoteCandidate?.protocol?.uppercased() ?? unknown
|
||||
let localText = localRelay == local || localCandidate?.relayProtocol == nil
|
||||
? local
|
||||
: "\(local) (\(localRelay))"
|
||||
return local == remote
|
||||
? localText
|
||||
: "\(localText) / \(remote)"
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
|
||||
struct RTCIceCandidate: Codable, Equatable {
|
||||
var candidateType: RTCIceCandidateType?
|
||||
var `protocol`: String?
|
||||
var relayProtocol: String?
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
|
||||
@@ -396,18 +411,20 @@ struct RTCIceServer: Codable, Equatable {
|
||||
}
|
||||
|
||||
// the servers are expected in this format:
|
||||
// stun:stun.simplex.im:443
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443
|
||||
// stun:stun.simplex.im:443?transport=tcp
|
||||
// turn:private:yleob6AVkiNI87hpR94Z@turn.simplex.im:443?transport=tcp
|
||||
func parseRTCIceServer(_ str: String) -> RTCIceServer? {
|
||||
var s = replaceScheme(str, "stun:")
|
||||
s = replaceScheme(s, "turn:")
|
||||
s = replaceScheme(s, "turns:")
|
||||
if let u: URL = URL(string: s),
|
||||
let scheme = u.scheme,
|
||||
let host = u.host,
|
||||
let port = u.port,
|
||||
u.path == "" && (scheme == "stun" || scheme == "turn") {
|
||||
u.path == "" && (scheme == "stun" || scheme == "turn" || scheme == "turns") {
|
||||
let query = u.query == nil || u.query == "" ? "" : "?" + (u.query ?? "")
|
||||
return RTCIceServer(
|
||||
urls: ["\(scheme):\(host):\(port)"],
|
||||
urls: ["\(scheme):\(host):\(port)\(query)"],
|
||||
username: u.user,
|
||||
credential: u.password
|
||||
)
|
||||
|
||||
@@ -263,14 +263,14 @@ struct ChatInfoView: View {
|
||||
.foregroundColor(.accentColor)
|
||||
.font(.system(size: 14))
|
||||
Spacer()
|
||||
Text(chat.serverInfo.networkStatus.statusString)
|
||||
Text(chatModel.contactNetworkStatus(contact).statusString)
|
||||
.foregroundColor(.secondary)
|
||||
serverImage()
|
||||
}
|
||||
}
|
||||
|
||||
private func serverImage() -> some View {
|
||||
let status = chat.serverInfo.networkStatus
|
||||
let status = chatModel.contactNetworkStatus(contact)
|
||||
return Image(systemName: status.imageName)
|
||||
.foregroundColor(status == .connected ? .green : .secondary)
|
||||
.font(.system(size: 12))
|
||||
@@ -337,7 +337,7 @@ struct ChatInfoView: View {
|
||||
private func networkStatusAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Network status"),
|
||||
message: Text(chat.serverInfo.networkStatus.statusExplanation)
|
||||
message: Text(chatModel.contactNetworkStatus(contact).statusExplanation)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,10 @@ struct CIFeaturePreferenceView: View {
|
||||
.scaleEffect(feature.iconScale)
|
||||
if let ct = chat.chatInfo.contact,
|
||||
allowed != .no && ct.allowsFeature(feature) && !ct.userAllowsFeature(feature) {
|
||||
featurePreferenceView(accept: true)
|
||||
let setParam = feature == .timedMessages && ct.mergedPreferences.timedMessages.userPreference.preference.ttl == nil
|
||||
featurePreferenceView(acceptText: setParam ? "Set 1 day" : "Accept")
|
||||
.onTapGesture {
|
||||
allowFeatureToContact(ct, feature)
|
||||
allowFeatureToContact(ct, feature, param: setParam ? 86400 : nil)
|
||||
}
|
||||
} else {
|
||||
featurePreferenceView()
|
||||
@@ -36,26 +37,28 @@ struct CIFeaturePreferenceView: View {
|
||||
.textSelection(.disabled)
|
||||
}
|
||||
|
||||
private func featurePreferenceView(accept: Bool = false) -> some View {
|
||||
private func featurePreferenceView(acceptText: LocalizedStringKey? = nil) -> some View {
|
||||
var r = Text(CIContent.preferenceText(feature, allowed, param) + " ")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
if accept {
|
||||
r = r + Text("Accept" + " ")
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
if let acceptText {
|
||||
r = r
|
||||
+ Text(acceptText)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.accentColor)
|
||||
+ Text(" ")
|
||||
}
|
||||
r = r + chatItem.timestampText
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
.fontWeight(.light)
|
||||
.foregroundColor(.secondary)
|
||||
return r.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
func allowFeatureToContact(_ contact: Contact, _ feature: ChatFeature) {
|
||||
func allowFeatureToContact(_ contact: Contact, _ feature: ChatFeature, param: Int? = nil) {
|
||||
Task {
|
||||
do {
|
||||
let prefs = contactUserPreferencesToPreferences(contact.mergedPreferences).setAllowed(feature)
|
||||
let prefs = contactUserPreferencesToPreferences(contact.mergedPreferences).setAllowed(feature, param: param)
|
||||
if let toContact = try await apiSetContactPrefs(contactId: contact.contactId, preferences: prefs) {
|
||||
await MainActor.run {
|
||||
ChatModel.shared.updateContact(toContact)
|
||||
|
||||
@@ -63,7 +63,9 @@ struct CIFileView: View {
|
||||
if fileSizeValid() {
|
||||
Task {
|
||||
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
||||
await receiveFile(fileId: file.fileId)
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE)
|
||||
|
||||
@@ -35,7 +35,9 @@ struct CIImageView: View {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation:
|
||||
Task {
|
||||
await receiveFile(fileId: file.fileId)
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user: user, fileId: file.fileId)
|
||||
}
|
||||
// TODO image accepted alert?
|
||||
}
|
||||
case .rcvAccepted:
|
||||
|
||||
53
apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift
Normal file
53
apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// CIInvalidJSONView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by JRoberts on 29.12.2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CIInvalidJSONView: View {
|
||||
var json: String
|
||||
@State private var showJSON = false
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .bottom, spacing: 0) {
|
||||
Text("invalid data")
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color(uiColor: .tertiarySystemGroupedBackground))
|
||||
.cornerRadius(18)
|
||||
.textSelection(.disabled)
|
||||
.onTapGesture { showJSON = true }
|
||||
.sheet(isPresented: $showJSON) {
|
||||
invalidJSONView(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func invalidJSONView(_ json: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
Button {
|
||||
showShareSheet(items: [json])
|
||||
} label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
ScrollView {
|
||||
Text(json)
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding()
|
||||
}
|
||||
|
||||
struct CIInvalidJSONView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIInvalidJSONView(json: "{}")
|
||||
}
|
||||
}
|
||||
@@ -17,25 +17,28 @@ struct CIVoiceView: View {
|
||||
@State var playbackTime: TimeInterval?
|
||||
|
||||
var body: some View {
|
||||
VStack (
|
||||
alignment: chatItem.chatDir.sent ? .trailing : .leading,
|
||||
spacing: 6
|
||||
) {
|
||||
HStack {
|
||||
if chatItem.chatDir.sent {
|
||||
playerTime()
|
||||
.frame(width: 50, alignment: .leading)
|
||||
player()
|
||||
} else {
|
||||
player()
|
||||
playerTime()
|
||||
.frame(width: 50, alignment: .leading)
|
||||
Group {
|
||||
if chatItem.chatDir.sent {
|
||||
VStack (alignment: .trailing, spacing: 6) {
|
||||
HStack {
|
||||
playerTime()
|
||||
player()
|
||||
}
|
||||
.frame(alignment: .trailing)
|
||||
metaView().padding(.trailing, 10)
|
||||
}
|
||||
} else {
|
||||
VStack (alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
player()
|
||||
playerTime()
|
||||
}
|
||||
.frame(alignment: .leading)
|
||||
metaView().padding(.leading, -6)
|
||||
}
|
||||
}
|
||||
CIMetaView(chatItem: chatItem)
|
||||
.padding(.leading, chatItem.chatDir.sent ? 0 : 12)
|
||||
.padding(.trailing, chatItem.chatDir.sent ? 12 : 0)
|
||||
}
|
||||
.padding([.top, .horizontal], 4)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
@@ -58,6 +61,10 @@ struct CIVoiceView: View {
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
private func metaView() -> some View {
|
||||
CIMetaView(chatItem: chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
struct VoiceMessagePlayerTime: View {
|
||||
@@ -66,13 +73,16 @@ struct VoiceMessagePlayerTime: View {
|
||||
@Binding var playbackTime: TimeInterval?
|
||||
|
||||
var body: some View {
|
||||
switch playbackState {
|
||||
case .noPlayback:
|
||||
Text(voiceMessageTime(recordingTime))
|
||||
case .playing:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
case .paused:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
ZStack(alignment: .leading) {
|
||||
Text(String("66:66")).foregroundColor(.clear)
|
||||
switch playbackState {
|
||||
case .noPlayback:
|
||||
Text(voiceMessageTime(recordingTime))
|
||||
case .playing:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
case .paused:
|
||||
Text(voiceMessageTime_(playbackTime))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, preview: Bool = false) -> Text {
|
||||
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text {
|
||||
let s = text
|
||||
var res: Text
|
||||
if let ft = formattedText, ft.count > 0 {
|
||||
@@ -98,6 +98,10 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
|
||||
res = Text(s)
|
||||
}
|
||||
|
||||
if let i = icon {
|
||||
res = Text(Image(systemName: i)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + res
|
||||
}
|
||||
|
||||
if let s = sender {
|
||||
let t = Text(s)
|
||||
return (preview ? t : t.fontWeight(.medium)) + Text(": ") + res
|
||||
|
||||
@@ -72,6 +72,7 @@ struct ChatItemContentView<Content: View>: View {
|
||||
case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor)
|
||||
case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red)
|
||||
case let .invalidJSON(json): CIInvalidJSONView(json: json)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ private let memberImageSize: CGFloat = 34
|
||||
struct ChatView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@ObservedObject var chat: Chat
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State @ObservedObject var chat: Chat
|
||||
@State private var showChatInfoSheet: Bool = false
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var composeState = ComposeState()
|
||||
@@ -51,7 +52,7 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
|
||||
ComposeView(
|
||||
chat: chat,
|
||||
composeState: $composeState,
|
||||
@@ -62,30 +63,30 @@ struct ChatView: View {
|
||||
.padding(.top, 1)
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.onAppear {
|
||||
if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft {
|
||||
composeState = draft
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
Task {
|
||||
await markChatUnread(chat, unreadChat: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
chatModel.chatId = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
if chatModel.chatId == nil {
|
||||
chatModel.reversedChatItems = []
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "chevron.backward")
|
||||
Text("Chats")
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if chatModel.chatId == nil { dismiss() }
|
||||
}
|
||||
.onDisappear {
|
||||
if chatModel.chatId == cInfo.id {
|
||||
chatModel.chatId = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
if chatModel.chatId == nil {
|
||||
chatModel.reversedChatItems = []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if case let .direct(contact) = cInfo {
|
||||
Button {
|
||||
@@ -177,7 +178,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func searchToolbar() -> some View {
|
||||
HStack {
|
||||
HStack {
|
||||
@@ -233,7 +234,6 @@ struct ChatView: View {
|
||||
if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) {
|
||||
Task {
|
||||
await apiMarkChatItemRead(cInfo, ci)
|
||||
NtfManager.shared.decNtfBadgeCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,9 +253,10 @@ struct ChatView: View {
|
||||
loadChat(chat: chat, search: searchText)
|
||||
}
|
||||
.onChange(of: chatModel.chatId) { _ in
|
||||
if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) {
|
||||
if let chatId = chatModel.chatId, let c = chatModel.getChat(chatId) {
|
||||
chat = c
|
||||
showChatInfoSheet = false
|
||||
loadChat(chat: chat)
|
||||
loadChat(chat: c)
|
||||
DispatchQueue.main.async {
|
||||
scrollToBottom(proxy)
|
||||
}
|
||||
@@ -441,9 +442,13 @@ struct ChatView: View {
|
||||
|
||||
var body: some View {
|
||||
let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading
|
||||
|
||||
let uiMenu: Binding<UIMenu> = Binding(
|
||||
get: { UIMenu(title: "", children: menu(live: composeState.liveMessage != nil)) },
|
||||
set: { _ in }
|
||||
)
|
||||
|
||||
ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed)
|
||||
.uiKitContextMenu(actions: menu())
|
||||
.uiKitContextMenu(menu: uiMenu)
|
||||
.confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) {
|
||||
Button("Delete for me", role: .destructive) {
|
||||
deleteMessage(.cidmInternal)
|
||||
@@ -458,30 +463,34 @@ struct ChatView: View {
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: alignment)
|
||||
}
|
||||
|
||||
private func menu() -> [UIAction] {
|
||||
private func menu(live: Bool) -> [UIAction] {
|
||||
var menu: [UIAction] = []
|
||||
if let mc = ci.content.msgContent, !ci.meta.itemDeleted || revealed {
|
||||
if !ci.meta.itemDeleted {
|
||||
if !ci.meta.itemDeleted && !ci.isLiveDummy && !live {
|
||||
menu.append(replyUIAction())
|
||||
}
|
||||
menu.append(shareUIAction())
|
||||
menu.append(copyUIAction())
|
||||
if let filePath = getLoadedFilePath(ci.file) {
|
||||
if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) {
|
||||
if image.imageData != nil, let filePath = getLoadedFilePath(ci.file) {
|
||||
if image.imageData != nil {
|
||||
menu.append(saveFileAction(filePath))
|
||||
} else {
|
||||
menu.append(saveImageAction(image))
|
||||
}
|
||||
} else if case .file = ci.content.msgContent, let filePath = getLoadedFilePath(ci.file) {
|
||||
} else {
|
||||
menu.append(saveFileAction(filePath))
|
||||
}
|
||||
if ci.meta.editable && !mc.isVoice {
|
||||
}
|
||||
if ci.meta.editable && !mc.isVoice && !live {
|
||||
menu.append(editAction())
|
||||
}
|
||||
if revealed {
|
||||
menu.append(hideUIAction())
|
||||
}
|
||||
menu.append(deleteUIAction())
|
||||
if !live || !ci.meta.isLive {
|
||||
menu.append(deleteUIAction())
|
||||
}
|
||||
} else if ci.meta.itemDeleted {
|
||||
menu.append(revealUIAction())
|
||||
menu.append(deleteUIAction())
|
||||
|
||||
@@ -14,9 +14,9 @@ import PhotosUI
|
||||
enum ComposePreview {
|
||||
case noPreview
|
||||
case linkPreview(linkPreview: LinkPreview?)
|
||||
case imagePreviews(imagePreviews: [String])
|
||||
case imagePreviews(imagePreviews: [(String, UploadContent?)])
|
||||
case voicePreview(recordingFileName: String, duration: Int)
|
||||
case filePreview(fileName: String)
|
||||
case filePreview(fileName: String, file: URL)
|
||||
}
|
||||
|
||||
enum ComposeContextItem {
|
||||
@@ -34,7 +34,7 @@ enum VoiceMessageRecordingState {
|
||||
struct LiveMessage {
|
||||
var chatItem: ChatItem
|
||||
var typedMsg: String
|
||||
var sentMsg: String
|
||||
var sentMsg: String?
|
||||
}
|
||||
|
||||
struct ComposeState {
|
||||
@@ -96,6 +96,13 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
var quoting: Bool {
|
||||
switch contextItem {
|
||||
case .quotedItem: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var sendEnabled: Bool {
|
||||
switch preview {
|
||||
case .imagePreviews: return true
|
||||
@@ -105,6 +112,10 @@ struct ComposeState {
|
||||
}
|
||||
}
|
||||
|
||||
var endLiveDisabled: Bool {
|
||||
liveMessage != nil && message.isEmpty && noPreview && !quoting
|
||||
}
|
||||
|
||||
var linkPreviewAllowed: Bool {
|
||||
switch preview {
|
||||
case .imagePreviews: return false
|
||||
@@ -150,6 +161,10 @@ struct ComposeState {
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
var empty: Bool {
|
||||
message == "" && noPreview
|
||||
}
|
||||
}
|
||||
|
||||
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
||||
@@ -160,11 +175,12 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
||||
case let .link(_, preview: preview):
|
||||
chatItemPreview = .linkPreview(linkPreview: preview)
|
||||
case let .image(_, image):
|
||||
chatItemPreview = .imagePreviews(imagePreviews: [image])
|
||||
chatItemPreview = .imagePreviews(imagePreviews: [(image, nil)])
|
||||
case let .voice(_, duration):
|
||||
chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
|
||||
case .file:
|
||||
chatItemPreview = .filePreview(fileName: chatItem.file?.fileName ?? "")
|
||||
let fileName = chatItem.file?.fileName ?? ""
|
||||
chatItemPreview = .filePreview(fileName: fileName, file: getAppFilePath(fileName))
|
||||
default:
|
||||
chatItemPreview = .noPreview
|
||||
}
|
||||
@@ -218,7 +234,6 @@ struct ComposeView: View {
|
||||
@State private var showTakePhoto = false
|
||||
@State var chosenImages: [UploadContent] = []
|
||||
@State private var showFileImporter = false
|
||||
@State var chosenFile: URL? = nil
|
||||
|
||||
@State private var audioRecorder: AudioRecorder?
|
||||
@State private var voiceMessageRecordingTime: TimeInterval?
|
||||
@@ -232,9 +247,9 @@ struct ComposeView: View {
|
||||
VStack(spacing: 0) {
|
||||
contextItemView()
|
||||
switch (composeState.editing, composeState.preview) {
|
||||
case (true, .filePreview): EmptyView()
|
||||
case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed
|
||||
default: previewView()
|
||||
case (true, .filePreview): EmptyView()
|
||||
case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed
|
||||
default: previewView()
|
||||
}
|
||||
HStack (alignment: .bottom) {
|
||||
Button {
|
||||
@@ -255,6 +270,10 @@ struct ComposeView: View {
|
||||
},
|
||||
sendLiveMessage: sendLiveMessage,
|
||||
updateLiveMessage: updateLiveMessage,
|
||||
cancelLiveMessage: {
|
||||
composeState.liveMessage = nil
|
||||
chatModel.removeLiveDummy()
|
||||
},
|
||||
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
||||
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
||||
startVoiceMessageRecording: {
|
||||
@@ -322,10 +341,10 @@ struct ComposeView: View {
|
||||
}
|
||||
.onChange(of: chosenImages) { images in
|
||||
Task {
|
||||
var imgs: [String] = []
|
||||
var imgs: [(String, UploadContent)] = []
|
||||
for image in images {
|
||||
if let img = resizeImageToStrSize(image.uiImage, maxDataSize: 14000) {
|
||||
imgs.append(img)
|
||||
imgs.append((img, image))
|
||||
await MainActor.run {
|
||||
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs))
|
||||
}
|
||||
@@ -352,9 +371,8 @@ struct ComposeView: View {
|
||||
}
|
||||
fileURL.stopAccessingSecurityScopedResource()
|
||||
if let fileSize = fileSize,
|
||||
fileSize <= MAX_FILE_SIZE {
|
||||
chosenFile = fileURL
|
||||
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent))
|
||||
fileSize <= MAX_FILE_SIZE {
|
||||
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL))
|
||||
} else {
|
||||
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -368,13 +386,20 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if let fileName = composeState.voiceMessageRecordingFileName {
|
||||
cancelVoiceMessageRecording(fileName)
|
||||
}
|
||||
if composeState.liveMessage != nil {
|
||||
if composeState.liveMessage != nil
|
||||
&& (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
|
||||
cancelCurrentVoiceRecording()
|
||||
clearCurrentDraft()
|
||||
sendMessage()
|
||||
resetLinkPreview()
|
||||
} else if !composeState.empty {
|
||||
saveCurrentDraft()
|
||||
} else {
|
||||
cancelCurrentVoiceRecording()
|
||||
clearCurrentDraft()
|
||||
clearState()
|
||||
}
|
||||
chatModel.removeLiveDummy(animated: false)
|
||||
}
|
||||
.onChange(of: chatModel.stopPreviousRecPlay) { _ in
|
||||
if !startingRecording {
|
||||
@@ -389,17 +414,29 @@ struct ComposeView: View {
|
||||
if !vmAllowed && composeState.voicePreview,
|
||||
let fileName = composeState.voiceMessageRecordingFileName {
|
||||
cancelVoiceMessageRecording(fileName)
|
||||
clearState()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if case let .voicePreview(_, duration) = composeState.preview {
|
||||
voiceMessageRecordingTime = TimeInterval(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sendLiveMessage() async {
|
||||
let typedMsg = composeState.message
|
||||
let sentMsg = truncateToWords(typedMsg)
|
||||
if composeState.liveMessage == nil,
|
||||
let ci = await sendMessageAsync(sentMsg, live: true) {
|
||||
let lm = composeState.liveMessage
|
||||
if (composeState.sendEnabled || composeState.quoting)
|
||||
&& (lm == nil || lm?.sentMsg == nil),
|
||||
let ci = await sendMessageAsync(typedMsg, live: true) {
|
||||
await MainActor.run {
|
||||
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: sentMsg))
|
||||
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: typedMsg))
|
||||
}
|
||||
} else if lm == nil {
|
||||
let cItem = chatModel.addLiveDummy(chat.chatInfo)
|
||||
await MainActor.run {
|
||||
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: cItem, typedMsg: typedMsg, sentMsg: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,7 +461,7 @@ struct ComposeView: View {
|
||||
|
||||
private func liveMessageToSend(_ lm: LiveMessage, _ t: String) -> String? {
|
||||
let s = t != lm.typedMsg ? truncateToWords(t) : t
|
||||
return s != lm.sentMsg ? s : nil
|
||||
return s != lm.sentMsg && (lm.sentMsg != nil || !s.isEmpty) ? s : nil
|
||||
}
|
||||
|
||||
private func truncateToWords(_ s: String) -> String {
|
||||
@@ -449,7 +486,7 @@ struct ComposeView: View {
|
||||
ComposeLinkView(linkPreview: preview, cancelPreview: cancelLinkPreview)
|
||||
case let .imagePreviews(imagePreviews: images):
|
||||
ComposeImageView(
|
||||
images: images,
|
||||
images: images.map { (img, _) in img },
|
||||
cancelImage: {
|
||||
composeState = composeState.copy(preview: .noPreview)
|
||||
chosenImages = []
|
||||
@@ -460,16 +497,18 @@ struct ComposeView: View {
|
||||
recordingFileName: recordingFileName,
|
||||
recordingTime: $voiceMessageRecordingTime,
|
||||
recordingState: $composeState.voiceMessageRecordingState,
|
||||
cancelVoiceMessage: { cancelVoiceMessageRecording($0) },
|
||||
cancelVoiceMessage: {
|
||||
cancelVoiceMessageRecording($0)
|
||||
clearState()
|
||||
},
|
||||
cancelEnabled: !composeState.editing,
|
||||
stopPlayback: $stopPlayback
|
||||
)
|
||||
case let .filePreview(fileName: fileName):
|
||||
case let .filePreview(fileName, _):
|
||||
ComposeFileView(
|
||||
fileName: fileName,
|
||||
cancelFile: {
|
||||
composeState = composeState.copy(preview: .noPreview)
|
||||
chosenFile = nil
|
||||
},
|
||||
cancelEnabled: !composeState.editing)
|
||||
}
|
||||
@@ -505,10 +544,14 @@ struct ComposeView: View {
|
||||
private func sendMessageAsync(_ text: String?, live: Bool) async -> ChatItem? {
|
||||
var sent: ChatItem?
|
||||
let msgText = text ?? composeState.message
|
||||
if !live { await sending() }
|
||||
let liveMessage = composeState.liveMessage
|
||||
if !live {
|
||||
if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) }
|
||||
await sending()
|
||||
}
|
||||
if case let .editingItem(ci) = composeState.contextItem {
|
||||
sent = await updateMessage(ci, live: live)
|
||||
} else if let liveMessage = composeState.liveMessage {
|
||||
} else if let liveMessage = liveMessage, liveMessage.sentMsg != nil {
|
||||
sent = await updateMessage(liveMessage.chatItem, live: live)
|
||||
} else {
|
||||
var quoted: Int64? = nil
|
||||
@@ -522,25 +565,23 @@ struct ComposeView: View {
|
||||
case .linkPreview:
|
||||
sent = await send(checkLinkPreview(), quoted: quoted, live: live)
|
||||
case let .imagePreviews(imagePreviews: images):
|
||||
let last = min(chosenImages.count, images.count) - 1
|
||||
for i in 0..<last {
|
||||
if let savedFile = saveAnyImage(chosenImages[i]) {
|
||||
_ = await send(.image(text: "", image: images[i]), quoted: nil, file: savedFile)
|
||||
let last = images.count - 1
|
||||
if last >= 0 {
|
||||
for i in 0..<last {
|
||||
sent = await sendImage(images[i])
|
||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
if let savedFile = saveAnyImage(chosenImages[last]) {
|
||||
sent = await send(.image(text: msgText, image: images[last]), quoted: quoted, file: savedFile, live: live)
|
||||
sent = await sendImage(images[last], text: msgText, quoted: quoted, live: live)
|
||||
}
|
||||
if sent == nil {
|
||||
sent = await send(.text(msgText), quoted: quoted, live: live)
|
||||
}
|
||||
case let .voicePreview(recordingFileName, duration):
|
||||
stopPlayback.toggle()
|
||||
chatModel.filesToDelete.removeAll { $0 == recordingFileName }
|
||||
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName)
|
||||
case .filePreview:
|
||||
if let fileURL = chosenFile,
|
||||
let savedFile = saveFileFromURL(fileURL) {
|
||||
case let .filePreview(_, file):
|
||||
if let savedFile = saveFileFromURL(file) {
|
||||
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live)
|
||||
}
|
||||
}
|
||||
@@ -595,6 +636,14 @@ struct ComposeView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false) async -> ChatItem? {
|
||||
let (image, data) = imageData
|
||||
if let data = data, let savedFile = saveAnyImage(data) {
|
||||
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false) async -> ChatItem? {
|
||||
if let chatItem = await apiSendMessage(
|
||||
type: chat.chatInfo.chatType,
|
||||
@@ -605,6 +654,7 @@ struct ComposeView: View {
|
||||
live: live
|
||||
) {
|
||||
await MainActor.run {
|
||||
chatModel.removeLiveDummy(animated: false)
|
||||
chatModel.addChatItem(chat.chatInfo, chatItem)
|
||||
}
|
||||
return chatItem
|
||||
@@ -704,11 +754,16 @@ struct ComposeView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func cancelCurrentVoiceRecording() {
|
||||
if let fileName = composeState.voiceMessageRecordingFileName {
|
||||
cancelVoiceMessageRecording(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelVoiceMessageRecording(_ fileName: String) {
|
||||
stopPlayback.toggle()
|
||||
audioRecorder?.stop()
|
||||
removeFile(fileName)
|
||||
clearState()
|
||||
}
|
||||
|
||||
private func clearState(live: Bool = false) {
|
||||
@@ -720,12 +775,29 @@ struct ComposeView: View {
|
||||
resetLinkPreview()
|
||||
}
|
||||
chosenImages = []
|
||||
chosenFile = nil
|
||||
audioRecorder = nil
|
||||
voiceMessageRecordingTime = nil
|
||||
startingRecording = false
|
||||
}
|
||||
|
||||
private func saveCurrentDraft() {
|
||||
if case .recording = composeState.voiceMessageRecordingState {
|
||||
finishVoiceMessageRecording()
|
||||
if let fileName = composeState.voiceMessageRecordingFileName {
|
||||
chatModel.filesToDelete.append(fileName)
|
||||
}
|
||||
}
|
||||
chatModel.draft = composeState
|
||||
chatModel.draftChatId = chat.id
|
||||
}
|
||||
|
||||
private func clearCurrentDraft() {
|
||||
if chatModel.draftChatId == chat.id {
|
||||
chatModel.draft = nil
|
||||
chatModel.draftChatId = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func showLinkPreview(_ s: String) {
|
||||
prevLinkUrl = linkUrl
|
||||
linkUrl = parseMessage(s)
|
||||
|
||||
@@ -16,13 +16,11 @@ enum VoiceMessagePlaybackState {
|
||||
}
|
||||
|
||||
func voiceMessageTime(_ time: TimeInterval) -> String {
|
||||
let min = Int(time / 60)
|
||||
let sec = Int(time.truncatingRemainder(dividingBy: 60))
|
||||
return String(format: "%02d:%02d", min, sec)
|
||||
durationText(Int(time))
|
||||
}
|
||||
|
||||
func voiceMessageTime_(_ time: TimeInterval?) -> String {
|
||||
return voiceMessageTime(time ?? TimeInterval(0))
|
||||
durationText(Int(time ?? 0))
|
||||
}
|
||||
|
||||
struct ComposeVoiceView: View {
|
||||
|
||||
@@ -21,7 +21,6 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let field = CustomUITextField()
|
||||
field.allowsEditingTextAttributes = true
|
||||
field.text = text
|
||||
field.font = font
|
||||
field.textAlignment = alignment == .leading ? .left : .right
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
private let liveMsgInterval: UInt64 = 3000_000000
|
||||
|
||||
struct SendMessageView: View {
|
||||
@Binding var composeState: ComposeState
|
||||
var sendMessage: () -> Void
|
||||
var sendLiveMessage: (() async -> Void)? = nil
|
||||
var updateLiveMessage: (() async -> Void)? = nil
|
||||
var cancelLiveMessage: (() -> Void)? = nil
|
||||
var showVoiceMessageButton: Bool = true
|
||||
var voiceMessageAllowed: Bool = true
|
||||
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
|
||||
@@ -73,39 +76,20 @@ struct SendMessageView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if (composeState.inProgress) {
|
||||
if composeState.inProgress {
|
||||
ProgressView()
|
||||
.scaleEffect(1.4)
|
||||
.frame(width: 31, height: 31, alignment: .center)
|
||||
.padding([.bottom, .trailing], 3)
|
||||
} else {
|
||||
let vmrs = composeState.voiceMessageRecordingState
|
||||
if showVoiceMessageButton
|
||||
&& composeState.message.isEmpty
|
||||
&& !composeState.editing
|
||||
&& composeState.liveMessage == nil
|
||||
&& ((composeState.noPreview && vmrs == .noRecording)
|
||||
|| (vmrs == .recording && holdingVMR)) {
|
||||
HStack {
|
||||
if voiceMessageAllowed {
|
||||
RecordVoiceMessageButton(
|
||||
startVoiceMessageRecording: startVoiceMessageRecording,
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
holdingVMR: $holdingVMR,
|
||||
disabled: composeState.disabled
|
||||
)
|
||||
} else {
|
||||
voiceMessageNotAllowedButton()
|
||||
}
|
||||
if let send = sendLiveMessage, let update = updateLiveMessage {
|
||||
startLiveMessageButton(send: send, update: update)
|
||||
}
|
||||
VStack(alignment: .trailing) {
|
||||
if teHeight > 100 {
|
||||
deleteTextButton()
|
||||
Spacer()
|
||||
}
|
||||
} else if vmrs == .recording && !holdingVMR {
|
||||
finishVoiceMessageRecordingButton()
|
||||
} else {
|
||||
sendMessageButton()
|
||||
composeActionButtons()
|
||||
}
|
||||
.frame(height: teHeight, alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +100,52 @@ struct SendMessageView: View {
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
@ViewBuilder private func composeActionButtons() -> some View {
|
||||
let vmrs = composeState.voiceMessageRecordingState
|
||||
if showVoiceMessageButton
|
||||
&& composeState.message.isEmpty
|
||||
&& !composeState.editing
|
||||
&& composeState.liveMessage == nil
|
||||
&& ((composeState.noPreview && vmrs == .noRecording)
|
||||
|| (vmrs == .recording && holdingVMR)) {
|
||||
HStack {
|
||||
if voiceMessageAllowed {
|
||||
RecordVoiceMessageButton(
|
||||
startVoiceMessageRecording: startVoiceMessageRecording,
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
holdingVMR: $holdingVMR,
|
||||
disabled: composeState.disabled
|
||||
)
|
||||
} else {
|
||||
voiceMessageNotAllowedButton()
|
||||
}
|
||||
if let send = sendLiveMessage,
|
||||
let update = updateLiveMessage,
|
||||
case .noContextItem = composeState.contextItem {
|
||||
startLiveMessageButton(send: send, update: update)
|
||||
}
|
||||
}
|
||||
} else if vmrs == .recording && !holdingVMR {
|
||||
finishVoiceMessageRecordingButton()
|
||||
} else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty {
|
||||
cancelLiveMessageButton {
|
||||
cancelLiveMessage?()
|
||||
}
|
||||
} else {
|
||||
sendMessageButton()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteTextButton() -> some View {
|
||||
Button {
|
||||
composeState.message = ""
|
||||
} label: {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
}
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
.padding([.top, .trailing], 4)
|
||||
}
|
||||
|
||||
@ViewBuilder private func sendMessageButton() -> some View {
|
||||
let v = Button(action: sendMessage) {
|
||||
Image(systemName: composeState.editing || composeState.liveMessage != nil
|
||||
@@ -129,11 +159,13 @@ struct SendMessageView: View {
|
||||
.disabled(
|
||||
!composeState.sendEnabled ||
|
||||
composeState.disabled ||
|
||||
(!voiceMessageAllowed && composeState.voicePreview)
|
||||
(!voiceMessageAllowed && composeState.voicePreview) ||
|
||||
composeState.endLiveDisabled
|
||||
)
|
||||
.frame(width: 29, height: 29)
|
||||
|
||||
if composeState.liveMessage == nil,
|
||||
case .noContextItem = composeState.contextItem,
|
||||
!composeState.voicePreview && !composeState.editing,
|
||||
let send = sendLiveMessage,
|
||||
let update = updateLiveMessage {
|
||||
@@ -141,7 +173,7 @@ struct SendMessageView: View {
|
||||
Button {
|
||||
startLiveMessage(send: send, update: update)
|
||||
} label: {
|
||||
Label("Send live message", systemImage: "ellipsis.circle")
|
||||
Label("Send live message", systemImage: "bolt.fill")
|
||||
}
|
||||
}
|
||||
.padding([.bottom, .trailing], 4)
|
||||
@@ -220,6 +252,20 @@ struct SendMessageView: View {
|
||||
.padding([.bottom, .trailing], 4)
|
||||
}
|
||||
|
||||
private func cancelLiveMessageButton(cancel: @escaping () -> Void) -> some View {
|
||||
return Button {
|
||||
cancel()
|
||||
} label: {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
.padding([.bottom, .horizontal], 4)
|
||||
}
|
||||
|
||||
private func startLiveMessageButton(send: @escaping () async -> Void, update: @escaping () async -> Void) -> some View {
|
||||
return Button {
|
||||
switch composeState.preview {
|
||||
@@ -227,9 +273,11 @@ struct SendMessageView: View {
|
||||
default: ()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle.fill")
|
||||
Image(systemName: "bolt.fill")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.foregroundColor(.accentColor)
|
||||
.frame(width: 20, height: 20)
|
||||
}
|
||||
.frame(width: 29, height: 29)
|
||||
.padding([.bottom, .horizontal], 4)
|
||||
@@ -269,9 +317,12 @@ struct SendMessageView: View {
|
||||
sendButtonOpacity = 1
|
||||
}
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { t in
|
||||
if composeState.liveMessage == nil { t.invalidate() }
|
||||
Task { await update() }
|
||||
Task {
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
while composeState.liveMessage != nil {
|
||||
await update()
|
||||
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user