mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
170 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d1a70af34 | |||
| ab91c54080 | |||
| 6b71cc59c8 | |||
| 33a866463d | |||
| 02fa81e8aa | |||
| a8b9200c9a | |||
| 002a081b3b | |||
| c2b76a75b5 | |||
| 2742fc3ca9 | |||
| f3731799bc | |||
| 7a78dfd3e3 | |||
| 5a2dd7b4bc | |||
| 1a4d2b6de6 | |||
| e4b46a45d3 | |||
| d61a7fb4d8 | |||
| bddb37593c | |||
| 8b794b2285 | |||
| 8d0ec01a9b | |||
| d85aa655cb | |||
| ba0cffb511 | |||
| d029ce9817 | |||
| 678f4f5e87 | |||
| b780a41272 | |||
| 1caaca83cb | |||
| c8b2bcb064 | |||
| adfe20b54c | |||
| b9d625da18 | |||
| 8631cf1471 | |||
| 0b6b8bd327 | |||
| e83ed30a49 | |||
| 29f919b3d6 | |||
| 2636f2ce1c | |||
| f80f56de61 | |||
| 941660625d | |||
| ad1432e0ee | |||
| 1cfbbd3115 | |||
| 21ffe0ad49 | |||
| 54ad071655 | |||
| 992c934fd1 | |||
| a6c6f1dbff | |||
| a652a14d58 | |||
| 9e77f05e58 | |||
| 355a3c429c | |||
| a1ce3b9c69 | |||
| 00af82cb19 | |||
| 68b6d9e966 | |||
| 75165cc70a | |||
| e5bf5092b1 | |||
| e5a4cca5e0 | |||
| 41f4f11155 | |||
| 99bfd446d1 | |||
| dd740e82cf | |||
| 5fabeff1fa | |||
| 324730f8ae | |||
| ed1faff500 | |||
| ce7d0ab8cf | |||
| 75dccf95c4 | |||
| fa5a70cd19 | |||
| dd9e94eefd | |||
| 0f65a001c8 | |||
| f3e59aa3c3 | |||
| 655041c657 | |||
| 4ca118666a | |||
| 365f92e958 | |||
| ddecd847e5 | |||
| 18677cec63 | |||
| 4e8dcab020 | |||
| eb0f78bd80 | |||
| cf1bd0d467 | |||
| 00f712dc59 | |||
| 0a27f8834d | |||
| f8678af261 | |||
| b2f663dde3 | |||
| 038b936bfe | |||
| 20def8c7a0 | |||
| a9b4489f4f | |||
| 5ca21dea13 | |||
| 80ca80f6d8 | |||
| 687a741723 | |||
| 54ab4e979a | |||
| b6696e901b | |||
| 89de5497ef | |||
| 1bf3154488 | |||
| c78acfda33 | |||
| 1432a04927 | |||
| d432dfba21 | |||
| 5243613045 | |||
| 83599adc80 | |||
| 538992eb95 | |||
| 658daf56bb | |||
| 7d31862576 | |||
| 7a1d0eac9d | |||
| d851396113 | |||
| cbdd9b9e37 | |||
| d5fc0d7dfc | |||
| 0d0de1da86 | |||
| 4e5a5c11dc | |||
| 14038ce370 | |||
| a72f603e13 | |||
| 85609ef217 | |||
| 7631d59695 | |||
| 38f305bb34 | |||
| 290ef9de61 | |||
| f38d3b4d7f | |||
| 8f638df7a9 | |||
| 8a121b4442 | |||
| 1bb7a954f5 | |||
| ddd8bd9061 | |||
| 179b9e093f | |||
| 352a4f3d2a | |||
| e06d4e5c85 | |||
| 385ebd2298 | |||
| a20f0050b9 | |||
| e17e9c641d | |||
| 2ac6866638 | |||
| d7f319aa9e | |||
| 1e10b0a49c | |||
| e9c321eaad | |||
| c58d069fbd | |||
| 15c8945fd3 | |||
| 2109dca1c7 | |||
| 7ef7d08c00 | |||
| e463e19114 | |||
| deeb2e891a | |||
| 5265667c0c | |||
| 15c1f9f9c8 | |||
| 341199d599 | |||
| dfb6dafd6f | |||
| 7f544da6cf | |||
| d0a0a0461f | |||
| 1dca577ca8 | |||
| 26984b62fe | |||
| 1470b8d128 | |||
| 5bcb725ea5 | |||
| 7f9c4ede02 | |||
| 34a74da0b9 | |||
| 98cb1c39f2 | |||
| c4fc8a97b1 | |||
| 9edb54b45c | |||
| 213b586f8f | |||
| e0582d656e | |||
| 65f8ad4423 | |||
| b115bda8f5 | |||
| 85ab37ae5d | |||
| dc5f6673a2 | |||
| ada4c1d2ec | |||
| 1f8bfbe3f5 | |||
| cf67c951fc | |||
| b78a5931e9 | |||
| e57b9f4cea | |||
| 104e1040bf | |||
| 2c347cb7b9 | |||
| fad60a1d24 | |||
| 61ed866c24 | |||
| 656c10976b | |||
| 63f40f030a | |||
| dec8e136f9 | |||
| a525b4e5db | |||
| 2f8b4a3e93 | |||
| 39818b6fbb | |||
| 3523c7ebd7 | |||
| 1b3984f52f | |||
| 560807b4b7 | |||
| fb03a119ea | |||
| f7da034cf1 | |||
| 4a259e44b1 | |||
| 799d9443bd | |||
| 3bf8361911 | |||
| 9670da4646 | |||
| 1e22028189 |
@@ -108,7 +108,8 @@ jobs:
|
||||
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest'
|
||||
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
|
||||
timeout-minutes: 10
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
|
||||
+12
-6
@@ -1,22 +1,26 @@
|
||||
# SimpleX Chat Terms & Privacy Policy
|
||||
|
||||
SimpleX Chat is the first chat platform that is 100% private by design - not only it has no access to your messages (thanks to open-source double-ratchet end-to-end encryption protocol and additional encryption layers), it also has no access to your profile and contacts - we do not have access to your connections graph.
|
||||
SimpleX Chat is the first communication platform that has no user profile IDs of any kind, not even random numbers. Not only it has no access to your messages (thanks to open-source double-ratchet end-to-end encryption protocol and additional encryption layers), it also has no access to your profile and contacts - we cannot observe your connections graph.
|
||||
|
||||
If you believe that some of the clauses in this document are not aligned with our mission or principles, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion).
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
SimpleX Chat Ltd. ("SimpleX Chat") uses the best industry practices for security and end-to-end encryption to provide secure end-to-end encrypted messaging via private connections. SimpleX Chat is built on top of SimpleX messaging and application platform that uses a new message routing protocol that allows establishing private connection without having any kind of addresses that identify its users - we don't use emails, phone numbers, usernames, identity keys or any other user identifiers to pass messages between the users.
|
||||
|
||||
SimpleX Chat security audit was performed in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
### Information you provide
|
||||
|
||||
We do not store user profiles. The profile you create in the app is local to your device. When you create a user profile, no records are created on our servers, and we have no access to any part of your profile information, and even to the fact that you created a profile - it is a local record stored only on your device. That means that if you delete the app, and have no backup, you will permanently lose all the data and the private connections you create with other users.
|
||||
|
||||
Messages. SimpleX Chat cannot decrypt or otherwise access the content or size of your messages (each message is padded to a fixed size of 16kb). SimpleX Chat temporarily stores end-to-end encrypted messages on its servers for delivery to the devices that are temporarily offline. Your message history is stored only on your own devices.
|
||||
Messages. SimpleX Chat cannot decrypt or otherwise access the content or even size of your messages (each message is padded to a fixed size of 16kb). SimpleX Chat temporarily stores end-to-end encrypted messages on its servers for delivery to the devices that are offline, these messages are permanently removed as soon as they are delivered. Your message history is stored only on your own devices.
|
||||
|
||||
Connections with other users. When you create a connection with another user, two messaging queues are created on our servers (we use separate queues for direct and response messages, that can be on two different servers), or on the servers that you configured in the app, in case it allows such configuration. At the time of updating this document only our terminal app allows configuring the servers, our mobile apps will allow such configuration in the near future. Our servers do not store information about which queues are linked to your profile on the device, and they do not have any information in common that allow us to establish that these queues are related to your device or your profile - the access to each queue is authorized by a set of unique encryption keys, different for each queue, and separate for sender and recipient of the messages that are transmitted through the queue.
|
||||
Connections with other users. When you create a connection with another user, two messaging queues (you can think about them as about mailboxes) are created on our servers, or on the servers that you configured in the app, in case it allows such configuration (SimpleX uses separate queues for direct and response messages, that the client applications prefer to create on two different servers, in case you have more than one server configured in the app, which is the default). At the time of updating this document all our client applications allow configuring the servers. Our servers do not store information about which queues are linked to your profile on the device, and they do not collect any information that would allow us to establish that these queues are related to your device or your profile - the access to each queue is authorized by a set of anonymous unique cryptographic keys, different for each queue, and separate for sender and recipient of the messages. The exception to that is when you choose to use instant push notifications in our iOS app, because the design of push notifications requires storing the device token on notification server, and the server can observe how many messaging queues your device uses, and approximate how many messages are sent to each queue. It does not allow though to determine the actual addresses of these queues, as a separate address is used to subscibe to the notifications (unless notification and messaging servers exchange information), and who, or even how many contacts, send messages to you, as notifications are delivered to your device end-to-end encrypted by the messaging servers. It also does not allow to see message content or sizes, as the actual messages are not sent via the notification service, only the fact that the message is available and where it can be received from (the latter information is encrypted, so that the notification server cannot see it). You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off).
|
||||
|
||||
Additional technical information can be stored on our servers, including randomly generated authentication tokens, keys, push tokens, and other material that is necessary to transmit messages. SimpleX Chat limits this additional technical information to the minimum required to operate the Services.
|
||||
|
||||
User Support. If you contact SimpleX Chat any personal data you may share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support via chat, when it is possible.
|
||||
User Support. If you contact SimpleX Chat any personal data you may share with us is kept only for the purposes of researching the issue and contacting you about your case. We recommend contacting support [via chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion), when it is possible.
|
||||
|
||||
### Information we may share
|
||||
|
||||
@@ -31,6 +35,8 @@ The cases when SimpleX Chat may need to share the data we temporarily store on t
|
||||
- To detect, prevent, or otherwise address fraud, security, or technical issues.
|
||||
- To protect against harm to the rights, property, or safety of SimpleX Chat, our users, or the public as required or permitted by law.
|
||||
|
||||
At the time of updating this document, we have never provided or have been requested the access to our servers or any information from our servers by any third parties. If we are ever requested to provide such access or information, we will follow the due legal process.
|
||||
|
||||
### Updates
|
||||
|
||||
We will update this privacy policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our Services confirms your acceptance of our updated Privacy Policy.
|
||||
@@ -47,7 +53,7 @@ You accept to our Terms of Service ("Terms") by installing or using any of our a
|
||||
|
||||
**Accessing the servers**. For the efficiency of the network access, the apps access all queues you create on any server via the same network (TCP/IP) connection. Our servers do not collect information about which queues were accessed via the same connection, so we do cannot establish which queues belong to the same users. Whoever might observe your network traffic would know which servers you use, and how much data you send, but not to whom it is sent - the data that leaves the servers is always different from the data they receive - there are no identifiers or cyphertext in common. Please refer to our [technical design document](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) for more information about our privacy model and known security and privacy risks.
|
||||
|
||||
**Privacy of user data**. We do not retain any data we transmit for any longer than necessary to provide the Services. We only collect aggregate statistics across all users, not per users - we do not have information about how many people use SimpleX Chat (we only know an approximate number of app installations and the aggregate traffic through our servers). In any case, we do not and will not sell or in any way monetize user data.
|
||||
**Privacy of user data**. We do not retain any data we transmit for any longer than necessary to provide the Services. We only collect aggregate statistics across all users, not per user - we do not have information about how many people use SimpleX Chat (we only know an approximate number of app installations and the aggregate traffic through our servers). In any case, we do not and will not sell or in any way monetize user data.
|
||||
|
||||
**Operating our services**. For the purpose of operating our Services, you agree that your end-to-end encrypted messages are transferred via our servers in the United Kingdom, the United States and other countries where we have or use facilities and service providers or partners.
|
||||
|
||||
@@ -87,4 +93,4 @@ You accept to our Terms of Service ("Terms") by installing or using any of our a
|
||||
|
||||
**Ending these Terms**. You may end these Terms with SimpleX Chat at any time by deleting SimpleX Chat app(s) from your device and discontinuing use of our Services. The provisions related to Licenses, Disclaimers, Limitation of Liability, Resolving dispute, Availability, Changes to the terms, Enforcing the terms, and Ending these Terms will survive termination of your relationship with SimpleX Chat.
|
||||
|
||||
Updated March 1, 2022
|
||||
Updated November 8, 2022
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
|
||||
- 🖲 Protects your messages and metadata - who you talk to and when.
|
||||
- 🔐 Double ratchet end-to-end encryption, with additional encryption layer.
|
||||
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/website/raw/master/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
|
||||
- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084).
|
||||
- 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**!
|
||||
- 🖥 Available as a terminal (console) app / CLI on Linux, MacOS, Windows.
|
||||
|
||||
**NEW**: v4.0 is released - now local chat database is encrypted with passphrase! See [the release announcement](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
**NEW**: Security audit by [Trail of Bits](https://www.trailofbits.com/about), the [new website](https://simplex.chat) and v4.2 released! [See the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md)
|
||||
|
||||
## Contents
|
||||
|
||||
@@ -42,8 +42,10 @@
|
||||
- [Privacy: technical details and limitations](#privacy-technical-details-and-limitations)
|
||||
- [For developers](#for-developers)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Help us pay for 3rd party security audit](#help-us-pay-for-3rd-party-security-audit)
|
||||
- [Disclaimer, License](#disclaimer)
|
||||
- [Join a user group](#join-a-user-group)
|
||||
- [Contribute](#contribute)
|
||||
- [Help us with donations](#help-us-with-donations)
|
||||
- [Disclaimers, Security contact, License](#disclaimers)
|
||||
|
||||
## Why privacy matters
|
||||
|
||||
@@ -83,6 +85,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent updates:
|
||||
|
||||
[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)
|
||||
|
||||
[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)
|
||||
@@ -149,7 +153,6 @@ 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).
|
||||
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.
|
||||
4. Independent implementation audit.
|
||||
|
||||
## For developers
|
||||
|
||||
@@ -175,42 +178,69 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- ✅ Manual chat history deletion.
|
||||
- ✅ End-to-end encrypted WebRTC audio and video calls via the mobile apps.
|
||||
- ✅ Privacy preserving instant notifications for iOS using Apple Push Notification service.
|
||||
- ✅ Chat database export and import
|
||||
- ✅ Chat database export and import.
|
||||
- ✅ Chat groups in mobile apps.
|
||||
- ✅ Connecting to messaging servers via Tor.
|
||||
- ✅ Dual server addresses to access messaging servers as v3 hidden services.
|
||||
- ✅ Chat server and TypeScript client SDK to develop chat interfaces, integrations and chat bots (ready for announcement).
|
||||
- ✅ Incognito mode to share a new random name with each contact.
|
||||
- ✅ Chat database encryption.
|
||||
- 🏗 Automatic chat history deletion.
|
||||
- 🏗 SMP queue redundancy and rotation.
|
||||
- 🏗 Links to join groups and improve groups stability.
|
||||
- Feeds/broadcasts
|
||||
- Disappearing messages, with mutual agreement.
|
||||
- Voice messages
|
||||
- Video messages
|
||||
- ✅ Automatic chat history deletion.
|
||||
- ✅ Links to join groups and improve groups stability.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Voice messages (with recipient opt-out per contact).
|
||||
- 🏗 Basic authentication for SMP servers (to authorize creating new queues).
|
||||
- View deleted messages, full message deletion by sender (with recipient opt-in per contact).
|
||||
- Block screenshots and view in recent apps.
|
||||
- Optionally avoid re-using the same TCP session for multiple connections.
|
||||
- Access password/pin (with optional alternative access password).
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Media server to optimize sending large files to groups.
|
||||
- Video messages.
|
||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- Multiple user profiles in the same chat database.
|
||||
- Advanced server configuration.
|
||||
- Feeds/broadcasts.
|
||||
- Unconfirmed: disappearing messages (with recipient opt-in per-contact).
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- Message delivery confirmation.
|
||||
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
||||
- Supporting the same profile on multiple devices.
|
||||
- Desktop client.
|
||||
- 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.
|
||||
- Media server to optimize sending large files to groups.
|
||||
- Desktop client.
|
||||
- Using the same profile on multiple devices.
|
||||
|
||||
## Help us pay for 3rd party security audit
|
||||
## Join a user group
|
||||
|
||||
I will get straight to the point: I ask you to support SimpleX Chat with donations.
|
||||
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).
|
||||
|
||||
We are prioritizing users privacy and security - it would be impossible without your support we were lucky to have so far.
|
||||
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).
|
||||
|
||||
We are planning a 3rd party security audit for the app, and it would hugely help us if some part of this $20,000+ expense could be covered with donations.
|
||||
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!
|
||||
|
||||
## 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.
|
||||
|
||||
## Help us with donations
|
||||
|
||||
Huge thank you to everybody who donated to SimpleX Chat!
|
||||
|
||||
We are prioritizing users privacy and security - it would be impossible without your support.
|
||||
|
||||
Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure.
|
||||
|
||||
If you are already using SimpleX Chat, or plan to use it in the future when it has more features, please consider making a donation - it will help us to raise more funds. Donating any amount, even the price of the cup of coffee, would make a huge difference for us.
|
||||
Your donations help us raise more funds – any amount, even the price of the cup of coffee, would make a big difference for us.
|
||||
|
||||
It is possible to donate via:
|
||||
|
||||
@@ -218,6 +248,7 @@ It is possible to donate via:
|
||||
- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies.
|
||||
- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt
|
||||
- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG
|
||||
- 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,
|
||||
|
||||
@@ -225,11 +256,27 @@ Evgeny
|
||||
|
||||
SimpleX Chat founder
|
||||
|
||||
## Disclaimer
|
||||
## Disclaimers
|
||||
|
||||
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed and had many improvements in v1.0.0; we are currently arranging for the independent implementation audit.
|
||||
[SimpleX protocols and security model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) was reviewed, and had many breaking changes and improvements in v1.0.0.
|
||||
|
||||
You are likely to discover some bugs - we would really appreciate if you use it and let us know anything that needs to be fixed or improved.
|
||||
The security audit was performed in October 2022 by [Trail of Bits](https://www.trailofbits.com/about), and most fixes were released in v4.2.0 – see [the announcement](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
SimpleX Chat is still a relatively early stage platform (the mobile apps were released in March 2022), so you may discover some bugs and missing features. We would really appreciate if you let us know anything that needs to be fixed or improved.
|
||||
|
||||
The default servers configured in the app are provided on the best effort basis. We are currently not guaranteeing any SLAs, although historically our servers had over 99.9% uptime each.
|
||||
|
||||
We have never provided or have been requested access to our servers or any information from our servers by any third parties. If we are ever requested to provide such access or information, we will be following due legal process.
|
||||
|
||||
We do not log IP addresses of the users and we do not perform any traffic correlation on our servers. If transport level security is critical you must use Tor or some other similar network to access messaging servers. We will be improving the client applications to reduce the opportunities for traffic correlation.
|
||||
|
||||
Please read more in [Terms & privacy policy](./PRIVACY.md).
|
||||
|
||||
## Security contact
|
||||
|
||||
To report a security vulnerability, please send us email to chat@simplex.chat. We will coordinate the fix and disclosure. Please do NOT report security vulnerabilities via GitHub issues.
|
||||
|
||||
Please treat any findings of possible traffic correlation attacks allowing to correlate two different conversations to the same user, other than covered in [the threat model](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#threat-model), as security vulnerabilities, and follow this disclosure process.
|
||||
|
||||
## License
|
||||
|
||||
@@ -243,4 +290,4 @@ You are likely to discover some bugs - we would really appreciate if you use it
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/website/raw/master/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
/.idea/deploymentTargetDropDown.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/uiDesigner.xml
|
||||
/.idea/kotlinc.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 29
|
||||
targetSdk 32
|
||||
versionCode 61
|
||||
versionName "4.1"
|
||||
versionCode 70
|
||||
versionName "4.2.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
ndk {
|
||||
|
||||
@@ -69,6 +69,11 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity-alias
|
||||
|
||||
@@ -14,7 +14,7 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Replay
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -33,8 +33,7 @@ import chat.simplex.app.views.chat.ChatView
|
||||
import chat.simplex.app.views.chatlist.*
|
||||
import chat.simplex.app.views.database.DatabaseErrorView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.connectViaUri
|
||||
import chat.simplex.app.views.newchat.withUriAction
|
||||
import chat.simplex.app.views.newchat.*
|
||||
import chat.simplex.app.views.onboarding.*
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@@ -65,6 +64,7 @@ class MainActivity: FragmentActivity() {
|
||||
// Only needed to be processed on first creation of activity
|
||||
if (savedInstanceState == null) {
|
||||
processNotificationIntent(intent, m)
|
||||
processIntent(intent, m)
|
||||
processExternalIntent(intent, m)
|
||||
}
|
||||
setContent {
|
||||
@@ -98,11 +98,21 @@ class MainActivity: FragmentActivity() {
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val enteredBackgroundVal = enteredBackground.value
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30 * 1e+3) {
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
/**
|
||||
* When new activity is created after a click on notification, the old one receives onPause before
|
||||
* recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent
|
||||
* unwanted multiple auth dialogs from [runAuthenticate]
|
||||
* */
|
||||
enteredBackground.value = elapsedRealtime()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
enteredBackground.value = elapsedRealtime()
|
||||
@@ -134,17 +144,10 @@ class MainActivity: FragmentActivity() {
|
||||
this@MainActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
LAResult.Success ->
|
||||
userAuthorized.value = true
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
is LAResult.Error, LAResult.Failed ->
|
||||
laFailed.value = true
|
||||
laErrorToast(applicationContext, laResult.errString)
|
||||
}
|
||||
LAResult.Failed -> {
|
||||
laFailed.value = true
|
||||
laFailedToast(applicationContext)
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
userAuthorized.value = true
|
||||
m.performLA.value = false
|
||||
@@ -180,15 +183,9 @@ class MainActivity: FragmentActivity() {
|
||||
prefPerformLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laErrorToast(applicationContext, laResult.errString)
|
||||
}
|
||||
LAResult.Failed -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laFailedToast(applicationContext)
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
@@ -213,15 +210,9 @@ class MainActivity: FragmentActivity() {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
laErrorToast(applicationContext, laResult.errString)
|
||||
}
|
||||
LAResult.Failed -> {
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
laFailedToast(applicationContext)
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
@@ -288,14 +279,14 @@ fun MainPage(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun retryAuthView() {
|
||||
fun authView() {
|
||||
Box(
|
||||
Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.auth_retry),
|
||||
icon = Icons.Outlined.Replay,
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
laFailed.value = false
|
||||
runAuthenticate()
|
||||
@@ -316,7 +307,7 @@ fun MainPage(
|
||||
onboarding == null || userCreated == null -> SplashView()
|
||||
!chatsAccessAuthorized -> {
|
||||
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
|
||||
retryAuthView()
|
||||
authView()
|
||||
} else {
|
||||
SplashView()
|
||||
}
|
||||
@@ -398,13 +389,24 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
chatModel.sharedContent.value = SharedContent.Text(it)
|
||||
}
|
||||
intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Image(it)
|
||||
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(it))
|
||||
} // All other mime types
|
||||
else -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.File(it)
|
||||
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
// Close active chat and show a list of chats
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
when {
|
||||
intent.type?.startsWith("image/") == true -> (intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
|
||||
} // All other mime types
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,23 +416,23 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
// TODO open from chat list view
|
||||
chatModel.appOpenUrl.value = uri
|
||||
} else {
|
||||
withUriAction(uri) { action ->
|
||||
val title = when (action) {
|
||||
"contact" -> generalGetString(R.string.connect_via_contact_link)
|
||||
"invitation" -> generalGetString(R.string.connect_via_invitation_link)
|
||||
else -> {
|
||||
Log.e(TAG, "URI has unexpected action. Alert shown.")
|
||||
action
|
||||
}
|
||||
withUriAction(uri) { linkType ->
|
||||
val title = when (linkType) {
|
||||
ConnectionLinkType.CONTACT -> generalGetString(R.string.connect_via_contact_link)
|
||||
ConnectionLinkType.INVITATION -> generalGetString(R.string.connect_via_invitation_link)
|
||||
ConnectionLinkType.GROUP -> generalGetString(R.string.connect_via_group_link)
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = title,
|
||||
text = generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
|
||||
text = if (linkType == ConnectionLinkType.GROUP)
|
||||
generalGetString(R.string.you_will_join_group)
|
||||
else
|
||||
generalGetString(R.string.profile_will_be_sent_to_contact_sending_link),
|
||||
confirmText = generalGetString(R.string.connect_via_link_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: connecting")
|
||||
connectViaUri(chatModel, action, uri)
|
||||
connectViaUri(chatModel, linkType, uri)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ class ChatModel(val controller: ChatController) {
|
||||
val groupMembers = mutableStateListOf<GroupMember>()
|
||||
|
||||
val terminalItems = mutableStateListOf<TerminalItem>()
|
||||
val userAddress = mutableStateOf<String?>(null)
|
||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||
val userSMPServers = mutableStateOf<(List<String>)?>(null)
|
||||
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
||||
|
||||
@@ -90,7 +90,7 @@ class ChatModel(val controller: ChatController) {
|
||||
|
||||
fun updateContactConnection(contactConnection: PendingContactConnection) = updateChat(ChatInfo.ContactConnection(contactConnection))
|
||||
|
||||
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = !contact.isIndirectContact)
|
||||
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = !contact.isIndirectContact && !contact.viaGroupLink)
|
||||
|
||||
fun updateGroup(groupInfo: GroupInfo) = updateChat(ChatInfo.Group(groupInfo))
|
||||
|
||||
@@ -307,6 +307,11 @@ class ChatModel(val controller: ChatController) {
|
||||
}
|
||||
|
||||
fun upsertGroupMember(groupInfo: GroupInfo, member: GroupMember): Boolean {
|
||||
// user member was updated
|
||||
if (groupInfo.membership.groupMemberId == member.groupMemberId) {
|
||||
updateGroup(groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
return if (chatId.value == groupInfo.id) {
|
||||
val memberIndex = groupMembers.indexOfFirst { it.id == member.id }
|
||||
@@ -387,7 +392,7 @@ data class Chat (
|
||||
val id: String get() = chatInfo.id
|
||||
|
||||
@Serializable
|
||||
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0)
|
||||
data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false)
|
||||
|
||||
@Serializable
|
||||
data class ServerInfo(val networkStatus: NetworkStatus)
|
||||
@@ -426,7 +431,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
abstract val incognito: Boolean
|
||||
|
||||
@Serializable @SerialName("direct")
|
||||
class Direct(val contact: Contact): ChatInfo() {
|
||||
data class Direct(val contact: Contact): ChatInfo() {
|
||||
override val chatType get() = ChatType.Direct
|
||||
override val localDisplayName get() = contact.localDisplayName
|
||||
override val id get() = contact.id
|
||||
@@ -448,7 +453,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
||||
}
|
||||
|
||||
@Serializable @SerialName("group")
|
||||
class Group(val groupInfo: GroupInfo): ChatInfo() {
|
||||
data class Group(val groupInfo: GroupInfo): ChatInfo() {
|
||||
override val chatType get() = ChatType.Group
|
||||
override val localDisplayName get() = groupInfo.localDisplayName
|
||||
override val id get() = groupInfo.id
|
||||
@@ -523,6 +528,8 @@ data class Contact(
|
||||
val activeConn: Connection,
|
||||
val viaGroup: Long? = null,
|
||||
val chatSettings: ChatSettings,
|
||||
// User applies his preferences for the contact here. Named user_preferences on the contact in DB
|
||||
val userPreferences: ChatPreferences,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -540,6 +547,9 @@ data class Contact(
|
||||
val isIndirectContact: Boolean get() =
|
||||
activeConn.connLevel > 0 || viaGroup != null
|
||||
|
||||
val viaGroupLink: Boolean get() =
|
||||
activeConn.viaGroupLink
|
||||
|
||||
val contactConnIncognito =
|
||||
activeConn.customUserProfileId != null
|
||||
|
||||
@@ -550,6 +560,7 @@ data class Contact(
|
||||
profile = LocalProfile.sampleData,
|
||||
activeConn = Connection.sampleData,
|
||||
chatSettings = ChatSettings(true),
|
||||
userPreferences = ChatPreferences(),
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
)
|
||||
@@ -571,10 +582,10 @@ class ContactSubStatus(
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val customUserProfileId: Long? = null) {
|
||||
class Connection(val connId: Long, val connStatus: ConnStatus, val connLevel: Int, val viaGroupLink: Boolean, val customUserProfileId: Long? = null) {
|
||||
val id: ChatId get() = ":$connId"
|
||||
companion object {
|
||||
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, customUserProfileId = null)
|
||||
val sampleData = Connection(connId = 1, connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,7 +594,9 @@ class Profile(
|
||||
override val displayName: String,
|
||||
override val fullName: String,
|
||||
override val image: String? = null,
|
||||
override val localAlias : String = ""
|
||||
override val localAlias : String = "",
|
||||
// Contact applies his preferences here
|
||||
val preferences: ChatPreferences? = null
|
||||
): NamedChat {
|
||||
val profileViewName: String
|
||||
get() {
|
||||
@@ -607,6 +620,8 @@ class LocalProfile(
|
||||
override val fullName: String,
|
||||
override val image: String? = null,
|
||||
override val localAlias: String,
|
||||
// Contact applies his preferences here
|
||||
val preferences: ChatPreferences? = null
|
||||
): NamedChat {
|
||||
val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" }
|
||||
|
||||
@@ -636,6 +651,7 @@ data class GroupInfo (
|
||||
val membership: GroupMember,
|
||||
val hostConnCustomUserProfileId: Long? = null,
|
||||
val chatSettings: ChatSettings,
|
||||
// val groupPreferences: GroupPreferences? = null,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
): SomeChat, NamedChat {
|
||||
@@ -689,7 +705,7 @@ class GroupProfile (
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class GroupMember (
|
||||
data class GroupMember (
|
||||
val groupMemberId: Long,
|
||||
val groupId: Long,
|
||||
val memberId: String,
|
||||
@@ -771,6 +787,12 @@ class GroupMember (
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class GroupMemberRef(
|
||||
val groupMemberId: Long,
|
||||
val profile: Profile
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class GroupMemberRole(val memberRole: String) {
|
||||
@SerialName("member") Member("member"), // order matters in comparisons
|
||||
@@ -902,6 +924,7 @@ class PendingContactConnection(
|
||||
val pccAgentConnId: String,
|
||||
val pccConnStatus: ConnStatus,
|
||||
val viaContactUri: Boolean,
|
||||
val groupLinkId: String? = null,
|
||||
val customUserProfileId: Long? = null,
|
||||
val connReqInv: String? = null,
|
||||
override val localAlias: String,
|
||||
@@ -940,8 +963,11 @@ class PendingContactConnection(
|
||||
return if (initiated == null) "" else generalGetString(
|
||||
if (initiated && !viaContactUri)
|
||||
if (incognito) R.string.description_you_shared_one_time_link_incognito else R.string.description_you_shared_one_time_link
|
||||
else if (viaContactUri )
|
||||
if (incognito) R.string.description_via_contact_address_link_incognito else R.string.description_via_contact_address_link
|
||||
else if (viaContactUri)
|
||||
if (groupLinkId != null)
|
||||
if (incognito) R.string.description_via_group_link_incognito else R.string.description_via_group_link
|
||||
else
|
||||
if (incognito) R.string.description_via_contact_address_link_incognito else R.string.description_via_contact_address_link
|
||||
else
|
||||
if (incognito) R.string.description_via_one_time_link_incognito else R.string.description_via_one_time_link
|
||||
)
|
||||
@@ -1040,6 +1066,7 @@ data class ChatItem (
|
||||
is RcvGroupEvent.MemberRole -> true
|
||||
is RcvGroupEvent.UserRole -> false
|
||||
is RcvGroupEvent.MemberDeleted -> false
|
||||
is RcvGroupEvent.InvitedViaGroupLink -> false
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> true
|
||||
else -> false
|
||||
@@ -1209,6 +1236,8 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupEvent") class SndGroupEventContent(val sndGroupEvent: SndGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvConnEvent") class RcvConnEventContent(val rcvConnEvent: RcvConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndConnEvent") class SndConnEventContent(val sndConnEvent: SndConnEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
|
||||
override val text: String get() = when(this) {
|
||||
is SndMsgContent -> msgContent.text
|
||||
@@ -1222,6 +1251,8 @@ sealed class CIContent: ItemContent {
|
||||
is SndGroupInvitation -> groupInvitation.text
|
||||
is RcvGroupEventContent -> rcvGroupEvent.text
|
||||
is SndGroupEventContent -> sndGroupEvent.text
|
||||
is RcvConnEventContent -> rcvConnEvent.text
|
||||
is SndConnEventContent -> sndConnEvent.text
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1547,17 +1578,19 @@ sealed class RcvGroupEvent() {
|
||||
@Serializable @SerialName("userDeleted") class UserDeleted(): RcvGroupEvent()
|
||||
@Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent()
|
||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent()
|
||||
@Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is MemberAdded -> String.format(generalGetString(R.string.rcv_group_event_member_added), profile.profileViewName)
|
||||
is MemberConnected -> generalGetString(R.string.rcv_group_event_member_connected)
|
||||
is MemberLeft -> generalGetString(R.string.rcv_group_event_member_left)
|
||||
is MemberRole -> String.format(generalGetString(R.string.member_role), profile.profileViewName, role.text)
|
||||
is UserRole -> String.format(generalGetString(R.string.your_member_role), role.text)
|
||||
is MemberRole -> String.format(generalGetString(R.string.rcv_group_event_changed_member_role), profile.profileViewName, role.text)
|
||||
is UserRole -> String.format(generalGetString(R.string.rcv_group_event_changed_your_role), role.text)
|
||||
is MemberDeleted -> String.format(generalGetString(R.string.rcv_group_event_member_deleted), profile.profileViewName)
|
||||
is UserDeleted -> generalGetString(R.string.rcv_group_event_user_deleted)
|
||||
is GroupDeleted -> generalGetString(R.string.rcv_group_event_group_deleted)
|
||||
is GroupUpdated -> generalGetString(R.string.rcv_group_event_updated_group_profile)
|
||||
is InvitedViaGroupLink -> generalGetString(R.string.rcv_group_event_invited_via_your_group_link)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1570,14 +1603,54 @@ sealed class SndGroupEvent() {
|
||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is MemberRole -> String.format(generalGetString(R.string.member_role), profile.profileViewName, role.text)
|
||||
is UserRole -> String.format(generalGetString(R.string.your_member_role), role.text)
|
||||
is MemberRole -> String.format(generalGetString(R.string.snd_group_event_changed_member_role), profile.profileViewName, role.text)
|
||||
is UserRole -> String.format(generalGetString(R.string.snd_group_event_changed_role_for_yourself), role.text)
|
||||
is MemberDeleted -> String.format(generalGetString(R.string.snd_group_event_member_deleted), profile.profileViewName)
|
||||
is UserLeft -> generalGetString(R.string.snd_group_event_user_left)
|
||||
is GroupUpdated -> generalGetString(R.string.snd_group_event_group_profile_updated)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class RcvConnEvent {
|
||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase): RcvConnEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is SwitchQueue -> when (phase) {
|
||||
SwitchPhase.Completed -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_completed)
|
||||
else -> generalGetString(R.string.rcv_conn_event_switch_queue_phase_changing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class SndConnEvent {
|
||||
@Serializable @SerialName("switchQueue") class SwitchQueue(val phase: SwitchPhase, val member: GroupMemberRef? = null): SndConnEvent()
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
is SwitchQueue -> {
|
||||
member?.profile?.profileViewName?.let {
|
||||
return when (phase) {
|
||||
SwitchPhase.Completed -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_completed_for_member), it)
|
||||
else -> String.format(generalGetString(R.string.snd_conn_event_switch_queue_phase_changing_for_member), it)
|
||||
}
|
||||
}
|
||||
when (phase) {
|
||||
SwitchPhase.Completed -> generalGetString(R.string.snd_conn_event_switch_queue_phase_completed)
|
||||
else -> generalGetString(R.string.snd_conn_event_switch_queue_phase_changing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class SwitchPhase {
|
||||
@SerialName("started") Started,
|
||||
@SerialName("confirmed") Confirmed,
|
||||
@SerialName("completed") Completed
|
||||
}
|
||||
|
||||
sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
|
||||
object Day: ChatItemTTL()
|
||||
object Week: ChatItemTTL()
|
||||
|
||||
@@ -89,6 +89,7 @@ class AppPreferences(val context: Context) {
|
||||
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
|
||||
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
|
||||
val privacyAcceptImages = mkBoolPreference(SHARED_PREFS_PRIVACY_ACCEPT_IMAGES, true)
|
||||
val privacyTransferImagesInline = mkBoolPreference(SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE, false)
|
||||
val privacyLinkPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_LINK_PREVIEWS, true)
|
||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||
@@ -178,6 +179,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
|
||||
private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers"
|
||||
private const val SHARED_PREFS_PRIVACY_ACCEPT_IMAGES = "PrivacyAcceptImages"
|
||||
private const val SHARED_PREFS_PRIVACY_TRANSFER_IMAGES_INLINE = "PrivacyTransferImagesInline"
|
||||
private const val SHARED_PREFS_PRIVACY_LINK_PREVIEWS = "PrivacyLinkPreviews"
|
||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
|
||||
@@ -242,6 +244,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
|
||||
chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now())
|
||||
chatModel.chatRunning.value = true
|
||||
chatModel.appOpenUrl.value?.let {
|
||||
chatModel.appOpenUrl.value = null
|
||||
connectIfOpenedViaUri(it, chatModel)
|
||||
}
|
||||
startReceiver()
|
||||
Log.d(TAG, "startChat: started")
|
||||
} else {
|
||||
@@ -258,8 +264,20 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
private fun startReceiver() {
|
||||
Log.d(TAG, "ChatController startReceiver")
|
||||
if (receiverStarted) return
|
||||
thread(name="receiver") {
|
||||
GlobalScope.launch { withContext(Dispatchers.IO) { recvMspLoop() } }
|
||||
receiverStarted = true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
while (true) {
|
||||
/** Global [ctrl] can be null. It's needed for having the same [ChatModel] that already made in [ChatController] without the need
|
||||
* to change it everywhere in code after changing a database.
|
||||
* Since it can be changed in background thread, making this check to prevent NullPointerException */
|
||||
val ctrl = ctrl
|
||||
if (ctrl == null) {
|
||||
receiverStarted = false
|
||||
break
|
||||
}
|
||||
val msg = recvMsg(ctrl)
|
||||
if (msg != null) processReceivedMsg(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,26 +303,18 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun recvMsg(ctrl: ChatCtrl): CR? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
|
||||
if (json == "") {
|
||||
null
|
||||
} else {
|
||||
val r = APIResponse.decodeStr(json).resp
|
||||
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
|
||||
if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json")
|
||||
r
|
||||
}
|
||||
private fun recvMsg(ctrl: ChatCtrl): CR? {
|
||||
val json = chatRecvMsgWait(ctrl, MESSAGE_TIMEOUT)
|
||||
return if (json == "") {
|
||||
null
|
||||
} else {
|
||||
val r = APIResponse.decodeStr(json).resp
|
||||
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
|
||||
if (r is CR.Response || r is CR.Invalid) Log.d(TAG, "chatRecvMsg json: $json")
|
||||
r
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun recvMspLoop() {
|
||||
val msg = recvMsg(ctrl ?: return)
|
||||
if (msg != null) processReceivedMsg(msg)
|
||||
recvMspLoop()
|
||||
}
|
||||
|
||||
suspend fun apiGetActiveUser(): User? {
|
||||
val r = sendCmd(CC.ShowActiveUser())
|
||||
if (r is CR.ActiveUser) return r.user
|
||||
@@ -496,6 +506,24 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSwitchContact(contactId: Long) {
|
||||
return when (val r = sendCmd(CC.APISwitchContact(contactId))) {
|
||||
is CR.CmdOk -> {}
|
||||
else -> {
|
||||
apiErrorAlert("apiSwitchContact", generalGetString(R.string.connection_error), r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiSwitchGroupMember(groupId: Long, groupMemberId: Long) {
|
||||
return when (val r = sendCmd(CC.APISwitchGroupMember(groupId, groupMemberId))) {
|
||||
is CR.CmdOk -> {}
|
||||
else -> {
|
||||
apiErrorAlert("apiSwitchGroupMember", generalGetString(R.string.error_changing_address), r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiAddContact(): String? {
|
||||
val r = sendCmd(CC.AddContact())
|
||||
return when (r) {
|
||||
@@ -552,15 +580,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
r is CR.ContactDeleted && type == ChatType.Direct -> return true
|
||||
r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> return true
|
||||
r is CR.GroupDeletedUser && type == ChatType.Group -> return true
|
||||
r is CR.ChatCmdError -> {
|
||||
val e = r.chatError
|
||||
if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.cannot_delete_contact),
|
||||
String.format(generalGetString(R.string.contact_cannot_be_deleted_as_they_are_in_groups), e.errorType.contact.displayName, e.errorType.groupNames.joinToString(", "))
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val titleId = when (type) {
|
||||
ChatType.Direct -> R.string.error_deleting_contact
|
||||
@@ -617,6 +636,13 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiSetContactPrefs(contactId: Long, prefs: ChatPreferences): Contact? {
|
||||
val r = sendCmd(CC.ApiSetContactPrefs(contactId, prefs))
|
||||
if (r is CR.ContactPrefsUpdated) return r.toContact
|
||||
Log.e(TAG, "apiSetContactPrefs bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiCreateUserAddress(): String? {
|
||||
val r = sendCmd(CC.CreateMyAddress())
|
||||
return when (r) {
|
||||
@@ -637,9 +663,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return false
|
||||
}
|
||||
|
||||
private suspend fun apiGetUserAddress(): String? {
|
||||
private suspend fun apiGetUserAddress(): UserContactLinkRec? {
|
||||
val r = sendCmd(CC.ShowMyAddress())
|
||||
if (r is CR.UserContactLink) return r.connReqContact
|
||||
if (r is CR.UserContactLink) return r.contactLink
|
||||
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
|
||||
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
|
||||
return null
|
||||
@@ -648,6 +674,17 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun userAddressAutoAccept(autoAccept: AutoAccept?): UserContactLinkRec? {
|
||||
val r = sendCmd(CC.AddressAutoAccept(autoAccept))
|
||||
if (r is CR.UserContactLinkUpdated) return r.contactLink
|
||||
if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore
|
||||
&& r.chatError.storeError is StoreError.UserContactLinkNotFound) {
|
||||
return null
|
||||
}
|
||||
Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? {
|
||||
val r = sendCmd(CC.ApiAcceptContact(contactReqId))
|
||||
return when {
|
||||
@@ -723,8 +760,15 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(fileId: Long): AChatItem? {
|
||||
val r = sendCmd(CC.ReceiveFile(fileId))
|
||||
suspend fun apiChatUnread(type: ChatType, id: Long, unreadChat: Boolean): Boolean {
|
||||
val r = sendCmd(CC.ApiChatUnread(type, id, unreadChat))
|
||||
if (r is CR.CmdOk) return true
|
||||
Log.e(TAG, "apiChatUnread bad response: ${r.responseType} ${r.details}")
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun apiReceiveFile(fileId: Long, inline: Boolean): AChatItem? {
|
||||
val r = sendCmd(CC.ReceiveFile(fileId, inline))
|
||||
return when (r) {
|
||||
is CR.RcvFileAccepted -> r.chatItem
|
||||
is CR.RcvFileAcceptedSndCancelled -> {
|
||||
@@ -839,6 +883,40 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiCreateGroupLink(groupId: Long): String? {
|
||||
return when (val r = sendCmd(CC.APICreateGroupLink(groupId))) {
|
||||
is CR.GroupLinkCreated -> r.connReqContact
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiCreateGroupLink", generalGetString(R.string.error_creating_link_for_group), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiDeleteGroupLink(groupId: Long): Boolean {
|
||||
return when (val r = sendCmd(CC.APIDeleteGroupLink(groupId))) {
|
||||
is CR.GroupLinkDeleted -> true
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiDeleteGroupLink", generalGetString(R.string.error_deleting_link_for_group), r)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiGetGroupLink(groupId: Long): String? {
|
||||
return when (val r = sendCmd(CC.APIGetGroupLink(groupId))) {
|
||||
is CR.GroupLink -> r.connReqContact
|
||||
else -> {
|
||||
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun networkErrorAlert(r: CR): Boolean {
|
||||
return when {
|
||||
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
|
||||
@@ -880,16 +958,20 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
chatModel.removeChat(r.connection.id)
|
||||
}
|
||||
is CR.ContactConnected -> {
|
||||
chatModel.updateContact(r.contact)
|
||||
chatModel.dismissConnReqView(r.contact.activeConn.id)
|
||||
chatModel.removeChat(r.contact.activeConn.id)
|
||||
chatModel.updateNetworkStatus(r.contact.id, Chat.NetworkStatus.Connected())
|
||||
ntfManager.notifyContactConnected(r.contact)
|
||||
if (!r.contact.viaGroupLink) {
|
||||
chatModel.updateContact(r.contact)
|
||||
chatModel.dismissConnReqView(r.contact.activeConn.id)
|
||||
chatModel.removeChat(r.contact.activeConn.id)
|
||||
chatModel.updateNetworkStatus(r.contact.id, Chat.NetworkStatus.Connected())
|
||||
ntfManager.notifyContactConnected(r.contact)
|
||||
}
|
||||
}
|
||||
is CR.ContactConnecting -> {
|
||||
chatModel.updateContact(r.contact)
|
||||
chatModel.dismissConnReqView(r.contact.activeConn.id)
|
||||
chatModel.removeChat(r.contact.activeConn.id)
|
||||
if (!r.contact.viaGroupLink) {
|
||||
chatModel.updateContact(r.contact)
|
||||
chatModel.dismissConnReqView(r.contact.activeConn.id)
|
||||
chatModel.removeChat(r.contact.activeConn.id)
|
||||
}
|
||||
}
|
||||
is CR.ReceivedContactRequest -> {
|
||||
val contactRequest = r.contactRequest
|
||||
@@ -903,6 +985,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
chatModel.updateChatInfo(cInfo)
|
||||
}
|
||||
}
|
||||
is CR.ContactsMerged -> {
|
||||
if (chatModel.hasChat(r.mergedContact.id)) {
|
||||
if (chatModel.chatId.value == r.mergedContact.id) {
|
||||
chatModel.chatId.value = r.intoContact.id
|
||||
}
|
||||
chatModel.removeChat(r.mergedContact.id)
|
||||
}
|
||||
}
|
||||
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Connected())
|
||||
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, Chat.NetworkStatus.Disconnected())
|
||||
is CR.ContactSubError -> processContactSubError(r.contact, r.chatError)
|
||||
@@ -953,9 +1043,16 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
}
|
||||
is CR.ReceivedGroupInvitation -> {
|
||||
chatModel.addChat(Chat(chatInfo = ChatInfo.Group(r.groupInfo), chatItems = listOf()))
|
||||
chatModel.updateGroup(r.groupInfo) // update so that repeat group invitations are not duplicated
|
||||
// TODO NtfManager.shared.notifyGroupInvitation
|
||||
}
|
||||
is CR.UserAcceptedGroupSent -> {
|
||||
chatModel.updateGroup(r.groupInfo)
|
||||
if (r.hostContact != null) {
|
||||
chatModel.dismissConnReqView(r.hostContact.activeConn.id)
|
||||
chatModel.removeChat(r.hostContact.activeConn.id)
|
||||
}
|
||||
}
|
||||
is CR.JoinedGroupMemberConnecting ->
|
||||
chatModel.upsertGroupMember(r.groupInfo, r.member)
|
||||
is CR.DeletedMemberUser -> // TODO update user member
|
||||
@@ -1056,7 +1153,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
}
|
||||
|
||||
suspend fun receiveFile(fileId: Long) {
|
||||
val chatItem = apiReceiveFile(fileId)
|
||||
val inline = appPrefs.privacyTransferImagesInline.get()
|
||||
val chatItem = apiReceiveFile(fileId, inline)
|
||||
if (chatItem != null) {
|
||||
chatItemSimpleUpdate(chatItem)
|
||||
}
|
||||
@@ -1253,15 +1351,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
|
||||
appPrefs.performLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
chatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laErrorToast(appContext, laResult.errString)
|
||||
}
|
||||
LAResult.Failed -> {
|
||||
chatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laFailedToast(appContext)
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
chatModel.performLA.value = false
|
||||
@@ -1366,6 +1458,9 @@ sealed class CC {
|
||||
class ApiLeaveGroup(val groupId: Long): CC()
|
||||
class ApiListMembers(val groupId: Long): CC()
|
||||
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
|
||||
class APICreateGroupLink(val groupId: Long): CC()
|
||||
class APIDeleteGroupLink(val groupId: Long): CC()
|
||||
class APIGetGroupLink(val groupId: Long): CC()
|
||||
class GetUserSMPServers: CC()
|
||||
class SetUserSMPServers(val smpServers: List<String>): CC()
|
||||
class APISetChatItemTTL(val seconds: Long?): CC()
|
||||
@@ -1375,6 +1470,8 @@ sealed class CC {
|
||||
class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC()
|
||||
class APIContactInfo(val contactId: Long): CC()
|
||||
class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APISwitchContact(val contactId: Long): CC()
|
||||
class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class AddContact: CC()
|
||||
class Connect(val connReq: String): CC()
|
||||
class ApiDeleteChat(val type: ChatType, val id: Long): CC()
|
||||
@@ -1384,9 +1481,11 @@ sealed class CC {
|
||||
class ApiParseMarkdown(val text: String): CC()
|
||||
class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC()
|
||||
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
|
||||
class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC()
|
||||
class CreateMyAddress: CC()
|
||||
class DeleteMyAddress: CC()
|
||||
class ShowMyAddress: CC()
|
||||
class AddressAutoAccept(val autoAccept: AutoAccept?): CC()
|
||||
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
|
||||
class ApiRejectCall(val contact: Contact): CC()
|
||||
class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC()
|
||||
@@ -1397,7 +1496,8 @@ sealed class CC {
|
||||
class ApiAcceptContact(val contactReqId: Long): CC()
|
||||
class ApiRejectContact(val contactReqId: Long): CC()
|
||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||
class ReceiveFile(val fileId: Long): CC()
|
||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||
class ReceiveFile(val fileId: Long, val inline: Boolean): CC()
|
||||
|
||||
val cmdString: String get() = when (this) {
|
||||
is Console -> cmd
|
||||
@@ -1424,6 +1524,9 @@ sealed class CC {
|
||||
is ApiLeaveGroup -> "/_leave #$groupId"
|
||||
is ApiListMembers -> "/_members #$groupId"
|
||||
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}"
|
||||
is APICreateGroupLink -> "/_create link #$groupId"
|
||||
is APIDeleteGroupLink -> "/_delete link #$groupId"
|
||||
is APIGetGroupLink -> "/_get link #$groupId"
|
||||
is GetUserSMPServers -> "/smp_servers"
|
||||
is SetUserSMPServers -> "/smp_servers ${smpServersStr(smpServers)}"
|
||||
is APISetChatItemTTL -> "/_ttl ${chatItemTTLStr(seconds)}"
|
||||
@@ -1433,6 +1536,8 @@ sealed class CC {
|
||||
is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}"
|
||||
is APIContactInfo -> "/_info @$contactId"
|
||||
is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId"
|
||||
is APISwitchContact -> "/_switch @$contactId"
|
||||
is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId"
|
||||
is AddContact -> "/connect"
|
||||
is Connect -> "/connect $connReq"
|
||||
is ApiDeleteChat -> "/_delete ${chatRef(type, id)}"
|
||||
@@ -1442,9 +1547,11 @@ sealed class CC {
|
||||
is ApiParseMarkdown -> "/_parse $text"
|
||||
is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}"
|
||||
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
|
||||
is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}"
|
||||
is CreateMyAddress -> "/address"
|
||||
is DeleteMyAddress -> "/delete_address"
|
||||
is ShowMyAddress -> "/show_address"
|
||||
is AddressAutoAccept -> "/auto_accept ${AutoAccept.cmdString(autoAccept)}"
|
||||
is ApiAcceptContact -> "/_accept $contactReqId"
|
||||
is ApiRejectContact -> "/_reject $contactReqId"
|
||||
is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}"
|
||||
@@ -1455,7 +1562,8 @@ sealed class CC {
|
||||
is ApiEndCall -> "/_call end @${contact.apiId}"
|
||||
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
|
||||
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
|
||||
is ReceiveFile -> "/freceive $fileId"
|
||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||
is ReceiveFile -> "/freceive $fileId inline=${onOff(inline)}"
|
||||
}
|
||||
|
||||
val cmdType: String get() = when (this) {
|
||||
@@ -1483,6 +1591,9 @@ sealed class CC {
|
||||
is ApiLeaveGroup -> "apiLeaveGroup"
|
||||
is ApiListMembers -> "apiListMembers"
|
||||
is ApiUpdateGroupProfile -> "apiUpdateGroupProfile"
|
||||
is APICreateGroupLink -> "apiCreateGroupLink"
|
||||
is APIDeleteGroupLink -> "apiDeleteGroupLink"
|
||||
is APIGetGroupLink -> "apiGetGroupLink"
|
||||
is GetUserSMPServers -> "getUserSMPServers"
|
||||
is SetUserSMPServers -> "setUserSMPServers"
|
||||
is APISetChatItemTTL -> "apiSetChatItemTTL"
|
||||
@@ -1492,6 +1603,8 @@ sealed class CC {
|
||||
is APISetChatSettings -> "/apiSetChatSettings"
|
||||
is APIContactInfo -> "apiContactInfo"
|
||||
is APIGroupMemberInfo -> "apiGroupMemberInfo"
|
||||
is APISwitchContact -> "apiSwitchContact"
|
||||
is APISwitchGroupMember -> "apiSwitchGroupMember"
|
||||
is AddContact -> "addContact"
|
||||
is Connect -> "connect"
|
||||
is ApiDeleteChat -> "apiDeleteChat"
|
||||
@@ -1501,9 +1614,11 @@ sealed class CC {
|
||||
is ApiParseMarkdown -> "apiParseMarkdown"
|
||||
is ApiSetContactAlias -> "apiSetContactAlias"
|
||||
is ApiSetConnectionAlias -> "apiSetConnectionAlias"
|
||||
is ApiSetContactPrefs -> "apiSetContactPrefs"
|
||||
is CreateMyAddress -> "createMyAddress"
|
||||
is DeleteMyAddress -> "deleteMyAddress"
|
||||
is ShowMyAddress -> "showMyAddress"
|
||||
is AddressAutoAccept -> "addressAutoAccept"
|
||||
is ApiAcceptContact -> "apiAcceptContact"
|
||||
is ApiRejectContact -> "apiRejectContact"
|
||||
is ApiSendCallInvitation -> "apiSendCallInvitation"
|
||||
@@ -1514,6 +1629,7 @@ sealed class CC {
|
||||
is ApiEndCall -> "apiEndCall"
|
||||
is ApiCallStatus -> "apiCallStatus"
|
||||
is ApiChatRead -> "apiChatRead"
|
||||
is ApiChatUnread -> "apiChatUnread"
|
||||
is ReceiveFile -> "receiveFile"
|
||||
}
|
||||
|
||||
@@ -1537,7 +1653,7 @@ sealed class CC {
|
||||
companion object {
|
||||
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
|
||||
|
||||
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ",")
|
||||
fun smpServersStr(smpServers: List<String>) = if (smpServers.isEmpty()) "default" else smpServers.joinToString(separator = ";")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1585,8 +1701,8 @@ data class NetCfg(
|
||||
val defaults: NetCfg =
|
||||
NetCfg(
|
||||
socksProxy = null,
|
||||
tcpConnectTimeout = 7_500_000,
|
||||
tcpTimeout = 5_000_000,
|
||||
tcpConnectTimeout = 10_000_000,
|
||||
tcpTimeout = 7_000_000,
|
||||
tcpKeepAlive = KeepAliveOpts.defaults,
|
||||
smpPingInterval = 600_000_000
|
||||
)
|
||||
@@ -1594,8 +1710,8 @@ data class NetCfg(
|
||||
val proxyDefaults: NetCfg =
|
||||
NetCfg(
|
||||
socksProxy = ":9050",
|
||||
tcpConnectTimeout = 15_000_000,
|
||||
tcpTimeout = 10_000_000,
|
||||
tcpConnectTimeout = 20_000_000,
|
||||
tcpTimeout = 15_000_000,
|
||||
tcpKeepAlive = KeepAliveOpts.defaults,
|
||||
smpPingInterval = 600_000_000
|
||||
)
|
||||
@@ -1646,6 +1762,32 @@ data class ChatSettings(
|
||||
val enableNtfs: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChatPreferences(
|
||||
val voice: ChatPreference? = null
|
||||
) {
|
||||
companion object {
|
||||
val default = ChatPreferences(
|
||||
voice = ChatPreference(allow = PrefAllowed.NO)
|
||||
)
|
||||
val empty = ChatPreferences(
|
||||
voice = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ChatPreference(
|
||||
val allow: PrefAllowed
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class PrefAllowed {
|
||||
@SerialName("always") ALWAYS,
|
||||
@SerialName("yes") YES,
|
||||
@SerialName("no") NO
|
||||
}
|
||||
|
||||
val json = Json {
|
||||
prettyPrint = true
|
||||
ignoreUnknownKeys = true
|
||||
@@ -1698,11 +1840,13 @@ sealed class CR {
|
||||
@Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR()
|
||||
@Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR()
|
||||
@Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val toConnection: PendingContactConnection): CR()
|
||||
@Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val toContact: Contact): CR()
|
||||
@Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR()
|
||||
@Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR()
|
||||
@Serializable @SerialName("userContactLink") class UserContactLink(val contactLink: UserContactLinkRec): CR()
|
||||
@Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val contactLink: UserContactLinkRec): CR()
|
||||
@Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR()
|
||||
@Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted: CR()
|
||||
@Serializable @SerialName("contactConnected") class ContactConnected(val contact: Contact): CR()
|
||||
@Serializable @SerialName("contactConnected") class ContactConnected(val contact: Contact, val userCustomProfile: Profile? = null): CR()
|
||||
@Serializable @SerialName("contactConnecting") class ContactConnecting(val contact: Contact): CR()
|
||||
@Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val contactRequest: UserContactRequest): CR()
|
||||
@Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val contact: Contact): CR()
|
||||
@@ -1724,7 +1868,7 @@ sealed class CR {
|
||||
// group events
|
||||
@Serializable @SerialName("groupCreated") class GroupCreated(val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val groupInfo: GroupInfo, val hostContact: Contact? = null): CR()
|
||||
@Serializable @SerialName("userDeletedMember") class UserDeletedMember(val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("leftMemberUser") class LeftMemberUser(val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("groupMembers") class GroupMembers(val group: Group): CR()
|
||||
@@ -1744,6 +1888,9 @@ sealed class CR {
|
||||
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("groupRemoved") class GroupRemoved(val groupInfo: GroupInfo): CR() // unused
|
||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val toGroup: GroupInfo): CR()
|
||||
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val groupInfo: GroupInfo, val connReqContact: String): CR()
|
||||
@Serializable @SerialName("groupLink") class GroupLink(val groupInfo: GroupInfo, val connReqContact: String): CR()
|
||||
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val groupInfo: GroupInfo): CR()
|
||||
// receiving file events
|
||||
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@@ -1790,8 +1937,10 @@ sealed class CR {
|
||||
is UserProfileUpdated -> "userProfileUpdated"
|
||||
is ContactAliasUpdated -> "contactAliasUpdated"
|
||||
is ConnectionAliasUpdated -> "connectionAliasUpdated"
|
||||
is ContactPrefsUpdated -> "contactPrefsUpdated"
|
||||
is ParsedMarkdown -> "apiParsedMarkdown"
|
||||
is UserContactLink -> "userContactLink"
|
||||
is UserContactLinkUpdated -> "userContactLinkUpdated"
|
||||
is UserContactLinkCreated -> "userContactLinkCreated"
|
||||
is UserContactLinkDeleted -> "userContactLinkDeleted"
|
||||
is ContactConnected -> "contactConnected"
|
||||
@@ -1835,6 +1984,9 @@ sealed class CR {
|
||||
is ConnectedToGroupMember -> "connectedToGroupMember"
|
||||
is GroupRemoved -> "groupRemoved"
|
||||
is GroupUpdated -> "groupUpdated"
|
||||
is GroupLinkCreated -> "groupLinkCreated"
|
||||
is GroupLink -> "groupLink"
|
||||
is GroupLinkDeleted -> "groupLinkDeleted"
|
||||
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
|
||||
is RcvFileAccepted -> "rcvFileAccepted"
|
||||
is RcvFileStart -> "rcvFileStart"
|
||||
@@ -1880,8 +2032,10 @@ sealed class CR {
|
||||
is UserProfileUpdated -> json.encodeToString(toProfile)
|
||||
is ContactAliasUpdated -> json.encodeToString(toContact)
|
||||
is ConnectionAliasUpdated -> json.encodeToString(toConnection)
|
||||
is ContactPrefsUpdated -> json.encodeToString(toContact)
|
||||
is ParsedMarkdown -> json.encodeToString(formattedText)
|
||||
is UserContactLink -> connReqContact
|
||||
is UserContactLink -> contactLink.responseDetails
|
||||
is UserContactLinkUpdated -> contactLink.responseDetails
|
||||
is UserContactLinkCreated -> connReqContact
|
||||
is UserContactLinkDeleted -> noDetails()
|
||||
is ContactConnected -> json.encodeToString(contact)
|
||||
@@ -1925,6 +2079,9 @@ sealed class CR {
|
||||
is ConnectedToGroupMember -> "groupInfo: $groupInfo\nmember: $member"
|
||||
is GroupRemoved -> json.encodeToString(groupInfo)
|
||||
is GroupUpdated -> json.encodeToString(toGroup)
|
||||
is GroupLinkCreated -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact"
|
||||
is GroupLink -> "groupInfo: $groupInfo\nconnReqContact: $connReqContact"
|
||||
is GroupLinkDeleted -> json.encodeToString(groupInfo)
|
||||
is RcvFileAcceptedSndCancelled -> noDetails()
|
||||
is RcvFileAccepted -> json.encodeToString(chatItem)
|
||||
is RcvFileStart -> json.encodeToString(chatItem)
|
||||
@@ -1981,6 +2138,24 @@ abstract class TerminalItem {
|
||||
@Serializable
|
||||
class ConnectionStats(val rcvServers: List<String>?, val sndServers: List<String>?)
|
||||
|
||||
@Serializable
|
||||
class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) {
|
||||
val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class AutoAccept(val acceptIncognito: Boolean, val autoReply: MsgContent?) {
|
||||
companion object {
|
||||
fun cmdString(autoAccept: AutoAccept?): String {
|
||||
if (autoAccept == null) return "off"
|
||||
val s = "on" + if (autoAccept.acceptIncognito) " incognito=on" else ""
|
||||
val msg = autoAccept.autoReply ?: return s
|
||||
return s + " " + msg.cmdString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
sealed class ChatError {
|
||||
val string: String get() = when (this) {
|
||||
@@ -2000,12 +2175,10 @@ sealed class ChatErrorType {
|
||||
val string: String get() = when (this) {
|
||||
is NoActiveUser -> "noActiveUser"
|
||||
is InvalidConnReq -> "invalidConnReq"
|
||||
is ContactGroups -> "groupNames $groupNames"
|
||||
is СommandError -> "commandError $message"
|
||||
}
|
||||
@Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType()
|
||||
@Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType()
|
||||
@Serializable @SerialName("contactGroups") class ContactGroups(val contact: Contact, val groupNames: List<String>): ChatErrorType()
|
||||
@Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
||||
val Gray = Color(0x22222222)
|
||||
val Indigo = Color(0xff330099)
|
||||
val Indigo = Color(0xFF9966FF)
|
||||
val SimplexBlue = Color(0, 136, 255, 255) // If this value changes also need to update #0088ff in string resource files
|
||||
val SimplexGreen = Color(77, 218, 103, 255)
|
||||
val SecretColor = Color(0x40808080)
|
||||
|
||||
@@ -2,6 +2,7 @@ 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.*
|
||||
@@ -24,25 +25,34 @@ 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.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
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)) }
|
||||
BackHandler(onBack = close)
|
||||
val authorized = remember { mutableStateOf(!chatModel.controller.appPrefs.performLA.get()) }
|
||||
val lastSuccessfulAuth = remember { lastSuccessfulAuth }
|
||||
BackHandler(onBack = {
|
||||
lastSuccessfulAuth.value = null
|
||||
close()
|
||||
})
|
||||
val authorized = remember { !chatModel.controller.appPrefs.performLA.get() }
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(authorized.value) {
|
||||
if (!authorized.value) {
|
||||
runAuth(authorized = authorized, context)
|
||||
LaunchedEffect(lastSuccessfulAuth.value) {
|
||||
if (!authorized && !authorizedPreviously(lastSuccessfulAuth)) {
|
||||
runAuth(lastSuccessfulAuth, context)
|
||||
}
|
||||
}
|
||||
if (authorized.value) {
|
||||
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,
|
||||
composeState,
|
||||
@@ -61,7 +71,7 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
stringResource(R.string.auth_unlock),
|
||||
icon = Icons.Outlined.Lock,
|
||||
click = {
|
||||
runAuth(authorized = authorized, context)
|
||||
runAuth(lastSuccessfulAuth, context)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -70,15 +80,18 @@ fun TerminalView(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAuth(authorized: MutableState<Boolean>, context: 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 ->
|
||||
when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> authorized.value = true
|
||||
is LAResult.Error, LAResult.Failed -> authorized.value = false
|
||||
lastSuccessfulAuth.value = when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> SystemClock.elapsedRealtime()
|
||||
is LAResult.Error, LAResult.Failed -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -139,13 +152,19 @@ fun TerminalLayout(
|
||||
}
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
val listState = rememberLazyListState()
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed() } }
|
||||
LazyColumn(state = listState, reverseLayout = true) {
|
||||
items(reversedTerminalItems) { item ->
|
||||
Text("${item.date.toString().subSequence(11, 19)} ${item.label}",
|
||||
Text(
|
||||
"${item.date.toString().subSequence(11, 19)} ${item.label}",
|
||||
style = TextStyle(fontFamily = FontFamily.Monospace, fontSize = 18.sp, color = MaterialTheme.colors.primary),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -154,7 +173,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) {
|
||||
.clickable {
|
||||
ModalManager.shared.showModal {
|
||||
SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text(item.details)
|
||||
Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING))
|
||||
}
|
||||
}
|
||||
}.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
|
||||
@@ -93,7 +93,7 @@ sealed class WCallResponse {
|
||||
@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?, val callTs: Instant) {
|
||||
@Serializable class RcvCallInvitation(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
|
||||
|
||||
@@ -47,7 +47,6 @@ fun ChatInfoView(
|
||||
customUserProfile: Profile?,
|
||||
localAlias: String,
|
||||
close: () -> Unit,
|
||||
onChatUpdated: (Chat) -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = close)
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
@@ -61,10 +60,13 @@ fun ChatInfoView(
|
||||
localAlias,
|
||||
developerTools,
|
||||
onLocalAliasChanged = {
|
||||
setContactAlias(chat.chatInfo.apiId, it, chatModel, onChatUpdated)
|
||||
setContactAlias(chat.chatInfo.apiId, it, chatModel)
|
||||
},
|
||||
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
switchContactAddress = {
|
||||
showSwitchContactAddressAlert(chatModel, contact.contactId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +119,7 @@ fun ChatInfoLayout(
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
deleteContact: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
switchContactAddress: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -142,8 +145,12 @@ fun ChatInfoLayout(
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
if (connStats != null) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
if (developerTools) {
|
||||
SwitchAddressButton(switchContactAddress)
|
||||
SectionDivider()
|
||||
}
|
||||
if (connStats != null) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.network_status),
|
||||
@@ -162,8 +169,8 @@ fun ChatInfoLayout(
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
ClearChatButton(clearChat)
|
||||
SectionDivider()
|
||||
@@ -231,7 +238,9 @@ fun LocalAliasEditor(
|
||||
color = HighOrLowlight
|
||||
)
|
||||
},
|
||||
leadingIcon = if (leadingIcon) {{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }} else null,
|
||||
leadingIcon = if (leadingIcon) {
|
||||
{ Icon(Icons.Default.Edit, null, Modifier.padding(start = 7.dp)) }
|
||||
} else null,
|
||||
color = HighOrLowlight,
|
||||
focus = focus,
|
||||
textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center),
|
||||
@@ -311,6 +320,13 @@ fun SimplexServers(text: String, servers: List<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchAddressButton(onClick: () -> Unit) {
|
||||
SectionItemView(onClick) {
|
||||
Text(stringResource(R.string.switch_receiving_address), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ClearChatButton(onClick: () -> Unit) {
|
||||
SettingsActionItem(
|
||||
@@ -333,13 +349,27 @@ fun DeleteContactButton(onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel, onChatUpdated: (Chat) -> Unit) = withApi {
|
||||
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
|
||||
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
|
||||
chatModel.updateContact(it)
|
||||
onChatUpdated(chatModel.getChat(chatModel.chatId.value ?: return@withApi) ?: return@withApi)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSwitchContactAddressAlert(m: ChatModel, contactId: Long) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.switch_receiving_address_question),
|
||||
text = generalGetString(R.string.switch_receiving_address_desc),
|
||||
confirmText = generalGetString(R.string.switch_verb),
|
||||
onConfirm = {
|
||||
switchContactAddress(m, contactId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchContactAddress(m: ChatModel, contactId: Long) = withApi {
|
||||
m.controller.apiSwitchContact(contactId)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
@@ -356,7 +386,9 @@ fun PreviewChatInfoLayout() {
|
||||
connStats = null,
|
||||
onLocalAliasChanged = {},
|
||||
customUserProfile = null,
|
||||
deleteContact = {}, clearChat = {}
|
||||
deleteContact = {},
|
||||
clearChat = {},
|
||||
switchContactAddress = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.*
|
||||
@@ -48,11 +50,10 @@ import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
import java.io.File
|
||||
import kotlin.math.sign
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun ChatView(chatModel: ChatModel) {
|
||||
var activeChat by remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) }
|
||||
val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }) }
|
||||
val searchText = rememberSaveable { mutableStateOf("") }
|
||||
val user = chatModel.currentUser.value
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
@@ -66,23 +67,36 @@ fun ChatView(chatModel: ChatModel) {
|
||||
LaunchedEffect(Unit) {
|
||||
// snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value.
|
||||
// With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
activeChat = if (chatModel.chatId.value == null) {
|
||||
null
|
||||
} else {
|
||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||
// Also for situation when chatId changes after clicking in notification, etc
|
||||
chatModel.getChat(chatModel.chatId.value!!)
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
if (activeChat.value?.id != chatModel.chatId.value && chatModel.chatId.value != null) {
|
||||
// Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly
|
||||
// Also for situation when chatId changes after clicking in notification, etc
|
||||
activeChat.value = chatModel.getChat(chatModel.chatId.value!!)
|
||||
}
|
||||
markUnreadChatAsRead(activeChat, chatModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
launch {
|
||||
// .toList() is important for making observation working
|
||||
snapshotFlow { chatModel.chats.toList() }
|
||||
.distinctUntilChanged()
|
||||
.collect { chats ->
|
||||
chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }.let {
|
||||
// Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions
|
||||
if (it?.chatInfo != activeChat.value?.chatInfo) {
|
||||
activeChat.value = it
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeChat == null || user == null) {
|
||||
val view = LocalView.current
|
||||
if (activeChat.value == null || user == null) {
|
||||
chatModel.chatId.value = null
|
||||
} else {
|
||||
val chat = activeChat!!
|
||||
val chat = activeChat.value!!
|
||||
BackHandler { chatModel.chatId.value = null }
|
||||
// We need to have real unreadCount value for displaying it inside top right button
|
||||
// Having activeChat reloaded on every change in it is inefficient (UI lags)
|
||||
@@ -112,16 +126,18 @@ fun ChatView(chatModel: ChatModel) {
|
||||
searchText,
|
||||
useLinkPreviews = useLinkPreviews,
|
||||
chatModelIncognito = chatModel.incognito.value,
|
||||
back = { chatModel.chatId.value = null },
|
||||
back = {
|
||||
hideKeyboard(view)
|
||||
chatModel.chatId.value = null
|
||||
},
|
||||
info = {
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(cInfo.apiId)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close) {
|
||||
activeChat = it
|
||||
}
|
||||
ChatInfoView(chatModel, cInfo.contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, close)
|
||||
}
|
||||
} else if (cInfo is ChatInfo.Group) {
|
||||
setGroupMembers(cInfo.groupInfo, chatModel)
|
||||
@@ -132,6 +148,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
}
|
||||
},
|
||||
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
val stats = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
@@ -175,6 +192,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
}
|
||||
},
|
||||
acceptCall = { contact ->
|
||||
hideKeyboard(view)
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg("Call already ended!")
|
||||
@@ -183,6 +201,7 @@ fun ChatView(chatModel: ChatModel) {
|
||||
}
|
||||
},
|
||||
addMembers = { groupInfo ->
|
||||
hideKeyboard(view)
|
||||
withApi {
|
||||
setGroupMembers(groupInfo, chatModel)
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
@@ -447,16 +466,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
val scope = rememberCoroutineScope()
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val cxt = LocalContext.current
|
||||
// Helps to scroll to bottom after moving from Group to Direct chat
|
||||
// and prevents scrolling to bottom on orientation change
|
||||
var shouldAutoScroll by rememberSaveable { mutableStateOf(true) }
|
||||
LaunchedEffect(chat.chatInfo.apiId, chat.chatInfo.chatType, shouldAutoScroll) {
|
||||
if (shouldAutoScroll && listState.firstVisibleItemIndex != 0) {
|
||||
scope.launch { listState.scrollToItem(0) }
|
||||
}
|
||||
// Don't autoscroll next time until it will be needed
|
||||
shouldAutoScroll = false
|
||||
}
|
||||
ScrollToBottom(chat.id, listState)
|
||||
var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) }
|
||||
// Scroll to bottom when search value changes from something to nothing and back
|
||||
LaunchedEffect(searchValue.value.isEmpty()) {
|
||||
@@ -548,7 +558,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, provider, showMember = showMember, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = 86.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(user, chat.chatInfo, cItem, composeState, cxt, uriHandler, provider, chatModelIncognito = chatModelIncognito, useLinkPreviews = useLinkPreviews, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, scrollToItem = scrollToItem)
|
||||
}
|
||||
}
|
||||
@@ -578,6 +588,21 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
FloatingButtons(chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScrollToBottom(chatId: ChatId, listState: LazyListState) {
|
||||
val scope = rememberCoroutineScope()
|
||||
// Helps to scroll to bottom after moving from Group to Direct chat
|
||||
// and prevents scrolling to bottom on orientation change
|
||||
var shouldAutoScroll by rememberSaveable { mutableStateOf(true to chatId) }
|
||||
LaunchedEffect(chatId, shouldAutoScroll) {
|
||||
if ((shouldAutoScroll.first || shouldAutoScroll.second != chatId) && listState.firstVisibleItemIndex != 0) {
|
||||
scope.launch { listState.scrollToItem(0) }
|
||||
}
|
||||
// Don't autoscroll next time until it will be needed
|
||||
shouldAutoScroll = false to chatId
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BoxWithConstraintsScope.FloatingButtons(
|
||||
chatItems: List<ChatItem>,
|
||||
@@ -784,6 +809,22 @@ private fun bottomEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: ChatModel) {
|
||||
val chat = activeChat.value
|
||||
if (chat?.chatStats?.unreadChat != true) return
|
||||
withApi {
|
||||
val success = chatModel.controller.apiChatUnread(
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
false
|
||||
)
|
||||
if (success && chat.id == activeChat.value?.id) {
|
||||
activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))
|
||||
chatModel.replaceChat(chat.id, activeChat.value!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun providerForGallery(
|
||||
listStateIndex: Int,
|
||||
chatItems: List<ChatItem>,
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.material.icons.outlined.Close
|
||||
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.asImageBitmap
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -2,11 +2,11 @@ package chat.simplex.app.views.chat
|
||||
|
||||
import ComposeFileView
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.graphics.ImageDecoder.DecodeException
|
||||
import android.graphics.drawable.AnimatedImageDrawable
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
@@ -14,7 +14,6 @@ import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -26,6 +25,7 @@ import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.outlined.Reply
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -34,7 +34,6 @@ 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.content.FileProvider
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -44,7 +43,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import java.io.File
|
||||
|
||||
@Serializable
|
||||
sealed class ComposePreview {
|
||||
@@ -69,7 +67,7 @@ data class ComposeState(
|
||||
val inProgress: Boolean = false,
|
||||
val useLinkPreviews: Boolean
|
||||
) {
|
||||
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this (
|
||||
constructor(editingItem: ChatItem, useLinkPreviews: Boolean): this(
|
||||
editingItem.content.text,
|
||||
chatItemPreview(editingItem),
|
||||
ComposeContextItem.EditingItem(editingItem),
|
||||
@@ -137,53 +135,22 @@ fun ComposeView(
|
||||
showChooseAttachment: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val linkUrl = remember { mutableStateOf<String?>(null) }
|
||||
val prevLinkUrl = remember { mutableStateOf<String?>(null) }
|
||||
val pendingLinkUrl = remember { mutableStateOf<String?>(null) }
|
||||
val cancelledLinks = remember { mutableSetOf<String>() }
|
||||
val linkUrl = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
|
||||
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 = remember { mutableStateOf<List<UploadContent>>(emptyList()) }
|
||||
val chosenFile = remember { mutableStateOf<Uri?>(null) }
|
||||
val photoUri = remember { mutableStateOf<Uri?>(null) }
|
||||
val photoTmpFile = remember { mutableStateOf<File?>(null) }
|
||||
|
||||
class ComposeTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
|
||||
@CallSuper
|
||||
override fun createIntent(context: Context, input: Void?): Intent {
|
||||
photoTmpFile.value = File.createTempFile("image", ".bmp", SimplexApp.context.filesDir)
|
||||
photoUri.value = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", photoTmpFile.value!!)
|
||||
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
.putExtra(MediaStore.EXTRA_OUTPUT, photoUri.value)
|
||||
}
|
||||
|
||||
override fun getSynchronousResult(
|
||||
context: Context,
|
||||
input: Void?
|
||||
): SynchronousResult<Bitmap?>? = null
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
|
||||
val photoUriVal = photoUri.value
|
||||
val photoTmpFileVal = photoTmpFile.value
|
||||
return if (resultCode == Activity.RESULT_OK && photoUriVal != null && photoTmpFileVal != null) {
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, photoUriVal)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
photoTmpFileVal.delete()
|
||||
bitmap
|
||||
} else {
|
||||
Log.e(TAG, "Getting image from camera cancelled or failed.")
|
||||
photoTmpFile.value?.delete()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cameraLauncher = rememberLauncherForActivityResult(contract = ComposeTakePicturePreview()) { bitmap: Bitmap? ->
|
||||
if (bitmap != null) {
|
||||
val chosenContent = rememberSaveable { mutableStateOf<List<UploadContent>>(emptyList()) }
|
||||
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(bitmap))
|
||||
chosenContent.value = listOf(UploadContent.SimpleImage(uri))
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview)))
|
||||
}
|
||||
}
|
||||
@@ -194,13 +161,22 @@ fun ComposeView(
|
||||
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
val processPickedImage = { uris: List<Uri> ->
|
||||
val processPickedImage = { uris: List<Uri>, text: String? ->
|
||||
val content = ArrayList<UploadContent>()
|
||||
val imagesPreview = ArrayList<String>()
|
||||
uris.forEach { uri ->
|
||||
val source = ImageDecoder.createSource(context.contentResolver, uri)
|
||||
val drawable = ImageDecoder.decodeDrawable(source)
|
||||
var bitmap: Bitmap? = ImageDecoder.decodeBitmap(source)
|
||||
val drawable = try {
|
||||
ImageDecoder.decodeDrawable(source)
|
||||
} catch (e: DecodeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.image_decoding_exception_title),
|
||||
text = generalGetString(R.string.image_decoding_exception_desc)
|
||||
)
|
||||
Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}")
|
||||
null
|
||||
}
|
||||
var bitmap: Bitmap? = if (drawable != null) ImageDecoder.decodeBitmap(source) else null
|
||||
if (drawable is AnimatedImageDrawable) {
|
||||
// It's a gif or webp
|
||||
val fileSize = getFileSize(context, uri)
|
||||
@@ -214,7 +190,7 @@ fun ComposeView(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (bitmap != null) content.add(UploadContent.SimpleImage(bitmap))
|
||||
content.add(UploadContent.SimpleImage(uri))
|
||||
}
|
||||
if (bitmap != null) {
|
||||
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
|
||||
@@ -223,17 +199,17 @@ fun ComposeView(
|
||||
|
||||
if (imagesPreview.isNotEmpty()) {
|
||||
chosenContent.value = content
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(imagesPreview))
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview))
|
||||
}
|
||||
}
|
||||
val processPickedFile = { uri: Uri? ->
|
||||
val processPickedFile = { uri: Uri?, text: String? ->
|
||||
if (uri != null) {
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
|
||||
val fileName = getFileName(SimplexApp.context, uri)
|
||||
if (fileName != null) {
|
||||
chosenFile.value = uri
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.FilePreview(fileName))
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName))
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -243,9 +219,9 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
}
|
||||
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery(), processPickedImage)
|
||||
val galleryLauncherFallback = rememberGetMultipleContentsLauncher(processPickedImage)
|
||||
val filesLauncher = rememberGetContentLauncher(processPickedFile)
|
||||
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
|
||||
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
|
||||
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
|
||||
|
||||
LaunchedEffect(attachmentOption.value) {
|
||||
when (attachmentOption.value) {
|
||||
@@ -294,6 +270,9 @@ fun ComposeView(
|
||||
if (lp != null && pendingLinkUrl.value == url) {
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.CLinkPreview(lp))
|
||||
pendingLinkUrl.value = null
|
||||
} else if (pendingLinkUrl.value == url) {
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview)
|
||||
pendingLinkUrl.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,7 +367,7 @@ fun ComposeView(
|
||||
is ComposePreview.ImagePreview -> {
|
||||
chosenContent.value.forEachIndexed { index, it ->
|
||||
val file = when (it) {
|
||||
is UploadContent.SimpleImage -> saveImage(context, it.bitmap)
|
||||
is UploadContent.SimpleImage -> saveImage(context, it.uri)
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
@@ -500,8 +479,8 @@ fun ComposeView(
|
||||
LaunchedEffect(chatModel.sharedContent.value) {
|
||||
when (val shared = chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> onMessageChange(shared.text)
|
||||
is SharedContent.Image -> processPickedImage(listOf(shared.uri))
|
||||
is SharedContent.File -> processPickedFile(shared.uri)
|
||||
is SharedContent.Images -> processPickedImage(shared.uris, shared.text)
|
||||
is SharedContent.File -> processPickedFile(shared.uri, shared.text)
|
||||
null -> {}
|
||||
}
|
||||
chatModel.sharedContent.value = null
|
||||
@@ -547,7 +526,16 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
class PickFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
class PickFromGallery: ActivityResultContract<Int, Uri?>() {
|
||||
override fun createIntent(context: Context, input: Int) =
|
||||
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply {
|
||||
type = "image/*"
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data
|
||||
}
|
||||
|
||||
class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
override fun createIntent(context: Context, input: Int) =
|
||||
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply {
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package chat.simplex.app.views.chat
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.text.InputType
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.*
|
||||
import android.widget.EditText
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
@@ -14,27 +17,25 @@ import androidx.compose.material.icons.outlined.ArrowUpward
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.text.BidiFormatter
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.SharedContent
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.min
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SendMsgView(
|
||||
composeState: MutableState<ComposeState>,
|
||||
@@ -43,82 +44,114 @@ fun SendMsgView(
|
||||
textStyle: MutableState<TextStyle>
|
||||
) {
|
||||
val cs = composeState.value
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
var showKeyboard by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(cs.contextItem) {
|
||||
if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect
|
||||
// In replying state
|
||||
focusRequester.requestFocus()
|
||||
delay(50)
|
||||
keyboard?.show()
|
||||
when (cs.contextItem) {
|
||||
is ComposeContextItem.QuotedItem -> {
|
||||
delay(100)
|
||||
showKeyboard = true
|
||||
}
|
||||
is ComposeContextItem.EditingItem -> {
|
||||
// Keyboard will not show up if we try to show it too fast
|
||||
delay(300)
|
||||
showKeyboard = true
|
||||
}
|
||||
}
|
||||
}
|
||||
val isRtl = remember(cs.message) { BidiFormatter.getInstance().isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
|
||||
BasicTextField(
|
||||
value = cs.message,
|
||||
onValueChange = onMessageChange,
|
||||
textStyle = textStyle.value,
|
||||
maxLines = 16,
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
autoCorrect = true
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 8.dp).focusRequester(focusRequester),
|
||||
cursorBrush = SolidColor(HighOrLowlight),
|
||||
decorationBox = { innerTextField ->
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
border = BorderStroke(1.dp, MaterialTheme.colors.secondary)
|
||||
) {
|
||||
Row(
|
||||
Modifier.background(MaterialTheme.colors.background),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
|
||||
val textColor = MaterialTheme.colors.onBackground
|
||||
val tintColor = MaterialTheme.colors.secondary
|
||||
val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() }
|
||||
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
|
||||
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||
|
||||
Column(Modifier.padding(vertical = 8.dp)) {
|
||||
Box {
|
||||
AndroidView(modifier = Modifier, factory = {
|
||||
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
|
||||
override fun setOnReceiveContentListener(
|
||||
mimeTypes: Array<out String>?,
|
||||
listener: android.view.OnReceiveContentListener?
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(top = 5.dp)
|
||||
.padding(bottom = 7.dp)
|
||||
) {
|
||||
innerTextField()
|
||||
}
|
||||
super.setOnReceiveContentListener(mimeTypes, listener)
|
||||
}
|
||||
val icon = if (cs.editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward
|
||||
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
|
||||
if (cs.inProgress
|
||||
&& (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.clickable {
|
||||
if (cs.sendEnabled()) {
|
||||
sendMessage()
|
||||
}
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
val onCommit = InputConnectionCompat.OnCommitContentListener { inputContentInfo, _, _ ->
|
||||
try {
|
||||
inputContentInfo.requestPermission()
|
||||
} catch (e: Exception) {
|
||||
return@OnCommitContentListener false
|
||||
}
|
||||
)
|
||||
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Images("", listOf(inputContentInfo.contentUri))
|
||||
true
|
||||
}
|
||||
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)
|
||||
}
|
||||
}
|
||||
editText.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
editText.maxLines = 16
|
||||
editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType
|
||||
editText.setTextColor(textColor.toArgb())
|
||||
editText.textSize = textStyle.value.fontSize.value
|
||||
val drawable = it.getDrawable(R.drawable.send_msg_view_background)!!
|
||||
DrawableCompat.setTint(drawable, tintColor.toArgb())
|
||||
editText.background = drawable
|
||||
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
|
||||
editText.setText(cs.message)
|
||||
editText.textCursorDrawable?.let { DrawableCompat.setTint(it, HighOrLowlight.toArgb()) }
|
||||
editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
|
||||
editText
|
||||
}) {
|
||||
it.setTextColor(textColor.toArgb())
|
||||
it.textSize = textStyle.value.fontSize.value
|
||||
DrawableCompat.setTint(it.background, tintColor.toArgb())
|
||||
if (cs.message != it.text.toString()) {
|
||||
it.setText(cs.message)
|
||||
// Set cursor to the end of the text
|
||||
it.setSelection(it.text.length)
|
||||
}
|
||||
if (showKeyboard) {
|
||||
it.requestFocus()
|
||||
val imm: InputMethodManager = SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
|
||||
showKeyboard = false
|
||||
}
|
||||
}
|
||||
Box(Modifier.align(Alignment.BottomEnd)) {
|
||||
val icon = if (cs.editing) Icons.Filled.Check else Icons.Outlined.ArrowUpward
|
||||
val color = if (cs.sendEnabled()) MaterialTheme.colors.primary else HighOrLowlight
|
||||
if (cs.inProgress
|
||||
&& (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp),
|
||||
color = HighOrLowlight,
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(R.string.icon_descr_send_message),
|
||||
tint = Color.White,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.padding(4.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.clickable {
|
||||
if (cs.sendEnabled()) {
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
||||
+34
-24
@@ -14,7 +14,6 @@ import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -35,14 +34,16 @@ import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
fun AddGroupMembersView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) {
|
||||
val selectedContacts = remember { mutableStateListOf<Long>() }
|
||||
val selectedRole = remember { mutableStateOf(GroupMemberRole.Member) }
|
||||
|
||||
var allowModifyMembers by remember { mutableStateOf(true) }
|
||||
BackHandler(onBack = close)
|
||||
AddGroupMembersLayout(
|
||||
groupInfo = groupInfo,
|
||||
contactsToAdd = getContactsToAdd(chatModel),
|
||||
selectedContacts = selectedContacts,
|
||||
selectedRole = selectedRole,
|
||||
allowModifyMembers = allowModifyMembers,
|
||||
inviteMembers = {
|
||||
allowModifyMembers = false
|
||||
withApi {
|
||||
for (contactId in selectedContacts) {
|
||||
val member = chatModel.controller.apiAddMember(groupInfo.groupId, contactId, selectedRole.value)
|
||||
@@ -79,8 +80,9 @@ fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
|
||||
fun AddGroupMembersLayout(
|
||||
groupInfo: GroupInfo,
|
||||
contactsToAdd: List<Contact>,
|
||||
selectedContacts: SnapshotStateList<Long>,
|
||||
selectedContacts: List<Long>,
|
||||
selectedRole: MutableState<GroupMemberRole>,
|
||||
allowModifyMembers: Boolean,
|
||||
inviteMembers: () -> Unit,
|
||||
clearSelection: () -> Unit,
|
||||
addContact: (Long) -> Unit,
|
||||
@@ -119,18 +121,18 @@ fun AddGroupMembersLayout(
|
||||
} else {
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
RoleSelectionRow(groupInfo, selectedRole)
|
||||
RoleSelectionRow(groupInfo, selectedRole, allowModifyMembers)
|
||||
}
|
||||
SectionDivider()
|
||||
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty())
|
||||
InviteMembersButton(inviteMembers, disabled = selectedContacts.isEmpty() || !allowModifyMembers)
|
||||
}
|
||||
SectionCustomFooter {
|
||||
InviteSectionFooter(selectedContactsCount = selectedContacts.count(), clearSelection)
|
||||
InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, addContact, removeContact)
|
||||
ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact)
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
@@ -138,7 +140,7 @@ fun AddGroupMembersLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole>) {
|
||||
private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<GroupMemberRole>, enabled: Boolean) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -150,7 +152,7 @@ private fun RoleSelectionRow(groupInfo: GroupInfo, selectedRole: MutableState<Gr
|
||||
values,
|
||||
selectedRole,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
enabled = rememberUpdatedState(enabled),
|
||||
onSelected = { selectedRole.value = it }
|
||||
)
|
||||
}
|
||||
@@ -169,7 +171,7 @@ fun InviteMembersButton(onClick: () -> Unit, disabled: Boolean) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit) {
|
||||
fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelection: () -> Unit) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
@@ -182,11 +184,11 @@ fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit)
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Box(
|
||||
Modifier.clickable { clearSelection() }
|
||||
Modifier.clickable { if (enabled) clearSelection() }
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.clear_contacts_selection_button),
|
||||
color = MaterialTheme.colors.primary,
|
||||
color = if (enabled) MaterialTheme.colors.primary else HighOrLowlight,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
@@ -203,8 +205,9 @@ fun InviteSectionFooter(selectedContactsCount: Int, clearSelection: () -> Unit)
|
||||
@Composable
|
||||
fun ContactList(
|
||||
contacts: List<Contact>,
|
||||
selectedContacts: SnapshotStateList<Long>,
|
||||
selectedContacts: List<Long>,
|
||||
groupInfo: GroupInfo,
|
||||
enabled: Boolean,
|
||||
addContact: (Long) -> Unit,
|
||||
removeContact: (Long) -> Unit
|
||||
) {
|
||||
@@ -212,7 +215,8 @@ fun ContactList(
|
||||
contacts.forEachIndexed { index, contact ->
|
||||
ContactCheckRow(
|
||||
contact, groupInfo, addContact, removeContact,
|
||||
checked = selectedContacts.contains(contact.apiId)
|
||||
checked = selectedContacts.contains(contact.apiId),
|
||||
enabled = enabled,
|
||||
)
|
||||
if (index < contacts.lastIndex) {
|
||||
SectionDivider()
|
||||
@@ -227,7 +231,8 @@ fun ContactCheckRow(
|
||||
groupInfo: GroupInfo,
|
||||
addContact: (Long) -> Unit,
|
||||
removeContact: (Long) -> Unit,
|
||||
checked: Boolean
|
||||
checked: Boolean,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
val prohibitedToInviteIncognito = !groupInfo.membership.memberIncognito && contact.contactConnIncognito
|
||||
val icon: ImageVector
|
||||
@@ -237,19 +242,23 @@ fun ContactCheckRow(
|
||||
iconColor = HighOrLowlight
|
||||
} else if (checked) {
|
||||
icon = Icons.Filled.CheckCircle
|
||||
iconColor = MaterialTheme.colors.primary
|
||||
iconColor = if (enabled) MaterialTheme.colors.primary else HighOrLowlight
|
||||
} else {
|
||||
icon = Icons.Outlined.Circle
|
||||
iconColor = HighOrLowlight
|
||||
}
|
||||
SectionItemView(click = {
|
||||
if (prohibitedToInviteIncognito) {
|
||||
showProhibitedToInviteIncognitoAlertDialog()
|
||||
} else if (!checked)
|
||||
addContact(contact.apiId)
|
||||
else
|
||||
removeContact(contact.apiId)
|
||||
}) {
|
||||
SectionItemView(
|
||||
click = if (enabled) {
|
||||
{
|
||||
if (prohibitedToInviteIncognito) {
|
||||
showProhibitedToInviteIncognitoAlertDialog()
|
||||
} else if (!checked)
|
||||
addContact(contact.apiId)
|
||||
else
|
||||
removeContact(contact.apiId)
|
||||
}
|
||||
} else null
|
||||
) {
|
||||
ProfileImage(size = 36.dp, contact.image)
|
||||
Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON))
|
||||
Text(
|
||||
@@ -282,6 +291,7 @@ fun PreviewAddGroupMembersLayout() {
|
||||
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
|
||||
selectedContacts = remember { mutableStateListOf() },
|
||||
selectedRole = remember { mutableStateOf(GroupMemberRole.Admin) },
|
||||
allowModifyMembers = true,
|
||||
inviteMembers = {},
|
||||
clearSelection = {},
|
||||
addContact = {},
|
||||
|
||||
+27
-2
@@ -65,6 +65,12 @@ fun GroupChatInfoView(chatModel: ChatModel, close: () -> Unit) {
|
||||
deleteGroup = { deleteGroupDialog(chat.chatInfo, groupInfo, chatModel, close) },
|
||||
clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) },
|
||||
leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) },
|
||||
manageGroupLink = {
|
||||
withApi {
|
||||
val groupLink = chatModel.controller.apiGetGroupLink(groupInfo.groupId)
|
||||
ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +123,7 @@ fun GroupChatInfoLayout(
|
||||
deleteGroup: () -> Unit,
|
||||
clearChat: () -> Unit,
|
||||
leaveGroup: () -> Unit,
|
||||
manageGroupLink: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -134,6 +141,8 @@ fun GroupChatInfoLayout(
|
||||
|
||||
SectionView(title = String.format(generalGetString(R.string.group_info_section_title_num_members), members.count() + 1)) {
|
||||
if (groupInfo.canAddMembers) {
|
||||
SectionItemView(manageGroupLink) { GroupLinkButton() }
|
||||
SectionDivider()
|
||||
val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers
|
||||
SectionItemView(onAddMembersClick) {
|
||||
val tint = if (chat.chatInfo.incognito) HighOrLowlight else MaterialTheme.colors.primary
|
||||
@@ -150,7 +159,6 @@ fun GroupChatInfoLayout(
|
||||
MembersList(members, showMemberInfo)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView {
|
||||
if (groupInfo.canEdit) {
|
||||
SectionItemView(editGroupProfile) { EditGroupProfileButton() }
|
||||
@@ -268,6 +276,23 @@ fun MemberRow(member: GroupMember, user: Boolean = false) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupLinkButton() {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Link,
|
||||
stringResource(R.string.group_link),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.group_link), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditGroupProfileButton() {
|
||||
Row(
|
||||
@@ -330,7 +355,7 @@ fun PreviewGroupChatInfoLayout() {
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData),
|
||||
developerTools = false,
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {},
|
||||
addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package chat.simplex.app.views.chat.group
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Text
|
||||
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.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.GroupInfo
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
|
||||
@Composable
|
||||
fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?) {
|
||||
var groupLink by remember { mutableStateOf(connReqContact) }
|
||||
val cxt = LocalContext.current
|
||||
GroupLinkLayout(
|
||||
groupLink = groupLink,
|
||||
createLink = {
|
||||
withApi {
|
||||
groupLink = chatModel.controller.apiCreateGroupLink(groupInfo.groupId)
|
||||
}
|
||||
},
|
||||
share = { shareText(cxt, groupLink ?: return@GroupLinkLayout) },
|
||||
deleteLink = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.delete_link_question),
|
||||
text = generalGetString(R.string.all_group_members_will_remain_connected),
|
||||
confirmText = generalGetString(R.string.delete_verb),
|
||||
onConfirm = {
|
||||
withApi {
|
||||
val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId)
|
||||
if (r) {
|
||||
groupLink = null
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupLinkLayout(
|
||||
groupLink: String?,
|
||||
createLink: () -> Unit,
|
||||
share: () -> Unit,
|
||||
deleteLink: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.group_link), false)
|
||||
Text(
|
||||
stringResource(R.string.you_can_share_group_link_anybody_will_be_able_to_connect),
|
||||
Modifier.padding(bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (groupLink == null) {
|
||||
SimpleButton(stringResource(R.string.button_create_group_link), icon = Icons.Outlined.AddLink, click = createLink)
|
||||
} else {
|
||||
QRCode(groupLink, Modifier.weight(1f, fill = false).aspectRatio(1f))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.share_link),
|
||||
icon = Icons.Outlined.Share,
|
||||
click = share
|
||||
)
|
||||
SimpleButton(
|
||||
stringResource(R.string.delete_link),
|
||||
icon = Icons.Outlined.Delete,
|
||||
color = Color.Red,
|
||||
click = deleteLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-11
@@ -24,6 +24,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.SimplexServers
|
||||
import chat.simplex.app.views.chat.SwitchAddressButton
|
||||
import chat.simplex.app.views.chatlist.openChat
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
@@ -81,6 +82,9 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
switchMemberAddress = {
|
||||
switchMemberAddress(chatModel, groupInfo, member)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -113,6 +117,7 @@ fun GroupMemberInfoLayout(
|
||||
openDirectChat: () -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -136,14 +141,14 @@ fun GroupMemberInfoLayout(
|
||||
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
|
||||
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
|
||||
SectionDivider()
|
||||
/*val roles = remember { member.canChangeRoleTo(groupInfo) }
|
||||
val roles = remember { member.canChangeRoleTo(groupInfo) }
|
||||
if (roles != null) {
|
||||
SectionItemView {
|
||||
RoleSelectionRow(roles, newRole, onRoleSelected)
|
||||
}
|
||||
} else {*/
|
||||
} else {
|
||||
InfoRow(stringResource(R.string.role_in_group), member.memberRole.text)
|
||||
//}
|
||||
}
|
||||
val conn = member.activeConn
|
||||
if (conn != null) {
|
||||
SectionDivider()
|
||||
@@ -154,12 +159,15 @@ fun GroupMemberInfoLayout(
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (connStats != null) {
|
||||
val rcvServers = connStats.rcvServers
|
||||
val sndServers = connStats.sndServers
|
||||
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
|
||||
if (developerTools) {
|
||||
SwitchAddressButton(switchMemberAddress)
|
||||
SectionDivider()
|
||||
}
|
||||
if (connStats != null) {
|
||||
val rcvServers = connStats.rcvServers
|
||||
val sndServers = connStats.sndServers
|
||||
if ((rcvServers != null && rcvServers.isNotEmpty()) || (sndServers != null && sndServers.isNotEmpty())) {
|
||||
if (rcvServers != null && rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(R.string.receiving_via), rcvServers)
|
||||
if (sndServers != null && sndServers.isNotEmpty()) {
|
||||
@@ -170,9 +178,9 @@ fun GroupMemberInfoLayout(
|
||||
SimplexServers(stringResource(R.string.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
if (member.canBeRemoved(groupInfo)) {
|
||||
SectionView {
|
||||
@@ -280,6 +288,10 @@ private fun updateMemberRoleDialog(
|
||||
)
|
||||
}
|
||||
|
||||
private fun switchMemberAddress(m: ChatModel, groupInfo: GroupInfo, member: GroupMember) = withApi {
|
||||
m.controller.apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewGroupMemberInfoLayout() {
|
||||
@@ -292,7 +304,8 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
developerTools = false,
|
||||
openDirectChat = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {}
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -2,11 +2,13 @@ package chat.simplex.app.views.chat.group
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
@@ -53,8 +55,8 @@ fun GroupProfileLayout(
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val displayName = remember { mutableStateOf(groupProfile.displayName) }
|
||||
val fullName = remember { mutableStateOf(groupProfile.fullName) }
|
||||
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
|
||||
val profileImage = remember { mutableStateOf(groupProfile.image) }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
+3
-3
@@ -17,7 +17,7 @@ import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
|
||||
@Composable
|
||||
fun CIGroupEventView(ci: ChatItem) {
|
||||
fun CIEventView(ci: ChatItem) {
|
||||
fun withGroupEventStyle(builder: AnnotatedString.Builder, text: String) {
|
||||
return builder.withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = HighOrLowlight)) { append(text) }
|
||||
}
|
||||
@@ -50,9 +50,9 @@ fun CIGroupEventView(ci: ChatItem) {
|
||||
name = "Dark Mode"
|
||||
)
|
||||
@Composable
|
||||
fun CIGroupEventViewPreview() {
|
||||
fun CIEventViewPreview() {
|
||||
SimpleXTheme {
|
||||
CIGroupEventView(
|
||||
CIEventView(
|
||||
ChatItem.getGroupEventSample()
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.compose.foundation.Image
|
||||
@@ -9,8 +11,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.MoreHoriz
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -18,16 +19,16 @@ import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.BuildConfig
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CIFile
|
||||
import chat.simplex.app.model.CIFileStatus
|
||||
import chat.simplex.app.views.chat.item.ImageFullScreenView
|
||||
import chat.simplex.app.views.chat.item.ImageGalleryProvider
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
@@ -93,6 +94,12 @@ fun CIImageView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imageViewFullWidth(): Dp {
|
||||
val approximatePadding = 100.dp
|
||||
return with(LocalDensity.current) { minOf(1000.dp, LocalView.current.width.toDp() - approximatePadding) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) {
|
||||
Image(
|
||||
@@ -101,7 +108,7 @@ fun CIImageView(
|
||||
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
.width(1000.dp)
|
||||
.width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else 1000.dp)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
@@ -118,7 +125,7 @@ fun CIImageView(
|
||||
// .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView
|
||||
// if text is short and take all available width if text is long
|
||||
modifier = Modifier
|
||||
.width(1000.dp)
|
||||
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else 1000.dp)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
@@ -134,27 +141,28 @@ fun CIImageView(
|
||||
return false
|
||||
}
|
||||
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> {
|
||||
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, file)
|
||||
return imageBitmap to filePath
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val imageBitmap: Bitmap? = getLoadedImage(context, file)
|
||||
val filePath = getLoadedFilePath(context, file)
|
||||
val (imageBitmap, filePath) = remember(file) { imageAndFilePath(file) }
|
||||
if (imageBitmap != null && filePath != null) {
|
||||
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
val imageLoader = ImageLoader.Builder(context)
|
||||
.components {
|
||||
if (SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.build()
|
||||
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
|
||||
val imagePainter = rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(context).data(data = uri).size(coil.size.Size.ORIGINAL).build(),
|
||||
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
|
||||
imageLoader = imageLoader
|
||||
)
|
||||
val view = LocalView.current
|
||||
imageView(imagePainter, onClick = {
|
||||
hideKeyboard(view)
|
||||
if (getLoadedFilePath(context, file) != null) {
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
@@ -190,3 +198,13 @@ fun CIImageView(
|
||||
loadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
private val imageLoader = ImageLoader.Builder(SimplexApp.context)
|
||||
.components {
|
||||
if (SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
@@ -55,10 +56,21 @@ fun ChatItemView(
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = alignment,
|
||||
) {
|
||||
val onClick = {
|
||||
when (cItem.meta.itemStatus) {
|
||||
is CIStatus.SndErrorAuth -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(R.string.message_delivery_error_desc))
|
||||
}
|
||||
is CIStatus.SndError -> {
|
||||
showMsgDeliveryErrorAlert(generalGetString(R.string.unknown_error) + ": ${cItem.meta.itemStatus.agentError.string}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
|
||||
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick)
|
||||
) {
|
||||
@Composable fun ContentItem() {
|
||||
if (cItem.file == null && cItem.quotedItem == null && isShortEmoji(cItem.content.text)) {
|
||||
@@ -81,7 +93,11 @@ fun ChatItemView(
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(R.string.share_verb), Icons.Outlined.Share, onClick = {
|
||||
shareText(cxt, cItem.content.text)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
|
||||
when {
|
||||
filePath != null -> shareFile(cxt, cItem.text, filePath)
|
||||
else -> shareText(cxt, cItem.content.text)
|
||||
}
|
||||
showMenu.value = false
|
||||
})
|
||||
ItemAction(stringResource(R.string.copy_verb), Icons.Outlined.ContentCopy, onClick = {
|
||||
@@ -152,8 +168,10 @@ fun ChatItemView(
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIGroupEventView(cItem)
|
||||
is CIContent.SndGroupEventContent -> CIGroupEventView(cItem)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndGroupEventContent -> CIEventView(cItem)
|
||||
is CIContent.RcvConnEventContent -> CIEventView(cItem)
|
||||
is CIContent.SndConnEventContent -> CIEventView(cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,6 +221,13 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, deleteMessage: (Long, CIDeleteM
|
||||
)
|
||||
}
|
||||
|
||||
private fun showMsgDeliveryErrorAlert(description: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.message_delivery_error_title),
|
||||
text = description,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatItemView() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import CIImageView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -10,16 +9,17 @@ import androidx.compose.material.icons.filled.InsertDriveFile
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
@@ -69,7 +69,10 @@ fun FramedItemView(
|
||||
Modifier
|
||||
.background(if (sent) SentQuoteColorLight else ReceivedQuoteColorLight)
|
||||
.fillMaxWidth()
|
||||
.clickable { scrollToItem(qi.itemId?: return@clickable) }
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = { scrollToItem(qi.itemId?: return@combinedClickable) }
|
||||
)
|
||||
) {
|
||||
when (qi.content) {
|
||||
is MsgContent.MCImage -> {
|
||||
@@ -102,46 +105,58 @@ fun FramedItemView(
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = if (sent) SentColorLight else ReceivedColorLight
|
||||
) {
|
||||
@Composable
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
|
||||
if (text != "") {
|
||||
CIMarkdownText(ci, showMember, uriHandler)
|
||||
}
|
||||
}
|
||||
|
||||
val transparentBackground = ci.content.msgContent is MsgContent.MCImage && ci.content.text.isEmpty() && ci.quotedItem == null
|
||||
Box(Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.background(
|
||||
when {
|
||||
transparentBackground -> Color.Transparent
|
||||
sent -> SentColorLight
|
||||
else -> ReceivedColorLight
|
||||
}
|
||||
)) {
|
||||
var metaColor = HighOrLowlight
|
||||
Box(contentAlignment = Alignment.BottomEnd) {
|
||||
Column(Modifier.width(IntrinsicSize.Max)) {
|
||||
val qi = ci.quotedItem
|
||||
if (qi != null) {
|
||||
ciQuoteView(qi)
|
||||
}
|
||||
if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(bottom = 2.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(ci.content.text)
|
||||
Text("")
|
||||
PriorityLayout(Modifier, CHAT_IMAGE_LAYOUT_ID) {
|
||||
ci.quotedItem?.let { ciQuoteView(it) }
|
||||
if (ci.file == null && ci.formattedText == null && isShortEmoji(ci.content.text)) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(bottom = 2.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(ci.content.text)
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
} else {
|
||||
when (val mc = ci.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@Box, showMenu, receiveFile)
|
||||
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
|
||||
if (mc.text == "") {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, showMember, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, receiveFile)
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, showMember, uriHandler)
|
||||
is MsgContent.MCFile -> ciFileView(ci, mc.text)
|
||||
is MsgContent.MCUnknown ->
|
||||
if (ci.file == null) {
|
||||
CIMarkdownText(ci, showMember, uriHandler, onLinkLongClick)
|
||||
} else {
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview)
|
||||
CIMarkdownText(ci, showMember, uriHandler, onLinkLongClick)
|
||||
@@ -174,6 +189,37 @@ fun CIMarkdownText(
|
||||
}
|
||||
}
|
||||
|
||||
const val CHAT_IMAGE_LAYOUT_ID = "chatImage"
|
||||
|
||||
@Composable
|
||||
fun PriorityLayout(
|
||||
modifier: Modifier = Modifier,
|
||||
priorityLayoutId: String,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Layout(
|
||||
content = content,
|
||||
modifier = modifier
|
||||
) { measureable, constraints ->
|
||||
// Find important element which should tell what max width other elements can use
|
||||
// Expecting only one such element. Can be less than one but not more
|
||||
val imagePlaceable = measureable.firstOrNull { it.layoutId == priorityLayoutId }?.measure(constraints)
|
||||
val placeables: List<Placeable> = measureable.fastMap {
|
||||
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 }) {
|
||||
var y = 0
|
||||
placeables.forEach {
|
||||
it.place(0, y)
|
||||
y += it.measuredHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditedProvider: PreviewParameterProvider<Boolean> {
|
||||
override val values = listOf(false, true).asSequence()
|
||||
}
|
||||
|
||||
+12
-2
@@ -13,7 +13,9 @@ import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.helpers.*
|
||||
@@ -25,6 +27,7 @@ import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import com.google.accompanist.pager.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
interface ImageGalleryProvider {
|
||||
val initialIndex: Int
|
||||
@@ -88,6 +91,8 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var translationX by remember { mutableStateOf(0f) }
|
||||
var translationY by remember { mutableStateOf(0f) }
|
||||
var viewWidth by remember { mutableStateOf(0) }
|
||||
var allowTranslate by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(settledCurrentPage) {
|
||||
scale = 1f
|
||||
translationX = 0f
|
||||
@@ -113,6 +118,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
viewWidth = it.size.width
|
||||
}
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
@@ -121,12 +129,14 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures(
|
||||
{ allowTranslate },
|
||||
onGesture = { _, pan, gestureZoom, _ ->
|
||||
scale = (scale * gestureZoom).coerceIn(1f, 20f)
|
||||
if (scale > 1) {
|
||||
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
|
||||
if (scale > 1 && allowTranslate) {
|
||||
translationX += pan.x * scale
|
||||
translationY += pan.y * scale
|
||||
} else {
|
||||
} else if (allowTranslate) {
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
|
||||
@@ -60,8 +60,7 @@ fun MarkdownText (
|
||||
else -> " "
|
||||
}
|
||||
CompositionLocalProvider(
|
||||
// When we draw `sender` is has issues on LTR languages set globally with RTL text language
|
||||
LocalLayoutDirection provides if (textLayoutDirection != LocalLayoutDirection.current && sender == null)
|
||||
LocalLayoutDirection provides if (textLayoutDirection != LocalLayoutDirection.current)
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Ltr) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
else
|
||||
LocalLayoutDirection.current
|
||||
|
||||
+69
-13
@@ -5,7 +5,7 @@ import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -29,12 +29,13 @@ import kotlinx.datetime.Clock
|
||||
@Composable
|
||||
fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
var showMarkRead by remember { mutableStateOf(false) }
|
||||
val showMarkRead = remember(chat.chatStats.unreadCount, chat.chatStats.unreadChat) {
|
||||
chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
|
||||
}
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
LaunchedEffect(chat.id, chat.chatStats.unreadCount > 0) {
|
||||
LaunchedEffect(chat.id) {
|
||||
showMenu.value = false
|
||||
delay(500L)
|
||||
showMarkRead = chat.chatStats.unreadCount > 0
|
||||
}
|
||||
when (chat.chatInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
@@ -123,6 +124,8 @@ suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
} else {
|
||||
MarkUnreadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
ClearChatAction(chat, chatModel, showMenu)
|
||||
@@ -141,6 +144,8 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
|
||||
else -> {
|
||||
if (showMarkRead) {
|
||||
MarkReadChatAction(chat, chatModel, showMenu)
|
||||
} else {
|
||||
MarkUnreadChatAction(chat, chatModel, showMenu)
|
||||
}
|
||||
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
|
||||
ClearChatAction(chat, chatModel, showMenu)
|
||||
@@ -167,6 +172,30 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
DropdownMenuItem({
|
||||
markChatUnread(chat, chatModel)
|
||||
showMenu.value = false
|
||||
}) {
|
||||
Row {
|
||||
Text(
|
||||
stringResource(R.string.mark_unread),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
.padding(end = 15.dp),
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Icon(
|
||||
Icons.Outlined.MarkChatUnread,
|
||||
stringResource(R.string.mark_unread),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: Boolean, showMenu: MutableState<Boolean>) {
|
||||
ItemAction(
|
||||
@@ -290,18 +319,45 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel:
|
||||
)
|
||||
}
|
||||
|
||||
fun markChatRead(chat: Chat, chatModel: ChatModel) {
|
||||
// Just to be sure
|
||||
if (chat.chatStats.unreadCount == 0) return
|
||||
|
||||
val minUnreadItemId = chat.chatStats.minUnreadItemId
|
||||
chatModel.markChatItemsRead(chat.chatInfo)
|
||||
fun markChatRead(c: Chat, chatModel: ChatModel) {
|
||||
var chat = c
|
||||
withApi {
|
||||
chatModel.controller.apiChatRead(
|
||||
if (chat.chatStats.unreadCount > 0) {
|
||||
val minUnreadItemId = chat.chatStats.minUnreadItemId
|
||||
chatModel.markChatItemsRead(chat.chatInfo)
|
||||
chatModel.controller.apiChatRead(
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
CC.ItemRange(minUnreadItemId, chat.chatItems.last().id)
|
||||
)
|
||||
chat = chatModel.getChat(chat.id) ?: return@withApi
|
||||
}
|
||||
if (chat.chatStats.unreadChat) {
|
||||
val success = chatModel.controller.apiChatUnread(
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
false
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun markChatUnread(chat: Chat, chatModel: ChatModel) {
|
||||
// Just to be sure
|
||||
if (chat.chatStats.unreadChat) return
|
||||
|
||||
withApi {
|
||||
val success = chatModel.controller.apiChatUnread(
|
||||
chat.chatInfo.chatType,
|
||||
chat.chatInfo.apiId,
|
||||
CC.ItemRange(minUnreadItemId, chat.chatItems.last().id)
|
||||
true
|
||||
)
|
||||
if (success) {
|
||||
chatModel.replaceChat(chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +534,7 @@ fun ChatListNavLinkLayout(
|
||||
showMenu: MutableState<Boolean>,
|
||||
stopped: Boolean
|
||||
) {
|
||||
var modifier = Modifier.fillMaxWidth().heightIn(min = 88.dp)
|
||||
var modifier = Modifier.fillMaxWidth()
|
||||
if (!stopped) modifier = modifier.combinedClickable(onClick = click, onLongClick = { showMenu.value = true })
|
||||
Surface(modifier) {
|
||||
Row(
|
||||
|
||||
@@ -3,8 +3,7 @@ package chat.simplex.app.views.chatlist
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -203,14 +202,21 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, stop
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
private fun ChatList(chatModel: ChatModel, search: String) {
|
||||
val filter: (Chat) -> Boolean = { chat: Chat ->
|
||||
chat.chatInfo.chatViewName.lowercase().contains(search.lowercase())
|
||||
}
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
}
|
||||
val chats by remember(search) { derivedStateOf { if (search.isEmpty()) chatModel.chats else chatModel.chats.filter(filter) } }
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
listState
|
||||
) {
|
||||
items(chats) { chat ->
|
||||
ChatListNavLinkView(chat, chatModel)
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -84,7 +85,8 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
MarkdownText(
|
||||
ci.text,
|
||||
ci.formattedText,
|
||||
sender = null,
|
||||
sender = if (cInfo is ChatInfo.Group && !ci.chatDir.sent) ci.memberDisplayName else null,
|
||||
senderBold = true,
|
||||
metaText = null,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
@@ -121,7 +123,10 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
.weight(1F)
|
||||
) {
|
||||
chatPreviewTitle()
|
||||
chatPreviewText(chatModelIncognito)
|
||||
val height = with(LocalDensity.current) { 46.sp.toDp() }
|
||||
Row(Modifier.heightIn(min = height)) {
|
||||
chatPreviewText(chatModelIncognito)
|
||||
}
|
||||
}
|
||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt)
|
||||
|
||||
@@ -136,13 +141,13 @@ fun ChatPreviewView(chat: Chat, chatModelIncognito: Boolean, currentUserProfileD
|
||||
)
|
||||
val n = chat.chatStats.unreadCount
|
||||
val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group)
|
||||
if (n > 0) {
|
||||
if (n > 0 || chat.chatStats.unreadChat) {
|
||||
Box(
|
||||
Modifier.padding(top = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
unreadCountStr(n),
|
||||
if (n > 0) unreadCountStr(n) else "",
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier
|
||||
|
||||
+4
-1
@@ -8,9 +8,11 @@ import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
@@ -34,7 +36,8 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) {
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
Text(contactConnection.description, maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
val height = with(LocalDensity.current) { 46.sp.toDp() }
|
||||
Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
}
|
||||
val ts = getTimestampText(contactConnection.updatedAt)
|
||||
Column(
|
||||
|
||||
+4
-1
@@ -5,10 +5,12 @@ import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
@@ -31,7 +33,8 @@ fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.Con
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary
|
||||
)
|
||||
Text(stringResource(R.string.contact_wants_to_connect_with_you), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
val height = with(LocalDensity.current) { 46.sp.toDp() }
|
||||
Text(stringResource(R.string.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight)
|
||||
}
|
||||
val ts = getTimestampText(contactRequest.contactRequest.updatedAt)
|
||||
Column(
|
||||
|
||||
@@ -93,7 +93,7 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
Text(
|
||||
when (chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> stringResource(R.string.share_message)
|
||||
is SharedContent.Image -> stringResource(R.string.share_image)
|
||||
is SharedContent.Images -> stringResource(R.string.share_image)
|
||||
is SharedContent.File -> stringResource(R.string.share_file)
|
||||
else -> stringResource(R.string.share_message)
|
||||
},
|
||||
|
||||
@@ -94,8 +94,7 @@ private fun rememberSaveArchiveLauncher(cxt: Context, chatArchivePath: String):
|
||||
val contentResolver = cxt.contentResolver
|
||||
contentResolver.openOutputStream(destination)?.let { stream ->
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
val file = File(chatArchivePath)
|
||||
outputStream.write(file.readBytes())
|
||||
File(chatArchivePath).inputStream().use { it.copyTo(outputStream) }
|
||||
outputStream.close()
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -58,13 +58,13 @@ fun DatabaseView(
|
||||
val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) }
|
||||
val chatArchiveFile = remember { mutableStateOf<String?>(null) }
|
||||
val saveArchiveLauncher = rememberSaveArchiveLauncher(cxt = context, chatArchiveFile)
|
||||
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory(context))) }
|
||||
val importArchiveLauncher = rememberGetContentLauncher { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
importArchiveAlert(m, context, uri, progressIndicator)
|
||||
importArchiveAlert(m, context, uri, appFilesCountAndSize, progressIndicator)
|
||||
}
|
||||
}
|
||||
val chatDbDeleted = remember { m.chatDbDeleted }
|
||||
val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory(context))) }
|
||||
LaunchedEffect(m.chatRunning) {
|
||||
runChat.value = m.chatRunning.value ?: true
|
||||
}
|
||||
@@ -489,8 +489,7 @@ private fun rememberSaveArchiveLauncher(cxt: Context, chatArchiveFile: MutableSt
|
||||
val contentResolver = cxt.contentResolver
|
||||
contentResolver.openOutputStream(destination)?.let { stream ->
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
val file = File(filePath)
|
||||
outputStream.write(file.readBytes())
|
||||
File(filePath).inputStream().use { it.copyTo(outputStream) }
|
||||
outputStream.close()
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -507,16 +506,28 @@ private fun rememberSaveArchiveLauncher(cxt: Context, chatArchiveFile: MutableSt
|
||||
}
|
||||
)
|
||||
|
||||
private fun importArchiveAlert(m: ChatModel, context: Context, importedArchiveUri: Uri, progressIndicator: MutableState<Boolean>) {
|
||||
private fun importArchiveAlert(
|
||||
m: ChatModel,
|
||||
context: Context,
|
||||
importedArchiveUri: Uri,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
progressIndicator: MutableState<Boolean>
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.import_database_question),
|
||||
text = generalGetString(R.string.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one),
|
||||
confirmText = generalGetString(R.string.import_database_confirmation),
|
||||
onConfirm = { importArchive(m, context, importedArchiveUri, progressIndicator) }
|
||||
onConfirm = { importArchive(m, context, importedArchiveUri, appFilesCountAndSize, progressIndicator) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun importArchive(m: ChatModel, context: Context, importedArchiveUri: Uri, progressIndicator: MutableState<Boolean>) {
|
||||
private fun importArchive(
|
||||
m: ChatModel,
|
||||
context: Context,
|
||||
importedArchiveUri: Uri,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
progressIndicator: MutableState<Boolean>
|
||||
) {
|
||||
progressIndicator.value = true
|
||||
val archivePath = saveArchiveFromUri(context, importedArchiveUri)
|
||||
if (archivePath != null) {
|
||||
@@ -527,6 +538,7 @@ private fun importArchive(m: ChatModel, context: Context, importedArchiveUri: Ur
|
||||
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
|
||||
m.controller.apiImportArchive(config)
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
|
||||
}
|
||||
|
||||
@@ -3,25 +3,20 @@ package chat.simplex.app.views.helpers
|
||||
import android.util.Log
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
|
||||
class AlertManager {
|
||||
var alertView = mutableStateOf<(@Composable () -> Unit)?>(null)
|
||||
var presentAlert = mutableStateOf<Boolean>(false)
|
||||
var alertViews = mutableStateListOf<(@Composable () -> Unit)>()
|
||||
|
||||
fun showAlert(alert: @Composable () -> Unit) {
|
||||
Log.d(TAG, "AlertManager.showAlert")
|
||||
alertView.value = alert
|
||||
presentAlert.value = true
|
||||
alertViews.add(alert)
|
||||
}
|
||||
|
||||
fun hideAlert() {
|
||||
presentAlert.value = false
|
||||
alertView.value = null
|
||||
alertViews.removeLastOrNull()
|
||||
}
|
||||
|
||||
fun showAlertDialogButtons(
|
||||
@@ -101,7 +96,7 @@ class AlertManager {
|
||||
|
||||
@Composable
|
||||
fun showInView() {
|
||||
if (presentAlert.value) alertView.value?.invoke()
|
||||
remember { alertViews }.lastOrNull()?.invoke()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -7,8 +7,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
sealed class SharedContent {
|
||||
data class Text(val text: String): SharedContent()
|
||||
data class Image(val uri: Uri): SharedContent()
|
||||
data class File(val uri: Uri): SharedContent()
|
||||
data class Images(val text: String, val uris: List<Uri>): SharedContent()
|
||||
data class File(val text: String, val uri: Uri): SharedContent()
|
||||
}
|
||||
|
||||
enum class NewChatSheetState {
|
||||
@@ -33,6 +33,6 @@ enum class NewChatSheetState {
|
||||
}
|
||||
|
||||
sealed class UploadContent {
|
||||
data class SimpleImage(val bitmap: Bitmap): UploadContent()
|
||||
data class SimpleImage(val uri: Uri): UploadContent()
|
||||
data class AnimatedImage(val uri: Uri): UploadContent()
|
||||
}
|
||||
|
||||
@@ -214,13 +214,14 @@ fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit)
|
||||
}
|
||||
|
||||
suspend fun PointerInputScope.detectTransformGestures(
|
||||
allowIntercept: () -> Boolean,
|
||||
panZoomLock: Boolean = false,
|
||||
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
|
||||
) {
|
||||
var zoom = 1f
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
var rotation = 0f
|
||||
var zoom = 1f
|
||||
var pan = Offset.Zero
|
||||
var pastTouchSlop = false
|
||||
val touchSlop = viewConfiguration.touchSlop
|
||||
@@ -264,7 +265,7 @@ suspend fun PointerInputScope.detectTransformGestures(
|
||||
onGesture(centroid, panChange, zoomChange, effectiveRotation)
|
||||
}
|
||||
event.changes.fastForEach {
|
||||
if (it.positionChanged() && zoomChange != 1f) {
|
||||
if (it.positionChanged() && zoom != 1f && allowIntercept()) {
|
||||
it.consume()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ package chat.simplex.app.views.helpers
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.*
|
||||
import android.net.Uri
|
||||
@@ -20,8 +19,9 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Collections
|
||||
import androidx.compose.material.icons.outlined.PhotoCamera
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -31,7 +31,12 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.json
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
import chat.simplex.app.views.chat.PickFromGallery
|
||||
import chat.simplex.app.views.newchat.ActionButton
|
||||
import kotlinx.serialization.builtins.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import kotlin.math.min
|
||||
@@ -65,26 +70,28 @@ fun resizeImageToStrSize(image: Bitmap, maxDataSize: Long): String {
|
||||
}
|
||||
|
||||
private fun compressImageStr(bitmap: Bitmap): String {
|
||||
return "data:image/jpg;base64," + Base64.encodeToString(compressImageData(bitmap).toByteArray(), Base64.NO_WRAP)
|
||||
val usePng = bitmap.hasAlpha()
|
||||
val ext = if (usePng) "png" else "jpg"
|
||||
return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun resizeImageToDataSize(image: Bitmap, maxDataSize: Long): ByteArrayOutputStream {
|
||||
fun resizeImageToDataSize(image: Bitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream {
|
||||
var img = image
|
||||
var stream = compressImageData(img)
|
||||
var stream = compressImageData(img, usePng)
|
||||
while (stream.size() > maxDataSize) {
|
||||
val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble())
|
||||
val clippedRatio = min(ratio, 2.0)
|
||||
val width = (img.width.toDouble() / clippedRatio).toInt()
|
||||
val height = img.height * width / img.width
|
||||
img = Bitmap.createScaledBitmap(img, width, height, true)
|
||||
stream = compressImageData(img)
|
||||
stream = compressImageData(img, usePng)
|
||||
}
|
||||
return stream
|
||||
}
|
||||
|
||||
private fun compressImageData(bitmap: Bitmap): ByteArrayOutputStream {
|
||||
private fun compressImageData(bitmap: Bitmap, usePng: Boolean): ByteArrayOutputStream {
|
||||
val stream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, stream)
|
||||
bitmap.compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream)
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -104,15 +111,12 @@ fun base64ToBitmap(base64ImageString: String): Bitmap {
|
||||
}
|
||||
}
|
||||
|
||||
class CustomTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
|
||||
private var uri: Uri? = null
|
||||
private var tmpFile: File? = null
|
||||
lateinit var externalContext: Context
|
||||
|
||||
class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() {
|
||||
@CallSuper
|
||||
override fun createIntent(context: Context, input: Void?): Intent {
|
||||
externalContext = context
|
||||
tmpFile = File.createTempFile("image", ".bmp", context.filesDir)
|
||||
// Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system
|
||||
tmpFile?.deleteOnExit()
|
||||
uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!)
|
||||
return Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
.putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||
@@ -121,20 +125,28 @@ class CustomTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
|
||||
override fun getSynchronousResult(
|
||||
context: Context,
|
||||
input: Void?
|
||||
): SynchronousResult<Bitmap?>? = null
|
||||
): SynchronousResult<Uri?>? = null
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? {
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Uri? {
|
||||
return if (resultCode == Activity.RESULT_OK && uri != null) {
|
||||
val source = ImageDecoder.createSource(externalContext.contentResolver, uri!!)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
tmpFile?.delete()
|
||||
bitmap
|
||||
uri
|
||||
} else {
|
||||
Log.e(TAG, "Getting image from camera cancelled or failed.")
|
||||
tmpFile?.delete()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun saver(): Saver<CustomTakePicturePreview, *> = Saver(
|
||||
save = { json.encodeToString(ListSerializer(String.serializer().nullable), listOf(it.uri?.toString(), it.tmpFile?.toString())) },
|
||||
restore = {
|
||||
val data = json.decodeFromString(ListSerializer(String.serializer().nullable), it)
|
||||
val uri = if (data[0] != null) Uri.parse(data[0]) else null
|
||||
val tmpFile = if (data[1] != null) File(data[1]) else null
|
||||
CustomTakePicturePreview(uri, tmpFile)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
//class GetGalleryContent: ActivityResultContracts.GetContent() {
|
||||
// override fun createIntent(context: Context, input: String): Intent {
|
||||
@@ -146,8 +158,12 @@ class CustomTakePicturePreview: ActivityResultContract<Void?, Bitmap?>() {
|
||||
//fun rememberGalleryLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher<String, Uri?> =
|
||||
// rememberLauncherForActivityResult(contract = GetGalleryContent(), cb)
|
||||
@Composable
|
||||
fun rememberCameraLauncher(cb: (Bitmap?) -> Unit): ManagedActivityResultLauncher<Void?, Bitmap?> =
|
||||
rememberLauncherForActivityResult(contract = CustomTakePicturePreview(), cb)
|
||||
fun rememberCameraLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLauncher<Void?, Uri?> {
|
||||
val contract = rememberSaveable(stateSaver = CustomTakePicturePreview.saver()) {
|
||||
mutableStateOf(CustomTakePicturePreview(null, null))
|
||||
}
|
||||
return rememberLauncherForActivityResult(contract = contract.value, cb)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberPermissionLauncher(cb: (Boolean) -> Unit): ManagedActivityResultLauncher<String, Boolean> =
|
||||
@@ -163,22 +179,26 @@ fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivit
|
||||
|
||||
@Composable
|
||||
fun GetImageBottomSheet(
|
||||
imageBitmap: MutableState<Bitmap?>,
|
||||
imageBitmap: MutableState<Uri?>,
|
||||
onImageChange: (Bitmap) -> Unit,
|
||||
hideBottomSheet: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val galleryLauncher = rememberGetContentLauncher { uri: Uri? ->
|
||||
val processPickedImage = { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val source = ImageDecoder.createSource(context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
imageBitmap.value = bitmap
|
||||
imageBitmap.value = uri
|
||||
onImageChange(bitmap)
|
||||
}
|
||||
}
|
||||
val cameraLauncher = rememberCameraLauncher { bitmap: Bitmap? ->
|
||||
if (bitmap != null) {
|
||||
imageBitmap.value = bitmap
|
||||
val galleryLauncher = rememberLauncherForActivityResult(contract = PickFromGallery()) { processPickedImage(it) }
|
||||
val galleryLauncherFallback = rememberGetContentLauncher { processPickedImage(it) }
|
||||
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
imageBitmap.value = uri
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
onImageChange(bitmap)
|
||||
}
|
||||
}
|
||||
@@ -217,7 +237,11 @@ fun GetImageBottomSheet(
|
||||
}
|
||||
}
|
||||
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
|
||||
galleryLauncher.launch("image/*")
|
||||
try {
|
||||
galleryLauncher.launch(0)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
galleryLauncherFallback.launch("image/*")
|
||||
}
|
||||
hideBottomSheet()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,29 +26,44 @@ import chat.simplex.app.views.chat.item.SentColorLight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jsoup.Jsoup
|
||||
import java.net.URL
|
||||
|
||||
private const val OG_SELECT_QUERY = "meta[property^=og:]"
|
||||
private const val ICON_SELECT_QUERY = "link[rel^=icon],link[rel^=apple-touch-icon],link[rel^=shortcut icon]"
|
||||
private val IMAGE_SUFFIXES = listOf(".jpg", ".png", ".ico", ".webp", ".gif")
|
||||
|
||||
suspend fun getLinkPreview(url: String): LinkPreview? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response = Jsoup.connect(url)
|
||||
.ignoreContentType(true)
|
||||
.timeout(10000)
|
||||
.followRedirects(true)
|
||||
.execute()
|
||||
val doc = response.parse()
|
||||
val ogTags = doc.select(OG_SELECT_QUERY)
|
||||
val imageUri = ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content")
|
||||
val title: String?
|
||||
val u = kotlin.runCatching { URL(url) }.getOrNull() ?: return@withContext null
|
||||
var imageUri = when {
|
||||
IMAGE_SUFFIXES.any { u.path.lowercase().endsWith(it) } -> {
|
||||
title = u.path.substringAfterLast("/")
|
||||
url
|
||||
}
|
||||
else -> {
|
||||
val response = Jsoup.connect(url)
|
||||
.ignoreContentType(true)
|
||||
.timeout(10000)
|
||||
.followRedirects(true)
|
||||
.execute()
|
||||
val doc = response.parse()
|
||||
val ogTags = doc.select(OG_SELECT_QUERY)
|
||||
title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content") ?: doc.title()
|
||||
ogTags.firstOrNull { it.attr("property") == "og:image" }?.attr("content")
|
||||
?: doc.select(ICON_SELECT_QUERY).firstOrNull { it.attr("rel").contains("icon") }?.attr("href")
|
||||
}
|
||||
}
|
||||
if (imageUri != null) {
|
||||
imageUri = normalizeImageUri(u, imageUri)
|
||||
try {
|
||||
val stream = java.net.URL(imageUri).openStream()
|
||||
val stream = URL(imageUri).openStream()
|
||||
val image = resizeImageToStrSize(BitmapFactory.decodeStream(stream), maxDataSize = 14000)
|
||||
// TODO add once supported in iOS
|
||||
// val description = ogTags.firstOrNull {
|
||||
// it.attr("property") == "og:description"
|
||||
// }?.attr("content") ?: ""
|
||||
val title = ogTags.firstOrNull { it.attr("property") == "og:title" }?.attr("content")
|
||||
if (title != null) {
|
||||
return@withContext LinkPreview(url, title, description = "", image)
|
||||
}
|
||||
@@ -63,8 +78,6 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) {
|
||||
Row(
|
||||
@@ -127,6 +140,37 @@ fun ChatItemLinkView(linkPreview: LinkPreview) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeImageUri(u: URL, imageUri: String) = when {
|
||||
!imageUri.lowercase().startsWith("http") -> {
|
||||
"${u.protocol}://${u.host}" +
|
||||
if (imageUri.startsWith("/"))
|
||||
imageUri
|
||||
else
|
||||
// When an icon is used as an image with relative href path like: site=site.com, <link rel="icon" href="icon.png">
|
||||
if (u.path.endsWith("/")) u.path + imageUri
|
||||
else u.path.substringBeforeLast("/") + "/$imageUri"
|
||||
}
|
||||
else -> imageUri
|
||||
}
|
||||
|
||||
/*fun normalizeImageUriTest() {
|
||||
val expect = mapOf<Pair<URL, String>, String>(
|
||||
URL("https://example.com") to "icon.png" to "https://example.com/icon.png",
|
||||
URL("https://example.com/") to "icon.png" to "https://example.com/icon.png",
|
||||
URL("https://example.com/") to "/icon.png" to "https://example.com/icon.png",
|
||||
URL("https://example.com") to "assets/images/favicon.png" to "https://example.com/assets/images/favicon.png",
|
||||
URL("https://example.com/") to "assets/images/favicon.png" to "https://example.com/assets/images/favicon.png",
|
||||
URL("https://example.com/dir") to "/favicon.png" to "https://example.com/favicon.png",
|
||||
URL("https://example.com/dir/") to "favicon.png" to "https://example.com/dir/favicon.png",
|
||||
URL("https://example.com/dir/") to "/favicon.png" to "https://example.com/favicon.png",
|
||||
URL("https://example.com/dir/page") to "favicon.png" to "https://example.com/dir/favicon.png",
|
||||
URL("https://example.com/abcde.gif") to "https://example.com/abcde.gif" to "https://example.com/abcde.gif",
|
||||
URL("https://example.com/abcde.gif?a=b") to "https://example.com/abcde.gif?a=b" to "https://example.com/abcde.gif?a=b",
|
||||
)
|
||||
expect.forEach {
|
||||
Log.d(TAG, "Image URI ${normalizeImageUri(it.key.first, it.key.second)} == ${normalizeImageUri(it.key.first, it.key.second) == it.value}")
|
||||
}
|
||||
}*/
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
|
||||
@@ -89,18 +89,6 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume)
|
||||
)
|
||||
|
||||
fun laErrorToast(context: Context, errString: CharSequence) = Toast.makeText(
|
||||
context,
|
||||
if (errString.isNotEmpty()) String.format(generalGetString(R.string.auth_error_w_desc), errString) else generalGetString(R.string.auth_error),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
fun laFailedToast(context: Context) = Toast.makeText(
|
||||
context,
|
||||
generalGetString(R.string.auth_failed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.auth_unavailable),
|
||||
generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled)
|
||||
|
||||
@@ -60,6 +60,8 @@ class ModalManager {
|
||||
modalCount.value = modalViews.size - toRemove.size
|
||||
}
|
||||
|
||||
fun hasModalsOpen() = modalCount.value > 0
|
||||
|
||||
fun closeModal() {
|
||||
if (modalViews.isNotEmpty()) {
|
||||
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
|
||||
|
||||
@@ -40,6 +40,12 @@ fun SearchTextField(modifier: Modifier, placeholder: String, onValueChange: (Str
|
||||
keyboard?.show()
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (searchText.text.isNotEmpty()) onValueChange("")
|
||||
}
|
||||
}
|
||||
|
||||
val enabled = true
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
|
||||
@@ -3,13 +3,15 @@ package chat.simplex.app.views.helpers
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.simplex.app.R
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.CIFile
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
@@ -24,6 +26,22 @@ fun shareText(cxt: Context, text: String) {
|
||||
cxt.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
fun shareFile(cxt: Context, text: String, filePath: String) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
val ext = filePath.substringAfterLast(".")
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
|
||||
val sendIntent: Intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
/*if (text.isNotEmpty()) {
|
||||
putExtra(Intent.EXTRA_TEXT, text)
|
||||
}*/
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = mimeType
|
||||
}
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
cxt.startActivity(shareIntent)
|
||||
}
|
||||
|
||||
fun copyText(cxt: Context, text: String) {
|
||||
val clipboard = ContextCompat.getSystemService(cxt, ClipboardManager::class.java)
|
||||
clipboard?.setPrimaryClip(ClipData.newPlainText("text", text))
|
||||
@@ -40,8 +58,7 @@ fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResu
|
||||
val contentResolver = cxt.contentResolver
|
||||
contentResolver.openOutputStream(destination)?.let { stream ->
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
val file = File(filePath)
|
||||
outputStream.write(file.readBytes())
|
||||
File(filePath).inputStream().use { it.copyTo(outputStream) }
|
||||
outputStream.close()
|
||||
Toast.makeText(cxt, generalGetString(R.string.file_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -52,30 +69,32 @@ fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResu
|
||||
}
|
||||
)
|
||||
|
||||
fun imageMimeType(fileName: String): String {
|
||||
val lowercaseName = fileName.lowercase()
|
||||
return when {
|
||||
lowercaseName.endsWith(".png") -> "image/png"
|
||||
lowercaseName.endsWith(".gif") -> "image/gif"
|
||||
lowercaseName.endsWith(".webp") -> "image/webp"
|
||||
lowercaseName.endsWith(".avif") -> "image/avif"
|
||||
lowercaseName.endsWith(".svg") -> "image/svg+xml"
|
||||
else -> "image/jpeg"
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(cxt: Context, ciFile: CIFile?) {
|
||||
val filePath = getLoadedFilePath(cxt, ciFile)
|
||||
val fileName = ciFile?.fileName
|
||||
if (filePath != null && fileName != null) {
|
||||
val values = ContentValues()
|
||||
val lowercaseName = fileName.lowercase()
|
||||
val mimeType = when {
|
||||
lowercaseName.endsWith(".png") -> "image/png"
|
||||
lowercaseName.endsWith(".gif") -> "image/gif"
|
||||
lowercaseName.endsWith(".webp") -> "image/webp"
|
||||
lowercaseName.endsWith(".avif") -> "image/avif"
|
||||
lowercaseName.endsWith(".svg") -> "image/svg+xml"
|
||||
else -> "image/jpeg"
|
||||
}
|
||||
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
|
||||
values.put(MediaStore.Images.Media.MIME_TYPE, imageMimeType(fileName))
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
values.put(MediaStore.MediaColumns.TITLE, fileName)
|
||||
val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
|
||||
uri?.let {
|
||||
cxt.contentResolver.openOutputStream(uri)?.let { stream ->
|
||||
val outputStream = BufferedOutputStream(stream)
|
||||
val file = File(filePath)
|
||||
outputStream.write(file.readBytes())
|
||||
File(filePath).inputStream().use { it.copyTo(outputStream) }
|
||||
outputStream.close()
|
||||
Toast.makeText(cxt, generalGetString(R.string.image_saved), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@@ -28,6 +28,22 @@ fun SimpleButton(text: String, icon: ImageVector,
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButtonIconEnded(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
color: Color = MaterialTheme.colors.primary,
|
||||
click: () -> Unit
|
||||
) {
|
||||
SimpleButtonFrame(click) {
|
||||
Text(text, style = MaterialTheme.typography.caption, color = color)
|
||||
Icon(
|
||||
icon, text, tint = color,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButtonFrame(click: () -> Unit, disabled: Boolean = false, content: @Composable () -> Unit) {
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
|
||||
@@ -14,7 +14,9 @@ import android.text.SpannedString
|
||||
import android.text.style.*
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -69,6 +71,9 @@ fun getKeyboardState(): State<KeyboardState> {
|
||||
return keyboardState
|
||||
}
|
||||
|
||||
fun hideKeyboard(view: View) =
|
||||
(SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0)
|
||||
|
||||
// Resource to annotated string from
|
||||
// https://stackoverflow.com/questions/68549248/android-jetpack-compose-how-to-show-styled-text-from-string-resources
|
||||
fun generalGetString(id: Int): String {
|
||||
@@ -306,11 +311,18 @@ fun getFileSize(context: Context, uri: Uri): Long? {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveImage(context: Context, uri: Uri): String? {
|
||||
val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri)
|
||||
val bitmap = ImageDecoder.decodeBitmap(source)
|
||||
return saveImage(context, bitmap)
|
||||
}
|
||||
|
||||
fun saveImage(context: Context, image: Bitmap): String? {
|
||||
return try {
|
||||
val dataResized = resizeImageToDataSize(image, maxDataSize = MAX_IMAGE_SIZE)
|
||||
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}.jpg")
|
||||
val fileToSave = uniqueCombine(context, "IMG_${timestamp}.$ext")
|
||||
val file = File(getAppFilePath(context, fileToSave))
|
||||
val output = FileOutputStream(file)
|
||||
dataResized.writeTo(output)
|
||||
|
||||
@@ -24,16 +24,8 @@ import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun AddContactView(chatModel: ChatModel, connReqInvitation: String, connIncognito: Boolean) {
|
||||
fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
|
||||
val cxt = LocalContext.current
|
||||
LaunchedEffect(connReqInvitation) {
|
||||
if (connReqInvitation.isNotEmpty()) {
|
||||
chatModel.connReqInv.value = connReqInvitation
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { chatModel.connReqInv.value = null }
|
||||
}
|
||||
AddContactLayout(
|
||||
connReq = connReqInvitation,
|
||||
connIncognito = connIncognito,
|
||||
@@ -83,7 +75,7 @@ fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit)
|
||||
annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
|
||||
lineHeight = 22.sp,
|
||||
modifier = Modifier
|
||||
.padding(bottom = if (screenHeight > 600.dp) 8.dp else 0.dp)
|
||||
.padding(top = 16.dp, bottom = if (screenHeight > 600.dp) 16.dp else 0.dp)
|
||||
)
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -8,6 +9,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
@@ -60,8 +62,8 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
|
||||
val scope = rememberCoroutineScope()
|
||||
val displayName = remember { mutableStateOf("") }
|
||||
val fullName = remember { mutableStateOf("") }
|
||||
val profileImage = remember { mutableStateOf<String?>(null) }
|
||||
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
|
||||
+19
-25
@@ -30,31 +30,27 @@ fun ContactConnectionInfoView(
|
||||
focusAlias: Boolean,
|
||||
close: () -> Unit
|
||||
) {
|
||||
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. It will be managed by [AddContactView] itself
|
||||
* Otherwise it will be called here AFTER [AddContactView] is launched and will clear the value too soon */
|
||||
val allowDispose = remember { mutableStateOf(true) }
|
||||
LaunchedEffect(connReqInvitation) {
|
||||
allowDispose.value = true
|
||||
chatModel.connReqInv.value = connReqInvitation
|
||||
}
|
||||
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
|
||||
* Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon.
|
||||
* It will be dropped automatically when connection established or when user goes away from this screen.
|
||||
**/
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (allowDispose.value) {
|
||||
if (!ModalManager.shared.hasModalsOpen()) {
|
||||
chatModel.connReqInv.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
ContactConnectionInfoLayout(
|
||||
connReq = connReqInvitation,
|
||||
contactConnection.localAlias,
|
||||
contactConnection.initiated,
|
||||
contactConnection.viaContactUri,
|
||||
contactConnection.incognito,
|
||||
contactConnection,
|
||||
focusAlias,
|
||||
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
|
||||
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
|
||||
showQr = {
|
||||
allowDispose.value = false
|
||||
ModalManager.shared.showModal {
|
||||
Column(
|
||||
Modifier
|
||||
@@ -62,7 +58,7 @@ fun ContactConnectionInfoView(
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
AddContactView(chatModel, connReqInvitation ?: return@showModal, contactConnection.incognito)
|
||||
AddContactView(connReqInvitation ?: return@showModal, contactConnection.incognito)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,10 +68,7 @@ fun ContactConnectionInfoView(
|
||||
@Composable
|
||||
private fun ContactConnectionInfoLayout(
|
||||
connReq: String?,
|
||||
localAlias: String,
|
||||
connectionInitiated: Boolean,
|
||||
connectionViaContactUri: Boolean,
|
||||
connectionIncognito: Boolean,
|
||||
contactConnection: PendingContactConnection,
|
||||
focusAlias: Boolean,
|
||||
deleteConnection: () -> Unit,
|
||||
onLocalAliasChanged: (String) -> Unit,
|
||||
@@ -87,23 +80,27 @@ private fun ContactConnectionInfoLayout(
|
||||
) {
|
||||
AppBarTitle(
|
||||
stringResource(
|
||||
if (connectionInitiated) R.string.you_invited_your_contact
|
||||
if (contactConnection.initiated) R.string.you_invited_your_contact
|
||||
else R.string.you_accepted_connection
|
||||
)
|
||||
)
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING)) {
|
||||
LocalAliasEditor(localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
|
||||
if (contactConnection.groupLinkId == null) {
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING)) {
|
||||
LocalAliasEditor(contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(
|
||||
if (connectionViaContactUri) R.string.you_will_be_connected_when_your_connection_request_is_accepted
|
||||
if (contactConnection.viaContactUri)
|
||||
if (contactConnection.groupLinkId != null) R.string.you_will_be_connected_when_group_host_device_is_online
|
||||
else R.string.you_will_be_connected_when_your_connection_request_is_accepted
|
||||
else R.string.you_will_be_connected_when_your_contacts_device_is_online
|
||||
),
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
|
||||
)
|
||||
SectionView {
|
||||
if (!connReq.isNullOrEmpty() && connectionInitiated) {
|
||||
ShowQrButton(connectionIncognito, showQr)
|
||||
if (!connReq.isNullOrEmpty() && contactConnection.initiated) {
|
||||
ShowQrButton(contactConnection.incognito, showQr)
|
||||
SectionDivider()
|
||||
}
|
||||
DeleteButton(deleteConnection)
|
||||
@@ -149,11 +146,8 @@ private fun setContactAlias(contactConnection: PendingContactConnection, localAl
|
||||
private fun PreviewContactConnectionInfoView() {
|
||||
SimpleXTheme {
|
||||
ContactConnectionInfoLayout(
|
||||
localAlias = "",
|
||||
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
connectionInitiated = true,
|
||||
connectionViaContactUri = true,
|
||||
connectionIncognito = false,
|
||||
PendingContactConnection.getSampleData(),
|
||||
focusAlias = false,
|
||||
deleteConnection = {},
|
||||
onLocalAliasChanged = {},
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -12,6 +13,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.helpers.ModalManager
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import chat.simplex.app.views.usersettings.UserAddressView
|
||||
|
||||
@@ -22,16 +24,27 @@ enum class CreateLinkTab {
|
||||
@Composable
|
||||
fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
val selection = remember { mutableStateOf(initialSelection) }
|
||||
val connReqInvitation = remember { mutableStateOf("") }
|
||||
val creatingConnReq = remember { mutableStateOf(false) }
|
||||
val connReqInvitation = rememberSaveable { m.connReqInv }
|
||||
val creatingConnReq = rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(selection.value) {
|
||||
if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() && !creatingConnReq.value) {
|
||||
if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() && !creatingConnReq.value) {
|
||||
createInvitation(m, creatingConnReq, connReqInvitation)
|
||||
}
|
||||
}
|
||||
/** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv].
|
||||
* Otherwise, it will be called here AFTER [AddContactView] is launched and will clear the value too soon.
|
||||
* It will be dropped automatically when connection established or when user goes away from this screen.
|
||||
**/
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (!ModalManager.shared.hasModalsOpen()) {
|
||||
m.connReqInv.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
val tabTitles = CreateLinkTab.values().map {
|
||||
when {
|
||||
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isEmpty() -> stringResource(R.string.create_one_time_link)
|
||||
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() -> stringResource(R.string.create_one_time_link)
|
||||
it == CreateLinkTab.ONE_TIME -> stringResource(R.string.one_time_link)
|
||||
it == CreateLinkTab.LONG_TERM -> stringResource(R.string.your_contact_address)
|
||||
else -> ""
|
||||
@@ -46,7 +59,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
when (selection.value) {
|
||||
CreateLinkTab.ONE_TIME -> {
|
||||
AddContactView(m, connReqInvitation.value, m.incognito.value)
|
||||
AddContactView(connReqInvitation.value ?: "", m.incognito.value)
|
||||
}
|
||||
CreateLinkTab.LONG_TERM -> {
|
||||
UserAddressView(m)
|
||||
@@ -79,7 +92,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createInvitation(m: ChatModel, creatingConnReq: MutableState<Boolean>, connReqInvitation: MutableState<String>) {
|
||||
private fun createInvitation(m: ChatModel, creatingConnReq: MutableState<Boolean>, connReqInvitation: MutableState<String?>) {
|
||||
creatingConnReq.value = true
|
||||
withApi {
|
||||
val connReq = m.controller.apiAddContact()
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.views.newchat
|
||||
import android.content.ClipboardManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
@@ -35,10 +37,21 @@ fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
connectViaLink = { connReqUri ->
|
||||
try {
|
||||
val uri = Uri.parse(connReqUri)
|
||||
withUriAction(uri) { action ->
|
||||
if (connectViaUri(chatModel, action, uri)) {
|
||||
close()
|
||||
withUriAction(uri) { linkType ->
|
||||
val action = suspend {
|
||||
Log.d(TAG, "connectViaUri: connecting")
|
||||
if (connectViaUri(chatModel, linkType, uri)) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
if (linkType == ConnectionLinkType.GROUP) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.connect_via_group_link),
|
||||
text = generalGetString(R.string.you_will_join_group),
|
||||
confirmText = generalGetString(R.string.connect_via_link_verb),
|
||||
onConfirm = { withApi { action() } }
|
||||
)
|
||||
} else action()
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
||||
+55
-10
@@ -3,6 +3,7 @@ package chat.simplex.app.views.newchat
|
||||
import android.Manifest
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
@@ -14,12 +15,17 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.json
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Composable
|
||||
fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
@@ -33,10 +39,21 @@ fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
QRCodeScanner { connReqUri ->
|
||||
try {
|
||||
val uri = Uri.parse(connReqUri)
|
||||
withUriAction(uri) { action ->
|
||||
if (connectViaUri(chatModel, action, uri)) {
|
||||
close()
|
||||
withUriAction(uri) { linkType ->
|
||||
val action = suspend {
|
||||
Log.d(TAG, "connectViaUri: connecting")
|
||||
if (connectViaUri(chatModel, linkType, uri)) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
if (linkType == ConnectionLinkType.GROUP) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.connect_via_group_link),
|
||||
text = generalGetString(R.string.you_will_join_group),
|
||||
confirmText = generalGetString(R.string.connect_via_link_verb),
|
||||
onConfirm = { withApi { action() } }
|
||||
)
|
||||
} else action()
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -49,10 +66,34 @@ fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) {
|
||||
)
|
||||
}
|
||||
|
||||
fun withUriAction(uri: Uri, run: suspend (String) -> Unit) {
|
||||
val action = uri.path?.drop(1)
|
||||
if (action == "contact" || action == "invitation") {
|
||||
withApi { run(action) }
|
||||
enum class ConnectionLinkType {
|
||||
CONTACT, INVITATION, GROUP
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CReqClientData {
|
||||
@Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData()
|
||||
}
|
||||
|
||||
fun withUriAction(uri: Uri, run: suspend (ConnectionLinkType) -> Unit) {
|
||||
val action = uri.path?.drop(1)?.replace("/", "")
|
||||
val data = uri.toString().replaceFirst("#/", "/").toUri().getQueryParameter("data")
|
||||
val type = when {
|
||||
data != null -> {
|
||||
val parsed = runCatching {
|
||||
json.decodeFromString(CReqClientData.serializer(), data)
|
||||
}
|
||||
when {
|
||||
parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
action == "contact" -> ConnectionLinkType.CONTACT
|
||||
action == "invitation" -> ConnectionLinkType.INVITATION
|
||||
else -> null
|
||||
}
|
||||
if (type != null) {
|
||||
withApi { run(type) }
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.invalid_contact_link),
|
||||
@@ -61,14 +102,17 @@ fun withUriAction(uri: Uri, run: suspend (String) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri): Boolean {
|
||||
suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: Uri): Boolean {
|
||||
val r = chatModel.controller.apiConnect(uri.toString())
|
||||
if (r) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.connection_request_sent),
|
||||
text =
|
||||
if (action == "contact") generalGetString(R.string.you_will_be_connected_when_your_connection_request_is_accepted)
|
||||
else generalGetString(R.string.you_will_be_connected_when_your_contacts_device_is_online)
|
||||
when (action) {
|
||||
ConnectionLinkType.CONTACT -> generalGetString(R.string.you_will_be_connected_when_your_connection_request_is_accepted)
|
||||
ConnectionLinkType.INVITATION -> generalGetString(R.string.you_will_be_connected_when_your_contacts_device_is_online)
|
||||
ConnectionLinkType.GROUP -> generalGetString(R.string.you_will_be_connected_when_group_host_device_is_online)
|
||||
}
|
||||
)
|
||||
}
|
||||
return r
|
||||
@@ -91,6 +135,7 @@ fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(ratio = 1F)
|
||||
.padding(bottom = 12.dp)
|
||||
) { qrCodeScanner() }
|
||||
Text(
|
||||
annotatedStringResource(R.string.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link),
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionCustomFooter
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.TheaterComedy
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun AcceptRequestsView(m: ChatModel, contactLink: UserContactLinkRec) {
|
||||
var contactLink by remember { mutableStateOf(contactLink) }
|
||||
AcceptRequestsLayout(
|
||||
contactLink,
|
||||
saveState = { new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState> ->
|
||||
withApi {
|
||||
val link = m.controller.userAddressAutoAccept(new.value.autoAccept)
|
||||
if (link != null) {
|
||||
contactLink = link
|
||||
m.userAddress.value = link
|
||||
old.value = new.value
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AcceptRequestsLayout(
|
||||
contactLink: UserContactLinkRec,
|
||||
saveState: (new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState>) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.contact_requests))
|
||||
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(contactLink)) }
|
||||
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
|
||||
SectionView(stringResource(R.string.accept_requests).uppercase()) {
|
||||
SectionItemView {
|
||||
PreferenceToggleWithIcon(stringResource(R.string.accept_automatically), Icons.Outlined.Check, checked = autoAcceptState.value.enable) {
|
||||
autoAcceptState.value = if (!it)
|
||||
AutoAcceptState()
|
||||
else
|
||||
AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText)
|
||||
}
|
||||
}
|
||||
if (autoAcceptState.value.enable) {
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
PreferenceToggleWithIcon(
|
||||
stringResource(R.string.incognito),
|
||||
if (autoAcceptState.value.incognito) Icons.Filled.TheaterComedy else Icons.Outlined.TheaterComedy,
|
||||
if (autoAcceptState.value.incognito) Indigo else HighOrLowlight,
|
||||
autoAcceptState.value.incognito,
|
||||
) {
|
||||
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val welcomeText = remember { mutableStateOf(autoAcceptState.value.welcomeText) }
|
||||
SectionCustomFooter(PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
ButtonsFooter(
|
||||
cancel = {
|
||||
autoAcceptState.value = autoAcceptStateSaved.value
|
||||
welcomeText.value = autoAcceptStateSaved.value.welcomeText
|
||||
},
|
||||
save = { saveState(autoAcceptState, autoAcceptStateSaved) },
|
||||
disabled = autoAcceptState.value == autoAcceptStateSaved.value
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
if (autoAcceptState.value.enable) {
|
||||
Text(
|
||||
stringResource(R.string.section_title_welcome_message), color = HighOrLowlight, style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
|
||||
)
|
||||
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
|
||||
LaunchedEffect(welcomeText.value) {
|
||||
if (welcomeText.value != autoAcceptState.value.welcomeText) {
|
||||
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ButtonsFooter(cancel: () -> Unit, save: () -> Unit, disabled: Boolean) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FooterButton(Icons.Outlined.Replay, stringResource(R.string.cancel_verb), cancel, disabled)
|
||||
FooterButton(Icons.Outlined.Check, stringResource(R.string.save_verb), save, disabled)
|
||||
}
|
||||
}
|
||||
|
||||
private class AutoAcceptState {
|
||||
var enable: Boolean = false
|
||||
private set
|
||||
var incognito: Boolean = false
|
||||
private set
|
||||
var welcomeText: String = ""
|
||||
private set
|
||||
|
||||
constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") {
|
||||
this.enable = enable
|
||||
this.incognito = incognito
|
||||
this.welcomeText = welcomeText
|
||||
}
|
||||
|
||||
constructor(contactLink: UserContactLinkRec) {
|
||||
contactLink.autoAccept?.let { aa ->
|
||||
enable = true
|
||||
incognito = aa.acceptIncognito
|
||||
aa.autoReply?.let { msg ->
|
||||
welcomeText = msg.text
|
||||
} ?: run {
|
||||
welcomeText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val autoAccept: AutoAccept?
|
||||
get() {
|
||||
if (enable) {
|
||||
var autoReply: MsgContent? = null
|
||||
val s = welcomeText.trim()
|
||||
if (s != "") {
|
||||
autoReply = MsgContent.MCText(s)
|
||||
}
|
||||
return AutoAccept(incognito, autoReply)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is AutoAcceptState) return false
|
||||
return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = enable.hashCode()
|
||||
result = 31 * result + incognito.hashCode()
|
||||
result = 31 * result + welcomeText.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package chat.simplex.app.views.usersettings
|
||||
import android.annotation.SuppressLint
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
@@ -10,11 +13,24 @@ import javax.crypto.spec.GCMParameterSpec
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
internal class Cryptor {
|
||||
private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
private var warningShown = false
|
||||
|
||||
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String {
|
||||
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? {
|
||||
val secretKey = getSecretKey(alias)
|
||||
if (secretKey == null) {
|
||||
if (!warningShown) {
|
||||
// Repeated calls will not show the alert again
|
||||
warningShown = true
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.wrong_passphrase),
|
||||
text = generalGetString(R.string.restore_passphrase_not_found_desc)
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
val spec = GCMParameterSpec(128, iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(alias), spec)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
|
||||
return String(cipher.doFinal(data))
|
||||
}
|
||||
|
||||
@@ -29,7 +45,7 @@ internal class Cryptor {
|
||||
keyStore.deleteEntry(alias)
|
||||
}
|
||||
|
||||
private fun createSecretKey(alias: String): SecretKey {
|
||||
private fun createSecretKey(alias: String): SecretKey? {
|
||||
if (keyStore.containsAlias(alias)) return getSecretKey(alias)
|
||||
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, "AndroidKeyStore")
|
||||
keyGenerator.init(
|
||||
@@ -41,8 +57,8 @@ internal class Cryptor {
|
||||
return keyGenerator.generateKey()
|
||||
}
|
||||
|
||||
private fun getSecretKey(alias: String): SecretKey {
|
||||
return (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
|
||||
private fun getSecretKey(alias: String): SecretKey? {
|
||||
return (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)?.secretKey
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+5
-5
@@ -4,19 +4,15 @@ import SectionDivider
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
import androidx.compose.material.icons.outlined.TravelExplore
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
@@ -33,6 +29,10 @@ fun PrivacySettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
SectionView(stringResource(R.string.settings_section_title_chats)) {
|
||||
SettingsPreferenceItem(Icons.Outlined.Image, stringResource(R.string.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||
SectionDivider()
|
||||
if (chatModel.controller.appPrefs.developerTools.get()) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,13 +146,25 @@ fun SettingsLayout(
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_support)) {
|
||||
ContributeItem(uriHandler)
|
||||
SectionDivider()
|
||||
RateAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
StarOnGithubItem(uriHandler)
|
||||
}
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_develop)) {
|
||||
ChatConsoleItem(showTerminal)
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools)
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
val devTools = remember { mutableStateOf(developerTools.get()) }
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
|
||||
SectionDivider()
|
||||
if (devTools.value) {
|
||||
ChatConsoleItem(showTerminal)
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
}
|
||||
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) })
|
||||
// SectionDivider()
|
||||
AppVersionItem()
|
||||
@@ -252,6 +264,46 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun ContributeItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat#contribute") }) {
|
||||
Icon(
|
||||
Icons.Outlined.Keyboard,
|
||||
contentDescription = "GitHub",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(generalGetString(R.string.contribute), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun RateAppItem(uriHandler: UriHandler) {
|
||||
SectionItemView({
|
||||
runCatching { uriHandler.openUri("market://details?id=chat.simplex.app") }
|
||||
.onFailure { uriHandler.openUri("https://play.google.com/store/apps/details?id=chat.simplex.app") }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.StarOutline,
|
||||
contentDescription = "Google Play",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(generalGetString(R.string.rate_the_app), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun StarOnGithubItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_github),
|
||||
contentDescription = "GitHub",
|
||||
tint = HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(generalGetString(R.string.star_on_github), color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) {
|
||||
SectionItemView(showTerminal) {
|
||||
Icon(
|
||||
@@ -342,6 +394,36 @@ fun SettingsPreferenceItemWithInfo(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceToggleWithIcon(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
iconColor: Color = HighOrLowlight,
|
||||
checked: Boolean,
|
||||
onChange: (Boolean) -> Unit = {},
|
||||
) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
icon,
|
||||
null,
|
||||
tint = iconColor
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(text)
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = {
|
||||
onChange(it)
|
||||
},
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
|
||||
+35
-25
@@ -2,10 +2,13 @@ package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -16,8 +19,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.model.UserContactLinkRec
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.QRCode
|
||||
|
||||
@@ -25,13 +28,21 @@ import chat.simplex.app.views.newchat.QRCode
|
||||
fun UserAddressView(chatModel: ChatModel) {
|
||||
val cxt = LocalContext.current
|
||||
UserAddressLayout(
|
||||
userAddress = chatModel.userAddress.value,
|
||||
userAddress = remember { chatModel.userAddress }.value,
|
||||
createAddress = {
|
||||
withApi {
|
||||
chatModel.userAddress.value = chatModel.controller.apiCreateUserAddress()
|
||||
val connReqContact = chatModel.controller.apiCreateUserAddress()
|
||||
if (connReqContact != null) {
|
||||
chatModel.userAddress.value = UserContactLinkRec(connReqContact)
|
||||
}
|
||||
}
|
||||
},
|
||||
share = { userAddress: String -> shareText(cxt, userAddress) },
|
||||
acceptRequests = {
|
||||
chatModel.userAddress.value?.let { address ->
|
||||
ModalManager.shared.showModal(settings = true) { AcceptRequestsView(chatModel, address) }
|
||||
}
|
||||
},
|
||||
deleteAddress = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.delete_address__question),
|
||||
@@ -50,12 +61,14 @@ fun UserAddressView(chatModel: ChatModel) {
|
||||
|
||||
@Composable
|
||||
fun UserAddressLayout(
|
||||
userAddress: String?,
|
||||
userAddress: UserContactLinkRec?,
|
||||
createAddress: () -> Unit,
|
||||
share: (String) -> Unit,
|
||||
acceptRequests: () -> Unit,
|
||||
deleteAddress: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Top
|
||||
) {
|
||||
@@ -66,40 +79,35 @@ fun UserAddressLayout(
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
if (userAddress == null) {
|
||||
Text(
|
||||
stringResource(R.string.if_you_later_delete_address_you_wont_lose_contacts),
|
||||
Modifier.padding(bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
SimpleButton(stringResource(R.string.create_address), icon = Icons.Outlined.QrCode, click = createAddress)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.if_you_delete_address_you_wont_lose_contacts),
|
||||
Modifier.padding(bottom = 12.dp),
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
QRCode(userAddress, Modifier.weight(1f, fill = false).aspectRatio(1f))
|
||||
QRCode(userAddress.connReqContact, Modifier.aspectRatio(1f))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 10.dp)
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
) {
|
||||
SimpleButton(
|
||||
stringResource(R.string.share_link),
|
||||
icon = Icons.Outlined.Share,
|
||||
click = { share(userAddress) })
|
||||
SimpleButton(
|
||||
stringResource(R.string.delete_address),
|
||||
icon = Icons.Outlined.Delete,
|
||||
color = Color.Red,
|
||||
click = deleteAddress
|
||||
click = { share(userAddress.connReqContact) })
|
||||
SimpleButtonIconEnded(
|
||||
stringResource(R.string.contact_requests),
|
||||
icon = Icons.Outlined.ChevronRight,
|
||||
click = acceptRequests
|
||||
)
|
||||
}
|
||||
SimpleButton(
|
||||
stringResource(R.string.delete_address),
|
||||
icon = Icons.Outlined.Delete,
|
||||
color = Color.Red,
|
||||
click = deleteAddress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,6 +126,7 @@ fun PreviewUserAddressLayoutNoAddress() {
|
||||
userAddress = null,
|
||||
createAddress = {},
|
||||
share = { _ -> },
|
||||
acceptRequests = {},
|
||||
deleteAddress = {},
|
||||
)
|
||||
}
|
||||
@@ -133,9 +142,10 @@ fun PreviewUserAddressLayoutNoAddress() {
|
||||
fun PreviewUserAddressLayoutAddressCreated() {
|
||||
SimpleXTheme {
|
||||
UserAddressLayout(
|
||||
userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
|
||||
userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"),
|
||||
createAddress = {},
|
||||
share = { _ -> },
|
||||
acceptRequests = {},
|
||||
deleteAddress = {},
|
||||
)
|
||||
}
|
||||
|
||||
+5
-3
@@ -2,6 +2,7 @@ package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -12,6 +13,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -34,7 +36,7 @@ import kotlinx.coroutines.launch
|
||||
fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
val editProfile = remember { mutableStateOf(false) }
|
||||
val editProfile = rememberSaveable { mutableStateOf(false) }
|
||||
var profile by remember { mutableStateOf(user.profile.toProfile()) }
|
||||
UserProfileLayout(
|
||||
editProfile = editProfile,
|
||||
@@ -67,8 +69,8 @@ fun UserProfileLayout(
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val displayName = remember { mutableStateOf(profile.displayName) }
|
||||
val fullName = remember { mutableStateOf(profile.fullName) }
|
||||
val chosenImage = remember { mutableStateOf<Bitmap?>(null) }
|
||||
val profileImage = remember { mutableStateOf(profile.image) }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf(profile.image) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape>
|
||||
<corners android:radius="18dp"/>
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#8b8786"/>
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -6,13 +6,15 @@
|
||||
<!-- Connect via Link - MainActivity.kt -->
|
||||
<string name="connect_via_contact_link">Über den Kontakt-Link verbinden?</string>
|
||||
<string name="connect_via_invitation_link">Über den Einladungs-Link verbinden?</string>
|
||||
<string name="connect_via_group_link">Über den Gruppen-Link verbinden?</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet von dem Sie diesen Link erhalten haben.</string>
|
||||
<string name="you_will_join_group">Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht und sich mit deren Gruppenmitgliedern verbinden.</string>
|
||||
<string name="connect_via_link_verb">Verbinden</string>
|
||||
|
||||
<!-- Server info - ChatModel.kt -->
|
||||
<string name="server_connected">Verbunden</string>
|
||||
<string name="server_error">Fehler</string>
|
||||
<string name="server_connecting">verbinde</string>
|
||||
<string name="server_connecting">Verbinde</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages_with_error">Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: <xliff:g id="errorMsg">%1$s</xliff:g>).</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string>
|
||||
@@ -26,14 +28,16 @@
|
||||
<string name="invalid_message_format">Unzulässiges Nachrichtenformat</string>
|
||||
|
||||
<!-- PendingContactConnection - ChatModel.kt -->
|
||||
<string name="connection_local_display_name">Verbindung <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">Verbindung hergestellt</string>
|
||||
<string name="display_name_invited_to_connect">Für eine Verbindung eingeladen</string>
|
||||
<string name="display_name_connecting">verbinde …</string>
|
||||
<string name="description_you_shared_one_time_link">Sie haben einen Einmal-Link geteilt</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">Sie haben Inkognito einen Einmal-Link geteilt</string>
|
||||
<string name="description_via_contact_address_link">über einen Kontaktadressen Link</string>
|
||||
<string name="description_via_contact_address_link_incognito">Inkognito über einen Kontaktadressen Link</string>
|
||||
<string name="connection_local_display_name">verbindung <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">verbindung hergestellt</string>
|
||||
<string name="display_name_invited_to_connect">für eine Verbindung eingeladen</string>
|
||||
<string name="display_name_connecting">verbinde…</string>
|
||||
<string name="description_you_shared_one_time_link">sie haben einen Einmal-Link geteilt</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">sie haben Inkognito einen Einmal-Link geteilt</string>
|
||||
<string name="description_via_group_link">über einen Gruppen-Link</string>
|
||||
<string name="description_via_group_link_incognito">Inkognito über einen Gruppen-Link</string>
|
||||
<string name="description_via_contact_address_link">über einen Kontaktadressen-Link</string>
|
||||
<string name="description_via_contact_address_link_incognito">Inkognito über einen Kontaktadressen-Link</string>
|
||||
<string name="description_via_one_time_link">über einen Einmal-Link</string>
|
||||
<string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string>
|
||||
|
||||
@@ -54,19 +58,18 @@
|
||||
<string name="error_receiving_file">Fehler beim Empfangen der Datei</string>
|
||||
<string name="error_creating_address">Fehler beim Erstellen der Adresse</string>
|
||||
<string name="contact_already_exists">Kontakt ist bereits vorhanden</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits über diesen Link mit <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> verbunden.</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> verbunden.</string>
|
||||
<string name="invalid_connection_link">Ungültiger Verbindungslink</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string>
|
||||
<string name="connection_error_auth">Verbindungsfehler (AUTH)</string>
|
||||
<string name="connection_error_auth_desc">Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns.\nBitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben.</string>
|
||||
<string name="error_accepting_contact_request">Fehler beim Akzeptieren der Kontaktanfrage</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">Der Absender hat möglicherweise die Verbindungsanfrage gelöscht.</string>
|
||||
<string name="cannot_delete_contact">Der Kontakt kann nicht gelöscht werden!</string>
|
||||
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Der Kontakt mit <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> kann nicht gelöscht werden, da er Mitglied einer oder mehrerer dieser Gruppen ist <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
|
||||
<string name="error_deleting_contact">Fehler beim Löschen des Kontakts</string>
|
||||
<string name="error_deleting_group">Fehler beim Löschen der Gruppe</string>
|
||||
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontakt-Anfrage</string>
|
||||
<string name="error_deleting_contact_request">Fehler beim Löschen der Kontaktanfrage</string>
|
||||
<string name="error_deleting_pending_contact_connection">Fehler beim Löschen der anstehenden Kontaktaufnahme</string>
|
||||
<string name="error_changing_address">Fehler beim Wechseln der Adresse</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string>
|
||||
@@ -111,9 +114,9 @@
|
||||
<string name="notification_preview_mode_contact_desc">Nur Kontakt anzeigen</string>
|
||||
<string name="notification_display_mode_hidden_desc">Kontakt und Nachricht verbergen</string>
|
||||
<string name="notification_preview_somebody">Kontakt verbergen:</string>
|
||||
<string name="notification_preview_new_message">neue Nachricht</string>
|
||||
<string name="notification_new_contact_request">New contact request</string>
|
||||
<string name="notification_contact_connected">Сonnected</string>
|
||||
<string name="notification_preview_new_message">Neue Nachricht</string>
|
||||
<string name="notification_new_contact_request">Neue Kontaktanfrage</string>
|
||||
<string name="notification_contact_connected">Verbunden</string>
|
||||
|
||||
<!-- local authentication notice - SimpleXAPI.kt -->
|
||||
<string name="la_notice_title_simplex_lock">SimpleX Sperre</string>
|
||||
@@ -128,16 +131,16 @@
|
||||
<string name="auth_enable_simplex_lock">SimpleX Sperre aktivieren</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX Sperre deaktivieren</string>
|
||||
<string name="auth_confirm_credential">Bestätigen Sie Ihre Zugangsdaten</string>
|
||||
<string name="auth_error">Authentifizierungsfehler</string>
|
||||
<string name="auth_error_w_desc">Authentifizierungsfehler: <xliff:g id="desc">%1$s</xliff:g></string>
|
||||
<string name="auth_failed">Authentifizierung fehlgeschlagen</string>
|
||||
<string name="auth_unavailable">Authentifizierung nicht verfügbar</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Geräteauthentifizierung ist nicht aktiviert. Sie können die SimpleX Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Geräteauthentifizierung ist deaktiviert. SimpleX Sperre ist abgeschaltet.</string>
|
||||
<string name="auth_retry">Wiederholen</string>
|
||||
<string name="auth_stop_chat">Chat beenden</string>
|
||||
<string name="auth_open_chat_console">Chat-Konsole öffnen</string>
|
||||
|
||||
<!-- Chat Alerts - ChatItemView.kt -->
|
||||
<string name="message_delivery_error_title">Fehler bei der Nachrichtenzustellung</string>
|
||||
<string name="message_delivery_error_desc">Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht.</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Antwort</string>
|
||||
<string name="share_verb">Teilen</string>
|
||||
@@ -151,11 +154,11 @@
|
||||
<string name="for_everybody">Für alle</string>
|
||||
|
||||
<!-- CIMetaView.kt -->
|
||||
<string name="icon_descr_edited">bearbeitet</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">gesendet</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">nicht autorisiertes Senden</string>
|
||||
<string name="icon_descr_edited">Bearbeitet</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">Gesendet</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">Nicht autorisiertes Senden</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">Senden fehlgeschlagen</string>
|
||||
<string name="icon_descr_received_msg_status_unread">ungelesen</string>
|
||||
<string name="icon_descr_received_msg_status_unread">Ungelesen</string>
|
||||
|
||||
<!-- ChatListView.kt -->
|
||||
<string name="personal_welcome">Willkommen <xliff:g>%1$s</xliff:g>!</string>
|
||||
@@ -164,9 +167,9 @@
|
||||
<string name="your_chats">Meine Chats</string>
|
||||
<string name="contact_connection_pending">verbinde …</string>
|
||||
<string name="group_preview_you_are_invited">Sie sind zu der Gruppe eingeladen</string>
|
||||
<string name="group_preview_join_as">beitreten als %s</string>
|
||||
<string name="group_preview_join_as">Beitreten als %s</string>
|
||||
<string name="group_connection_pending">verbinde …</string>
|
||||
<string name="tap_to_start_new_chat">Tippen Sie um einen neuen Chat zu starten</string>
|
||||
<string name="tap_to_start_new_chat">Tippen Sie, um einen neuen Chat zu starten</string>
|
||||
<string name="chat_with_developers">Chatten Sie mit den Entwicklern</string>
|
||||
<string name="you_have_no_chats">Sie haben keine Chats</string>
|
||||
|
||||
@@ -182,14 +185,16 @@
|
||||
<string name="icon_descr_cancel_file_preview">Dateivorschau abbrechen</string>
|
||||
<string name="images_limit_title">Zu viele Bilder!</string>
|
||||
<string name="images_limit_desc">Es können nur 10 Bilder auf einmal gesendet werden</string>
|
||||
<string name="image_decoding_exception_title">Dekodierungsfehler</string>
|
||||
<string name="image_decoding_exception_desc">Das Bild kann nicht dekodiert werden. Bitte versuchen Sie es mit einem anderen Bild oder wenden Sie sich an die Entwickler.</string>
|
||||
|
||||
<!-- Images - CIImageView.kt -->
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Bild</string>
|
||||
<string name="icon_descr_waiting_for_image">Warten auf ein Bild</string>
|
||||
<string name="icon_descr_asked_to_receive">Um Empfang des Bildes gebeten</string>
|
||||
<string name="icon_descr_asked_to_receive">Es wird um den Empfang eines Bildes gebeten</string>
|
||||
<string name="icon_descr_image_snd_complete">Bild gesendet</string>
|
||||
<string name="waiting_for_image">Warten auf ein Bild</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">Das Bild wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="image_saved">Bild wurde im Fotoalbum gespeichert</string>
|
||||
|
||||
<!-- Files - CIFileView.kt -->
|
||||
@@ -198,7 +203,7 @@
|
||||
<string name="contact_sent_large_file">Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (<xliff:g id="maxFileSize">%1$s</xliff:g>).</string>
|
||||
<string name="maximum_supported_file_size">Die derzeit maximal unterstützte Dateigröße beträgt <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="waiting_for_file">Warte auf Datei</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="file_saved">Datei gespeichert</string>
|
||||
<string name="file_not_found">Datei nicht gefunden</string>
|
||||
<string name="error_saving_file">Fehler beim Speichern der Datei</string>
|
||||
@@ -215,6 +220,8 @@
|
||||
<string name="icon_descr_server_status_disconnected">Getrennt</string>
|
||||
<string name="icon_descr_server_status_error">Fehler</string>
|
||||
<string name="icon_descr_server_status_pending">Ausstehend</string>
|
||||
<string name="switch_receiving_address_question">Empfängeradresse wechseln?</string>
|
||||
<string name="switch_receiving_address_desc">Diese Funktion ist experimentell! Sie wird nur funktionieren, wenn der Kontakt ebenfalls die SimpleX-Version 4.2 installiert hat. Sobald der Adress-Wechsel abgeschlossen ist, sollten Sie die Nachricht im Chat-Verlauf sehen. Bitte prüfen Sie, ob Sie weiterhin Nachrichten von diesem Kontakt (oder Gruppenmitglied) empfangen können.</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Nachricht senden</string>
|
||||
@@ -224,7 +231,7 @@
|
||||
<string name="cancel_verb">Abbrechen</string>
|
||||
<string name="confirm_verb">Bestätigen</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="no_details">keine Details</string>
|
||||
<string name="no_details">Keine Details</string>
|
||||
<string name="add_contact">Kontakt hinzufügen</string>
|
||||
<string name="copied">In die Zwischenablage kopiert</string>
|
||||
|
||||
@@ -236,7 +243,7 @@
|
||||
<string name="create_group">Geheime Gruppe erstellen</string>
|
||||
<string name="to_share_with_your_contact">(zum Teilen mit Ihrem Kontakt)</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(Scannen oder Einfügen aus der Zwischenablage)</string>
|
||||
<string name="only_stored_on_members_devices">(wird nur von Gruppenmitgliedern gespeichert)</string>
|
||||
<string name="only_stored_on_members_devices">(Wird nur von Gruppenmitgliedern gespeichert)</string>
|
||||
|
||||
<!-- GetImageView -->
|
||||
<string name="toast_permission_denied">Berechtigung verweigert!</string>
|
||||
@@ -249,7 +256,7 @@
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Sie können sich <font color="#0088ff">mit <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.</string>
|
||||
<string name="to_start_a_new_chat_help_header">Um einen neuen Chat zu starten</string>
|
||||
<string name="chat_help_tap_button">Schaltfläche antippen</string>
|
||||
<string name="above_then_preposition_continuation">über, dann:</string>
|
||||
<string name="above_then_preposition_continuation">Danach die gewünschte Aktion auswählen:</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Neuen Kontakt hinzufügen</b>: Um Ihren Einmal-QR-Code für Ihren Kontakt zu erstellen.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR-Code scannen</b>: Um sich mit Ihrem Kontakt zu verbinden, der Ihnen seinen QR-Code zeigt.</string>
|
||||
<string name="to_connect_via_link_title">Über Link verbinden</string>
|
||||
@@ -273,6 +280,7 @@
|
||||
<string name="delete_contact_menu_action">Kontakt löschen</string>
|
||||
<string name="delete_group_menu_action">Gruppe löschen</string>
|
||||
<string name="mark_read">Als gelesen markieren</string>
|
||||
<string name="mark_unread">Als ungelesen markieren</string>
|
||||
<string name="set_contact_name">Kontaktname festlegen</string>
|
||||
|
||||
<!-- Actions - ChatListNavLinkView.kt -->
|
||||
@@ -298,7 +306,7 @@
|
||||
<string name="image_descr_profile_image">Profilbild</string>
|
||||
|
||||
<!-- Content Descriptions -->
|
||||
<string name="icon_descr_close_button">Schließen Schaltfläche</string>
|
||||
<string name="icon_descr_close_button">Schließen der Schaltfläche</string>
|
||||
<string name="image_descr_link_preview">Vorschaubild verlinken</string>
|
||||
<string name="icon_descr_cancel_link_preview">Link Vorschau abbrechen</string>
|
||||
<string name="icon_descr_settings">Einstellungen</string>
|
||||
@@ -319,8 +327,9 @@
|
||||
<string name="invalid_contact_link">Ungültiger Link!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Dieser Link ist kein gültiger Verbindungslink!</string>
|
||||
<string name="connection_request_sent">Verbindungsanfrage gesendet!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!</string>
|
||||
<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>
|
||||
@@ -349,12 +358,15 @@
|
||||
<string name="how_to_use_simplex_chat">Wie man SimpleX nutzt</string>
|
||||
<string name="markdown_help">Markdown Hilfe</string>
|
||||
<string name="markdown_in_messages">Markdown in Nachrichten</string>
|
||||
<string name="chat_with_the_founder">Verbinden Sie sich mit den Entwicklern</string>
|
||||
<string name="chat_with_the_founder">Senden Sie Fragen und Ideen</string>
|
||||
<string name="send_us_an_email">Senden Sie uns eine E-Mail</string>
|
||||
<string name="chat_lock">SimpleX Sperre</string>
|
||||
<string name="chat_console">Chat Konsole</string>
|
||||
<string name="smp_servers">SMP-Server</string>
|
||||
<string name="install_simplex_chat_for_terminal">Installieren Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> als Terminalanwendung</string>
|
||||
<string name="star_on_github">Stern auf GitHub</string>
|
||||
<string name="contribute">Beitragen</string>
|
||||
<string name="rate_the_app">Bewerte die App</string>
|
||||
<string name="use_simplex_chat_servers__question">Verwenden Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Server?</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Gespeicherte SMP-Server werden entfernt.</string>
|
||||
<string name="your_SMP_servers">Ihre SMP-Server</string>
|
||||
@@ -394,12 +406,16 @@
|
||||
<string name="create_address">Adresse erstellen</string>
|
||||
<string name="delete_address__question">Adresse löschen?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Alle Ihre Kontakte bleiben verbunden.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen – Jeder kann sich mit Ihnen verbinden.</string>
|
||||
<string name="if_you_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse löschen, werden Sie Ihre Kontakte nicht verlieren.</string>
|
||||
<string name="if_you_later_delete_address_you_wont_lose_contacts">Wenn Sie diese Adresse später löschen, werden Sie Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string>
|
||||
<string name="share_link">Link teilen</string>
|
||||
<string name="delete_address">Adresse löschen</string>
|
||||
|
||||
<!-- AcceptRequestsView.kt -->
|
||||
<string name="contact_requests">Kontaktanfragen</string>
|
||||
<string name="accept_requests">Anfragen annehmen</string>
|
||||
<string name="accept_automatically">Automatisch</string>
|
||||
<string name="section_title_welcome_message">Begrüßungsmeldung</string>
|
||||
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Angezeigter Name:</string>
|
||||
<string name="full_name__field">"Vollständiger Name:</string>
|
||||
@@ -449,8 +465,8 @@
|
||||
<string name="callstate_received_answer">Antwort erhalten…</string>
|
||||
<string name="callstate_received_confirmation">Bestätigung erhalten…</string>
|
||||
<string name="callstate_connecting">Verbindung wird hergestellt…</string>
|
||||
<string name="callstate_connected">verbunden</string>
|
||||
<string name="callstate_ended">beendet</string>
|
||||
<string name="callstate_connected">Verbunden</string>
|
||||
<string name="callstate_ended">Beendet</string>
|
||||
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">Die nächste Generation von privatem Messaging</string>
|
||||
@@ -459,7 +475,7 @@
|
||||
<string name="immune_to_spam_and_abuse">Immun gegen Spam und Missbrauch</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen.</string>
|
||||
<string name="decentralized">Dezentral</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-Source-Protokoll und -Code – Jeder kann seine eigenen Server nutzen.</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen.</string>
|
||||
<string name="create_your_profile">Erstellen Sie Ihr Profil</string>
|
||||
<string name="make_private_connection">Stellen Sie eine private Verbindung her</string>
|
||||
<string name="how_it_works">Wie es funktioniert</string>
|
||||
@@ -544,12 +560,14 @@
|
||||
<string name="privacy_and_security">Datenschutz & Sicherheit</string>
|
||||
<string name="your_privacy">Meine Privatsphäre</string>
|
||||
<string name="auto_accept_images">Bilder automatisch akzeptieren</string>
|
||||
<string name="transfer_images_faster">Bilder schneller übertragen (BETA)</string>
|
||||
<string name="send_link_previews">Link-Vorschau senden</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">DU</string>
|
||||
<string name="settings_section_title_you">MEINE DATEN</string>
|
||||
<string name="settings_section_title_settings">EINSTELLUNGEN</string>
|
||||
<string name="settings_section_title_help">HILFE</string>
|
||||
<string name="settings_section_title_support">UNTERSTÜTZEN SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_develop">ENTWICKLUNG</string>
|
||||
<string name="settings_section_title_device">GERÄT</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
@@ -565,8 +583,8 @@
|
||||
<!-- DatabaseView.kt -->
|
||||
<string name="your_chat_database">Meine Chat-Datenbank</string>
|
||||
<string name="run_chat_section">CHAT STARTEN</string>
|
||||
<string name="chat_is_running">Chat läuft</string>
|
||||
<string name="chat_is_stopped">Chat ist beendet</string>
|
||||
<string name="chat_is_running">Der Chat läuft</string>
|
||||
<string name="chat_is_stopped">Der Chat ist beendet</string>
|
||||
<string name="chat_database_section">CHAT-DATENBANK</string>
|
||||
<string name="database_passphrase">Datenbank-Passwort</string>
|
||||
<string name="export_database">Datenbank exportieren</string>
|
||||
@@ -576,7 +594,7 @@
|
||||
<string name="delete_database">Datenbank löschen</string>
|
||||
<string name="error_starting_chat">Fehler beim Starten des Chats</string>
|
||||
<string name="stop_chat_question">Chat beenden?</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Sie können keine Nachrichten empfangen oder senden, solange der Chat angehalten ist.</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Beenden Sie den Chat, um die Chat-Datenbank zu exportieren, zu importieren oder zu löschen. Solange der Chat angehalten ist, können Sie keine Nachrichten empfangen oder senden.</string>
|
||||
<string name="stop_chat_confirmation">Beenden</string>
|
||||
<string name="set_password_to_export">Passwort zum Exportieren festlegen</string>
|
||||
<string name="set_password_to_export_desc">Die Datenbank ist mit einem zufälligen Passwort verschlüsselt. Bitte ändern Sie es vor dem Exportieren.</string>
|
||||
@@ -598,17 +616,17 @@
|
||||
<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="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>
|
||||
<string name="total_files_count_and_size">%d Datei(en) mit einem Gesamtspeicherverbrauch von %s</string>
|
||||
<string name="chat_item_ttl_none">nie</string>
|
||||
<string name="chat_item_ttl_day">1 Tag</string>
|
||||
<string name="chat_item_ttl_week">1 Woche</string>
|
||||
<string name="chat_item_ttl_month">1 Monat</string>
|
||||
<string name="chat_item_ttl_day">täglich</string>
|
||||
<string name="chat_item_ttl_week">wöchentlich</string>
|
||||
<string name="chat_item_ttl_month">monatlich</string>
|
||||
<string name="chat_item_ttl_seconds">%s Sekunde(n)</string>
|
||||
<string name="delete_messages_after">Löschen der Nachrichten nach</string>
|
||||
<string name="delete_messages_after">Löschen der Nachrichten</string>
|
||||
<string name="enable_automatic_deletion_question">Automatisches Löschen von Nachrichten aktivieren?</string>
|
||||
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, werden gelöscht. Dies kann mehrere Minuten dauern.</string>
|
||||
<string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</string>
|
||||
<string name="delete_messages">Nachrichten löschen</string>
|
||||
<string name="error_changing_message_deletion">Fehler beim Ändern der Einstellung</string>
|
||||
|
||||
@@ -664,6 +682,7 @@
|
||||
<string name="restore_database_alert_desc">Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.</string>
|
||||
<string name="restore_database_alert_confirm">Wiederherstellen</string>
|
||||
<string name="database_restore_error">Fehler bei der Wiederherstellung der Datenbank</string>
|
||||
<string name="restore_passphrase_not_found_desc">***Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers.</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Chat wurde beendet</string>
|
||||
@@ -683,10 +702,10 @@
|
||||
<string name="you_are_invited_to_group_join_to_connect_with_group_members">Sie sind zu einer Gruppe eingeladen worden. Treten Sie bei, um sich mit Gruppenmitgliedern zu verbinden.</string>
|
||||
<string name="join_group_button">Beitreten</string>
|
||||
<string name="join_group_incognito_button">Inkognito beitreten</string>
|
||||
<string name="joining_group">Gruppe beitreten</string>
|
||||
<string name="joining_group">Der Gruppe beitreten</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden.</string>
|
||||
<string name="leave_group_button">Verlassen</string>
|
||||
<string name="leave_group_question">Gruppe verlassen?</string>
|
||||
<string name="leave_group_question">Die Gruppe verlassen?</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</string>
|
||||
<string name="icon_descr_add_members">Mitglieder einladen</string>
|
||||
<string name="icon_descr_group_inactive">Gruppe inaktiv</string>
|
||||
@@ -707,33 +726,46 @@
|
||||
<string name="group_invitation_expired">Die Gruppeneinladung ist abgelaufen</string>
|
||||
|
||||
<!-- Group event chat items -->
|
||||
<string name="rcv_group_event_member_added">Sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> eingeladen.</string>
|
||||
<string name="rcv_group_event_member_connected">verbunden</string>
|
||||
<string name="rcv_group_event_member_left">verlassen</string>
|
||||
<string name="rcv_group_event_member_deleted">entfernte <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g>.</string>
|
||||
<string name="rcv_group_event_user_deleted">hat Sie entfernt</string>
|
||||
<string name="rcv_group_event_member_added">hat <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> eingeladen.</string>
|
||||
<string name="rcv_group_event_member_connected">beigetreten</string>
|
||||
<string name="rcv_group_event_member_left">hat die Gruppe verlassen</string>
|
||||
<string name="rcv_group_event_changed_member_role">änderte die Rolle von %s auf %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">änderte Ihre Rolle auf %s</string>
|
||||
<string name="rcv_group_event_member_deleted">entfernte <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> aus der Gruppe.</string>
|
||||
<string name="rcv_group_event_user_deleted">hat Sie aus der Gruppe entfernt</string>
|
||||
<string name="rcv_group_event_group_deleted">Gruppe gelöscht</string>
|
||||
<string name="rcv_group_event_updated_group_profile">aktualisiertes Gruppenprofil</string>
|
||||
<string name="snd_group_event_member_deleted">Sie haben <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> entfernt.</string>
|
||||
<string name="snd_group_event_user_left">Sie haben verlassen</string>
|
||||
<string name="rcv_group_event_updated_group_profile">Aktualisiertes Gruppenprofil</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">wurde über Ihren Gruppen-Link eingeladen</string>
|
||||
<string name="snd_group_event_changed_member_role">Sie haben die Rolle von %s auf %s geändert</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">Sie haben Ihre eigene Rolle auf %s geändert</string>
|
||||
<string name="snd_group_event_member_deleted">entfernt <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> aus der Gruppe.</string>
|
||||
<string name="snd_group_event_user_left">hat die Gruppe verlassen</string>
|
||||
<string name="snd_group_event_group_profile_updated">Gruppenprofil aktualisiert</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">wechselte die Adresse für Sie</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">Wechsel der Adresse ...</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">Sie haben die Adresse für %s gewechselt</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">Wechsel der Adresse für %s ...</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">Sie haben die Adresse gewechselt</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">Wechsel der Adresse ...</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_member">Mitglied</string>
|
||||
<string name="group_member_role_admin">Admin</string>
|
||||
<string name="group_member_role_owner">Eigentümer</string>
|
||||
|
||||
<!-- GroupMemberStatus -->
|
||||
<string name="group_member_status_removed">entfernt</string>
|
||||
<string name="group_member_status_left">verlassen</string>
|
||||
<string name="group_member_status_removed">Entfernt</string>
|
||||
<string name="group_member_status_left">Verlassen</string>
|
||||
<string name="group_member_status_group_deleted">Gruppe gelöscht</string>
|
||||
<string name="group_member_status_invited">eingeladen</string>
|
||||
<string name="group_member_status_invited">Eingeladen</string>
|
||||
<string name="group_member_status_introduced">Verbindung (erstellt)</string>
|
||||
<string name="group_member_status_intro_invitation">Verbindung (eingeladen)</string>
|
||||
<string name="group_member_status_accepted">Verbindung (akzeptiert)</string>
|
||||
<string name="group_member_status_announced">Verbindung (angekündigt)</string>
|
||||
<string name="group_member_status_connected">verbunden</string>
|
||||
<string name="group_member_status_complete">vollständig</string>
|
||||
<string name="group_member_status_connected">Verbunden</string>
|
||||
<string name="group_member_status_complete">Vollständig</string>
|
||||
<string name="group_member_status_creator">Ersteller</string>
|
||||
|
||||
<string name="group_member_status_connecting">verbinden</string>
|
||||
@@ -760,6 +792,14 @@
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="button_leave_group">Gruppe verlassen</string>
|
||||
<string name="button_edit_group_profile">Gruppenprofil bearbeiten</string>
|
||||
<string name="group_link">Gruppen-Link</string>
|
||||
<string name="button_create_group_link">Link erzeugen</string>
|
||||
<string name="delete_link_question">Link löschen?</string>
|
||||
<string name="delete_link">Link löschen</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.</string>
|
||||
<string name="all_group_members_will_remain_connected">Alle Gruppenmitglieder bleiben verbunden.</string>
|
||||
<string name="error_creating_link_for_group">Fehler beim Erzeugen des Gruppen-Links</string>
|
||||
<string name="error_deleting_link_for_group">Fehler beim Löschen des Gruppen-Links</string>
|
||||
|
||||
<!-- For Console chat info section -->
|
||||
<string name="section_title_for_console">FÜR KONSOLE</string>
|
||||
@@ -775,13 +815,12 @@
|
||||
<string name="role_in_group">Rolle</string>
|
||||
<string name="change_role">Rolle ändern</string>
|
||||
<string name="change_verb">Ändern</string>
|
||||
<string name="switch_verb">Wechseln</string>
|
||||
<string name="change_member_role_question">Die Mitgliederrolle ändern?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
|
||||
<string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string>
|
||||
<string name="error_changing_role">Fehler beim Ändern der Rolle</string>
|
||||
<string name="member_role">Mitglieder %s Rolle: %s</string>
|
||||
<string name="your_member_role">Meine Rolle: %s</string>
|
||||
<string name="info_row_group">Gruppe</string>
|
||||
<string name="info_row_connection">Verbindung</string>
|
||||
<string name="conn_level_desc_direct">direkt</string>
|
||||
@@ -792,6 +831,7 @@
|
||||
<string name="receiving_via">Empfangen über</string>
|
||||
<string name="sending_via">Senden über</string>
|
||||
<string name="network_status">Netzwerkstatus</string>
|
||||
<string name="switch_receiving_address">Wechseln der Empfängeradresse (BETA)</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Geheime Gruppe erstellen</string>
|
||||
@@ -799,7 +839,7 @@
|
||||
<string name="group_display_name_field">Anzeigename der Gruppe:</string>
|
||||
<string name="group_full_name_field">Vollständiger Gruppenname:</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">Der Inkognito-Modus wird hier nicht unterstützt - Ihr Hauptprofil wird an die Gruppenmitglieder gesendet</string>
|
||||
<string name="group_main_profile_sent">Ihr Chat-Profil wird an Gruppenmitglieder gesendet</string>
|
||||
<string name="group_main_profile_sent">Ihr Chat-Profil wird an die Gruppenmitglieder gesendet</string>
|
||||
|
||||
|
||||
<!-- GroupProfileView.kt -->
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<!-- Connect via Link - MainActivity.kt -->
|
||||
<string name="connect_via_contact_link">Соединиться через ссылку-контакт?</string>
|
||||
<string name="connect_via_invitation_link">Соединиться через ссылку-приглашение?</string>
|
||||
<string name="connect_via_group_link">Соединиться через ссылку группы?</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Ваш профиль будет отправлен контакту, от которого вы получили эту ссылку.</string>
|
||||
<string name="you_will_join_group">Вы вступите в группу, на которую ссылается эта ссылка.</string>
|
||||
<string name="connect_via_link_verb">Соединиться</string>
|
||||
|
||||
<!-- Server info - ChatModel.kt -->
|
||||
@@ -32,6 +34,8 @@
|
||||
<string name="display_name_connecting">соединяется…</string>
|
||||
<string name="description_you_shared_one_time_link">вы создали одноразовую ссылку</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">вы создали одноразовую ссылку инкогнито</string>
|
||||
<string name="description_via_group_link">через ссылку группы</string>
|
||||
<string name="description_via_group_link_incognito">инкогнито через ссылку группы</string>
|
||||
<string name="description_via_contact_address_link">через ссылку-контакт</string>
|
||||
<string name="description_via_contact_address_link_incognito">инкогнито через ссылку-контакт</string>
|
||||
<string name="description_via_one_time_link">через одноразовую ссылку</string>
|
||||
@@ -54,19 +58,18 @@
|
||||
<string name="error_receiving_file">Ошибка при получении файла</string>
|
||||
<string name="error_creating_address">Ошибка при создании адреса</string>
|
||||
<string name="contact_already_exists">Существующий контакт</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> через эту ссылку.</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">Вы уже соединены с контактом <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
|
||||
<string name="invalid_connection_link">Ошибка в ссылке контакта</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Пожалуйста, проверьте, что вы использовали правильную ссылку, или попросите ваш контакт отправить вам новую.</string>
|
||||
<string name="connection_error_auth">Ошибка соединения (AUTH)</string>
|
||||
<string name="connection_error_auth_desc">Возможно, ваш контакт удалил ссылку, или она уже была использована. Если это не так, то это может быть ошибкой - пожалуйста, сообщите нам об этом.\nЧтобы установить соединение, попросите ваш контакт создать еще одну ссылку и проверьте ваше соединение с сетью.</string>
|
||||
<string name="error_accepting_contact_request">Ошибка при принятии запроса на соединение</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">Отправитель мог удалить запрос на соединение.</string>
|
||||
<string name="cannot_delete_contact">Невозможно удалить контакт!</string>
|
||||
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Контакт <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> не может быть удален, так как является членом групп(ы) <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
|
||||
<string name="error_deleting_contact">Ошибка удаления контакта</string>
|
||||
<string name="error_deleting_contact">Ошибка при удалении контакта</string>
|
||||
<string name="error_deleting_group">Ошибка удаления группы</string>
|
||||
<string name="error_deleting_contact_request">Ошибка удаления запроса</string>
|
||||
<string name="error_deleting_pending_contact_connection">Ошибка удаления ожидаемого соединения</string>
|
||||
<string name="error_changing_address">Ошибка при изменении адреса</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Мгновенные уведомления</string>
|
||||
@@ -128,16 +131,16 @@
|
||||
<string name="auth_enable_simplex_lock">Включить блокировку SimpleX</string>
|
||||
<string name="auth_disable_simplex_lock">Отключить блокировку SimpleX</string>
|
||||
<string name="auth_confirm_credential">Пройдите аутентификацию</string>
|
||||
<string name="auth_error">Ошибка аутентификации</string>
|
||||
<string name="auth_error_w_desc">Ошибка аутентификации: <xliff:g id="desc">%1$s</xliff:g></string>
|
||||
<string name="auth_failed">Ошибка аутентификации</string>
|
||||
<string name="auth_unavailable">Аутентификация недоступна</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Аутентификация устройства не включена. Вы можете включить блокировку SimpleX в Настройках после включения аутентификации.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Аутентификация устройства выключена. Отключение блокировки SimpleX Chat.</string>
|
||||
<string name="auth_retry">Повторить</string>
|
||||
<string name="auth_stop_chat">Остановить чат</string>
|
||||
<string name="auth_open_chat_console">Открыть консоль</string>
|
||||
|
||||
<!-- Chat Alerts - ChatItemView.kt -->
|
||||
<string name="message_delivery_error_title">Ошибка доставки сообщения</string>
|
||||
<string name="message_delivery_error_desc">Скорее всего, этот контакт удалил соединение с вами.</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Ответить</string>
|
||||
<string name="share_verb">Поделиться</string>
|
||||
@@ -182,8 +185,10 @@
|
||||
<string name="icon_descr_cancel_file_preview">Удалить превью файла</string>
|
||||
<string name="images_limit_title">Слишком много изображений!</string>
|
||||
<string name="images_limit_desc">Только 10 изображений могут быть отправлены одномоментно</string>
|
||||
<string name="image_decoding_exception_title">Ошибка декодирования</string>
|
||||
<string name="image_decoding_exception_desc">Не получается декодировать изображение. Пожалуйста, попробуйте другое изображение или свяжитесь с разработчиками.</string>
|
||||
|
||||
<!-- Images - CIImageView.kt -->
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Изображение</string>
|
||||
<string name="icon_descr_waiting_for_image">Ожидается прием изображения</string>
|
||||
<string name="icon_descr_asked_to_receive">Предложено получить изображение</string>
|
||||
@@ -215,6 +220,8 @@
|
||||
<string name="icon_descr_server_status_disconnected">Соединение с сервером не установлено</string>
|
||||
<string name="icon_descr_server_status_error">Ошибка соединения с сервером</string>
|
||||
<string name="icon_descr_server_status_pending">Ожидается соединение с сервером</string>
|
||||
<string name="switch_receiving_address_question">Переключить адрес получения?</string>
|
||||
<string name="switch_receiving_address_desc">Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Отправить сообщение</string>
|
||||
@@ -273,6 +280,7 @@
|
||||
<string name="delete_contact_menu_action">Удалить</string>
|
||||
<string name="delete_group_menu_action">Удалить</string>
|
||||
<string name="mark_read">Прочитано</string>
|
||||
<string name="mark_unread">Не прочитано</string>
|
||||
<string name="set_contact_name">Имя контакта</string>
|
||||
|
||||
<!-- Actions - ChatListNavLinkView.kt -->
|
||||
@@ -319,8 +327,9 @@
|
||||
<string name="invalid_contact_link">Неверная ссылка!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Эта ссылка не является ссылкой-приглашением!</string>
|
||||
<string name="connection_request_sent">Запрос на соединение послан!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Соединение будет установлено когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Соединение будет установлено когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Соединение будет установлено, когда ваш контакт будет онлайн. Пожалуйста, подождите или проверьте позже!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Ваш контакт может сосканировать QR код в приложении.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Если вы не можете встретиться лично, вы можете <b>показать QR код во время видеозвонка</b> или поделиться ссылкой.</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Ваш профиль будет отправлен\nвашему контакту</string>
|
||||
@@ -346,12 +355,15 @@
|
||||
<string name="how_to_use_simplex_chat">Как использовать</string>
|
||||
<string name="markdown_help">Форматирование сообщений</string>
|
||||
<string name="markdown_in_messages">Форматирование сообщений</string>
|
||||
<string name="chat_with_the_founder">Соединиться с разработчиками</string>
|
||||
<string name="chat_with_the_founder">Отправьте вопросы и идеи</string>
|
||||
<string name="send_us_an_email">Отправить email</string>
|
||||
<string name="chat_lock">Блокировка SimpleX</string>
|
||||
<string name="chat_console">Консоль</string>
|
||||
<string name="smp_servers">SMP серверы</string>
|
||||
<string name="install_simplex_chat_for_terminal"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> для терминала</string>
|
||||
<string name="star_on_github">Поставить звездочку в GitHub</string>
|
||||
<string name="contribute">Внести свой вклад</string>
|
||||
<string name="rate_the_app">Оценить приложение</string>
|
||||
<string name="use_simplex_chat_servers__question">Использовать серверы предосталенные <xliff:g id="appNameFull">SimpleX Chat</xliff:g>?</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Сохраненные SMP серверы будут удалены.</string>
|
||||
<string name="your_SMP_servers">Ваши SMP серверы</string>
|
||||
@@ -391,12 +403,16 @@
|
||||
<string name="create_address">Создать адрес</string>
|
||||
<string name="delete_address__question">Удалить адрес?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Все контакты, которые соединились через этот адрес, сохранятся.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать адрес как ссылку или как QR код - через него можно с вами соединиться.</string>
|
||||
<string name="if_you_later_delete_address_you_wont_lose_contacts">Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
|
||||
<string name="if_you_delete_address_you_wont_lose_contacts">Вы можете удалить адрес, сохранив контакты, которые через него соединились.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</string>
|
||||
<string name="share_link">Поделиться\nссылкой</string>
|
||||
<string name="delete_address">Удалить\nадрес</string>
|
||||
|
||||
<!-- AcceptRequestsView.kt -->
|
||||
<string name="contact_requests">Запросы контактов</string>
|
||||
<string name="accept_requests">Принимать запросы</string>
|
||||
<string name="accept_automatically">Автоматически</string>
|
||||
<string name="section_title_welcome_message">ПРИВЕТСТВЕННОЕ СООБЩЕНИЕ</string>
|
||||
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Имя профиля:</string>
|
||||
<string name="full_name__field">"Полное имя:</string>
|
||||
@@ -544,12 +560,14 @@
|
||||
<string name="privacy_and_security">Конфиденциальность</string>
|
||||
<string name="your_privacy">Конфиденциальность</string>
|
||||
<string name="auto_accept_images">Автоприем изображений</string>
|
||||
<string name="transfer_images_faster">Передавать изображения быстрее (BETA)</string>
|
||||
<string name="send_link_previews">Отправлять картинки ссылок</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">ВЫ</string>
|
||||
<string name="settings_section_title_settings">НАСТРОЙКИ</string>
|
||||
<string name="settings_section_title_help">ПОМОЩЬ</string>
|
||||
<string name="settings_section_title_support">ПОДДЕРЖАТЬ SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_develop">ДЛЯ РАЗРАБОТЧИКОВ</string>
|
||||
<string name="settings_section_title_device">УСТРОЙСТВО</string>
|
||||
<string name="settings_section_title_chats">ЧАТЫ</string>
|
||||
@@ -664,6 +682,7 @@
|
||||
<string name="restore_database_alert_desc">Введите предыдущий пароль после восстановления резервной копии. Это действие нельзя отменить.</string>
|
||||
<string name="restore_database_alert_confirm">Восстановить</string>
|
||||
<string name="database_restore_error">Ошибка при восстановлении базы данных</string>
|
||||
<string name="restore_passphrase_not_found_desc">Пароль не найден в Keystore, пожалуйста, введите его вручную. Это могло произойти, если вы восстановили данные приложения с помощью инструмента резервного копирования. Если это не так, пожалуйста, свяжитесь с разработчиками.</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Чат остановлен</string>
|
||||
@@ -710,14 +729,27 @@
|
||||
<string name="rcv_group_event_member_added">пригласил(а) <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_connected">соединен(а)</string>
|
||||
<string name="rcv_group_event_member_left">покинул(а) группу</string>
|
||||
<string name="rcv_group_event_changed_member_role">поменял(а) роль члена %s на: %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">поменял(а) вашу роль на: %s</string>
|
||||
<string name="rcv_group_event_member_deleted">удалил(а) <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_user_deleted">удалил(а) вас из группы</string>
|
||||
<string name="rcv_group_event_group_deleted">удалил(а) группу</string>
|
||||
<string name="rcv_group_event_updated_group_profile">обновил(а) профиль группы</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">приглашен(а) через вашу ссылку группы</string>
|
||||
<string name="snd_group_event_changed_member_role">вы поменяли роль члена %s на: %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">вы поменяли роль себе на: %s</string>
|
||||
<string name="snd_group_event_member_deleted">вы удалили <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_user_left">вы покинули группу</string>
|
||||
<string name="snd_group_event_group_profile_updated">профиль группы обновлен</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">поменял(а) адрес для вас</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">смена адреса…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">вы поменяли адрес для %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">смена адреса для %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">вы поменяли адрес</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">смена адреса…</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_member">член группы</string>
|
||||
<string name="group_member_role_admin">админ</string>
|
||||
@@ -760,6 +792,14 @@
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Группа будет удалена для вас - это действие нельзя отменить!</string>
|
||||
<string name="button_leave_group">Выйти из группы</string>
|
||||
<string name="button_edit_group_profile">Редактировать профиль группы</string>
|
||||
<string name="group_link">Ссылка группы</string>
|
||||
<string name="button_create_group_link">Создать ссылку</string>
|
||||
<string name="delete_link_question">Удалить ссылку?</string>
|
||||
<string name="delete_link">Удалить ссылку</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.</string>
|
||||
<string name="all_group_members_will_remain_connected">Все члены группы, которые соединились через эту ссылку, останутся в группе.</string>
|
||||
<string name="error_creating_link_for_group">Ошибка при создании ссылки группы</string>
|
||||
<string name="error_deleting_link_for_group">Ошибка при удалении ссылки группы</string>
|
||||
|
||||
<!-- For Console chat info section -->
|
||||
<string name="section_title_for_console">ДЛЯ КОНСОЛИ</string>
|
||||
@@ -775,13 +815,12 @@
|
||||
<string name="role_in_group">Роль</string>
|
||||
<string name="change_role">Поменять роль</string>
|
||||
<string name="change_verb">Поменять</string>
|
||||
<string name="switch_verb">Переключить</string>
|
||||
<string name="change_member_role_question">Поменять роль в группе?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на \"%s\". Все в группе получат сообщение.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на \"%s\". Будет отправлено новое приглашение.</string>
|
||||
<string name="error_removing_member">Ошибка при удалении члена группы</string>
|
||||
<string name="error_changing_role">Ошибка при изменении роли</string>
|
||||
<string name="member_role">роль %s: %s</string>
|
||||
<string name="your_member_role">ваша роль: %s</string>
|
||||
<string name="info_row_group">Группа</string>
|
||||
<string name="info_row_connection">Соединение</string>
|
||||
<string name="conn_level_desc_direct">прямое</string>
|
||||
@@ -792,6 +831,7 @@
|
||||
<string name="receiving_via">Получение через</string>
|
||||
<string name="sending_via">Отправка через</string>
|
||||
<string name="network_status">Состояние сети</string>
|
||||
<string name="switch_receiving_address">Переключить адрес получения (BETA)</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Создать скрытую группу</string>
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
<!-- Connect via Link - MainActivity.kt -->
|
||||
<string name="connect_via_contact_link">Connect via contact link?</string>
|
||||
<string name="connect_via_invitation_link">Connect via invitation link?</string>
|
||||
<string name="connect_via_group_link">Connect via group link?</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Your profile will be sent to the contact that you received this link from.</string>
|
||||
<string name="you_will_join_group">You will join a group this link refers to and connect to its group members.</string>
|
||||
<string name="connect_via_link_verb">Connect</string>
|
||||
|
||||
<!-- Server info - ChatModel.kt -->
|
||||
@@ -32,6 +34,8 @@
|
||||
<string name="display_name_connecting">connecting…</string>
|
||||
<string name="description_you_shared_one_time_link">you shared one-time link</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">you shared one-time link incognito</string>
|
||||
<string name="description_via_group_link">via group link</string>
|
||||
<string name="description_via_group_link_incognito">incognito via group link</string>
|
||||
<string name="description_via_contact_address_link">via contact address link</string>
|
||||
<string name="description_via_contact_address_link_incognito">incognito via contact address link</string>
|
||||
<string name="description_via_one_time_link">via one-time link</string>
|
||||
@@ -54,19 +58,18 @@
|
||||
<string name="error_receiving_file">Error receiving file</string>
|
||||
<string name="error_creating_address">Error creating address</string>
|
||||
<string name="contact_already_exists">Contact already exists</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> via this link.</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
|
||||
<string name="invalid_connection_link">Invalid connection link</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
|
||||
<string name="connection_error_auth">Connection error (AUTH)</string>
|
||||
<string name="connection_error_auth_desc">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.</string>
|
||||
<string name="error_accepting_contact_request">Error accepting contact request</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string>
|
||||
<string name="cannot_delete_contact">Can\'t delete contact!</string>
|
||||
<string name="contact_cannot_be_deleted_as_they_are_in_groups">Contact <xliff:g id="contactName" example="Jane Doe">%1$s!</xliff:g> cannot be deleted, they are a member of the group(s) <xliff:g id="groups" example="[team, chess club]">%2$s</xliff:g>.</string>
|
||||
<string name="error_deleting_contact">Error deleting contact</string>
|
||||
<string name="error_deleting_group">Error deleting group</string>
|
||||
<string name="error_deleting_contact_request">Error deleting contact request</string>
|
||||
<string name="error_deleting_pending_contact_connection">Error deleting pending contact connection</string>
|
||||
<string name="error_changing_address">Error changing address</string>
|
||||
|
||||
<!-- background service notice - SimpleXAPI.kt -->
|
||||
<string name="icon_descr_instant_notifications">Instant notifications</string>
|
||||
@@ -128,16 +131,16 @@
|
||||
<string name="auth_enable_simplex_lock">Enable SimpleX Lock</string>
|
||||
<string name="auth_disable_simplex_lock">Disable SimpleX Lock</string>
|
||||
<string name="auth_confirm_credential">Confirm your credential</string>
|
||||
<string name="auth_error">Authentication error</string>
|
||||
<string name="auth_error_w_desc">Authentication error: <xliff:g id="desc">%1$s</xliff:g></string>
|
||||
<string name="auth_failed">Authentication failed</string>
|
||||
<string name="auth_unavailable">Authentication unavailable</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Device authentication is disabled. Turning off SimpleX Lock.</string>
|
||||
<string name="auth_retry">Retry</string>
|
||||
<string name="auth_stop_chat">Stop chat</string>
|
||||
<string name="auth_open_chat_console">Open chat console</string>
|
||||
|
||||
<!-- Chat Alerts - ChatItemView.kt -->
|
||||
<string name="message_delivery_error_title">Message delivery error</string>
|
||||
<string name="message_delivery_error_desc">Most likely this contact has deleted the connection with you.</string>
|
||||
|
||||
<!-- Chat Actions - ChatItemView.kt (and general) -->
|
||||
<string name="reply_verb">Reply</string>
|
||||
<string name="share_verb">Share</string>
|
||||
@@ -182,8 +185,10 @@
|
||||
<string name="icon_descr_cancel_file_preview">Cancel file preview</string>
|
||||
<string name="images_limit_title">Too many images!</string>
|
||||
<string name="images_limit_desc">Only 10 images can be sent at the same time</string>
|
||||
<string name="image_decoding_exception_title">Decoding error</string>
|
||||
<string name="image_decoding_exception_desc">The image cannot be decoded. Please, try a different image or contact developers.</string>
|
||||
|
||||
<!-- Images - CIImageView.kt -->
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Image</string>
|
||||
<string name="icon_descr_waiting_for_image">Waiting for image</string>
|
||||
<string name="icon_descr_asked_to_receive">Asked to receive the image</string>
|
||||
@@ -215,6 +220,8 @@
|
||||
<string name="icon_descr_server_status_disconnected">Disconnected</string>
|
||||
<string name="icon_descr_server_status_error">Error</string>
|
||||
<string name="icon_descr_server_status_pending">Pending</string>
|
||||
<string name="switch_receiving_address_question">Switch receiving address?</string>
|
||||
<string name="switch_receiving_address_desc">This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</string>
|
||||
|
||||
<!-- Message Actions - SendMsgView.kt -->
|
||||
<string name="icon_descr_send_message">Send Message</string>
|
||||
@@ -273,6 +280,7 @@
|
||||
<string name="delete_contact_menu_action">Delete</string>
|
||||
<string name="delete_group_menu_action">Delete</string>
|
||||
<string name="mark_read">Mark read</string>
|
||||
<string name="mark_unread">Mark unread</string>
|
||||
<string name="set_contact_name">Set contact name</string>
|
||||
|
||||
<!-- Actions - ChatListNavLinkView.kt -->
|
||||
@@ -319,6 +327,7 @@
|
||||
<string name="invalid_contact_link">Invalid link!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">This link is not a valid connection link!</string>
|
||||
<string name="connection_request_sent">Connection request sent!</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">You will be connected to group when the group host\'s device is online, please wait or check later!</string>
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">You will be connected when your connection request is accepted, please wait or check later!</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">You will be connected when your contact\'s device is online, please wait or check later!</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Your contact can scan QR code from the app.</string>
|
||||
@@ -349,12 +358,15 @@
|
||||
<string name="how_to_use_simplex_chat">How to use it</string>
|
||||
<string name="markdown_help">Markdown help</string>
|
||||
<string name="markdown_in_messages">Markdown in messages</string>
|
||||
<string name="chat_with_the_founder">Connect to the developers</string>
|
||||
<string name="chat_with_the_founder">Send questions and ideas</string>
|
||||
<string name="send_us_an_email">Send us email</string>
|
||||
<string name="chat_lock">SimpleX Lock</string>
|
||||
<string name="chat_console">Chat console</string>
|
||||
<string name="smp_servers">SMP servers</string>
|
||||
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
|
||||
<string name="star_on_github">Star on GitHub</string>
|
||||
<string name="contribute">Contribute</string>
|
||||
<string name="rate_the_app">Rate the app</string>
|
||||
<string name="use_simplex_chat_servers__question">Use <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers?</string>
|
||||
<string name="saved_SMP_servers_will_be_removed">Saved SMP servers will be removed.</string>
|
||||
<string name="your_SMP_servers">Your SMP servers</string>
|
||||
@@ -394,12 +406,16 @@
|
||||
<string name="create_address">Create address</string>
|
||||
<string name="delete_address__question">Delete address?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">All your contacts will remain connected.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you.</string>
|
||||
<string name="if_you_later_delete_address_you_wont_lose_contacts">If you later delete it - you won\'t lose your contacts made via the address.</string>
|
||||
<string name="if_you_delete_address_you_wont_lose_contacts">If you delete it - you won\'t lose your contacts made via this address.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">You can share your address as a link or as a QR code - anybody will be able to connect to you. You won\'t lose your contacts if you later delete it.</string>
|
||||
<string name="share_link">Share link</string>
|
||||
<string name="delete_address">Delete address</string>
|
||||
|
||||
<!-- AcceptRequestsView.kt -->
|
||||
<string name="contact_requests">Contact requests</string>
|
||||
<string name="accept_requests">Accept requests</string>
|
||||
<string name="accept_automatically">Automatically</string>
|
||||
<string name="section_title_welcome_message">WELCOME MESSAGE</string>
|
||||
|
||||
<!-- User profile details - UserProfileView.kt -->
|
||||
<string name="display_name__field">Display name:</string>
|
||||
<string name="full_name__field">"Full name:</string>
|
||||
@@ -544,12 +560,14 @@
|
||||
<string name="privacy_and_security">Privacy & security</string>
|
||||
<string name="your_privacy">Your privacy</string>
|
||||
<string name="auto_accept_images">Auto-accept images</string>
|
||||
<string name="transfer_images_faster">Transfer images faster (BETA)</string>
|
||||
<string name="send_link_previews">Send link previews</string>
|
||||
|
||||
<!-- Settings sections -->
|
||||
<string name="settings_section_title_you">YOU</string>
|
||||
<string name="settings_section_title_settings">SETTINGS</string>
|
||||
<string name="settings_section_title_help">HELP</string>
|
||||
<string name="settings_section_title_support">SUPPORT SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_develop">DEVELOP</string>
|
||||
<string name="settings_section_title_device">DEVICE</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
@@ -664,6 +682,7 @@
|
||||
<string name="restore_database_alert_desc">Please enter the previous password after restoring database backup. This action can not be undone.</string>
|
||||
<string name="restore_database_alert_confirm">Restore</string>
|
||||
<string name="database_restore_error">Restore database error</string>
|
||||
<string name="restore_passphrase_not_found_desc">Passphrase not found in Keystore, please enter it manually. This may have happened if you restored the app\'s data using a backup tool. If it\'s not the case, please, contact developers.</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Chat is stopped</string>
|
||||
@@ -710,14 +729,27 @@
|
||||
<string name="rcv_group_event_member_added">invited <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_connected">connected</string>
|
||||
<string name="rcv_group_event_member_left">left</string>
|
||||
<string name="rcv_group_event_changed_member_role">changed role of %s to %s</string>
|
||||
<string name="rcv_group_event_changed_your_role">changed your role to %s</string>
|
||||
<string name="rcv_group_event_member_deleted">removed <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_user_deleted">removed you</string>
|
||||
<string name="rcv_group_event_group_deleted">deleted group</string>
|
||||
<string name="rcv_group_event_updated_group_profile">updated group profile</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string>
|
||||
<string name="snd_group_event_changed_member_role">you changed role of %s to %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string>
|
||||
<string name="snd_group_event_member_deleted">you removed <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_user_left">you left</string>
|
||||
<string name="snd_group_event_group_profile_updated">group profile updated</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">changed address for you</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">you changed address for %s</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">changing address for %s…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">you changed address</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
|
||||
<!-- GroupMemberRole -->
|
||||
<string name="group_member_role_member">member</string>
|
||||
<string name="group_member_role_admin">admin</string>
|
||||
@@ -760,6 +792,14 @@
|
||||
<string name="delete_group_for_self_cannot_undo_warning">Group will be deleted for you - this cannot be undone!</string>
|
||||
<string name="button_leave_group">Leave group</string>
|
||||
<string name="button_edit_group_profile">Edit group profile</string>
|
||||
<string name="group_link">Group link</string>
|
||||
<string name="button_create_group_link">Create link</string>
|
||||
<string name="delete_link_question">Delete link?</string>
|
||||
<string name="delete_link">Delete link</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">You can share a link or a QR code - anybody will be able to join the group. You won\'t lose members of the group if you later delete it.</string>
|
||||
<string name="all_group_members_will_remain_connected">All group members will remain connected.</string>
|
||||
<string name="error_creating_link_for_group">Error creating group link</string>
|
||||
<string name="error_deleting_link_for_group">Error deleting group link</string>
|
||||
|
||||
<!-- For Console chat info section -->
|
||||
<string name="section_title_for_console">FOR CONSOLE</string>
|
||||
@@ -775,13 +815,12 @@
|
||||
<string name="role_in_group">Role</string>
|
||||
<string name="change_role">Change role</string>
|
||||
<string name="change_verb">Change</string>
|
||||
<string name="switch_verb">Switch</string>
|
||||
<string name="change_member_role_question">Change group role?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string>
|
||||
<string name="error_removing_member">Error removing member</string>
|
||||
<string name="error_changing_role">Error changing role</string>
|
||||
<string name="member_role">member %s role: %s</string>
|
||||
<string name="your_member_role">your role: %s</string>
|
||||
<string name="info_row_group">Group</string>
|
||||
<string name="info_row_connection">Connection</string>
|
||||
<string name="conn_level_desc_direct">direct</string>
|
||||
@@ -792,6 +831,7 @@
|
||||
<string name="receiving_via">Receiving via</string>
|
||||
<string name="sending_via">Sending via</string>
|
||||
<string name="network_status">Network status</string>
|
||||
<string name="switch_receiving_address">Switch receiving address (BETA)</string>
|
||||
|
||||
<!-- AddGroupView.kt -->
|
||||
<string name="create_secret_group_title">Create secret group</string>
|
||||
|
||||
@@ -102,9 +102,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
|
||||
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
||||
var window: UIWindow?
|
||||
var windowScene: UIWindowScene?
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
self.windowScene = windowScene
|
||||
window = windowScene.keyWindow
|
||||
window?.tintColor = UIColor(cgColor: getUIAccentColorDefault())
|
||||
window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault()
|
||||
|
||||
@@ -70,6 +70,7 @@ struct ContentView: View {
|
||||
dismissAllSheets(animated: false) {
|
||||
justAuthenticate()
|
||||
}
|
||||
chatModel.chatId = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var groupMembers: [GroupMember] = []
|
||||
// items in the terminal view
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@Published var userAddress: String?
|
||||
@Published var userAddress: UserContactLink?
|
||||
@Published var userSMPServers: [String]?
|
||||
@Published var chatItemTTL: ChatItemTTL = .none
|
||||
@Published var appOpenUrl: URL?
|
||||
@@ -98,7 +98,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateContact(_ contact: Contact) {
|
||||
updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact)
|
||||
updateChat(.direct(contact: contact), addMissing: !contact.isIndirectContact && !contact.viaGroupLink)
|
||||
}
|
||||
|
||||
func updateGroup(_ groupInfo: GroupInfo) {
|
||||
@@ -113,6 +113,17 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func _updateChat(_ id: ChatId, _ update: @escaping (Chat) -> Void) {
|
||||
if let i = getChatIndex(id) {
|
||||
// we need to separately update the chat object, as it is ObservedObject,
|
||||
// and chat in the list so the list view is updated...
|
||||
// simply updating chats[i] replaces the object without updating the current object in the list
|
||||
let chat = chats[i]
|
||||
update(chat)
|
||||
chats[i] = chat
|
||||
}
|
||||
}
|
||||
|
||||
func updateNetworkStatus(_ id: ChatId, _ status: Chat.NetworkStatus) {
|
||||
if let i = getChatIndex(id) {
|
||||
chats[i].serverInfo.networkStatus = status
|
||||
@@ -257,7 +268,7 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func markChatItemsRead(_ cInfo: ChatInfo) {
|
||||
// update preview
|
||||
if let chat = getChat(cInfo.id) {
|
||||
_updateChat(cInfo.id) { chat in
|
||||
NtfManager.shared.decNtfBadgeCount(by: chat.chatStats.unreadCount)
|
||||
chat.chatStats = ChatStats()
|
||||
}
|
||||
@@ -282,11 +293,11 @@ final class ChatModel: ObservableObject {
|
||||
if let cItem = aboveItem {
|
||||
if chatId == cInfo.id, let i = reversedChatItems.firstIndex(where: { $0.id == cItem.id }) {
|
||||
markCurrentChatRead(fromIndex: i)
|
||||
if let chat = getChat(cInfo.id) {
|
||||
_updateChat(cInfo.id) { chat in
|
||||
var unreadBelow = 0
|
||||
var j = i - 1
|
||||
while j >= 0 {
|
||||
if case .rcvNew = reversedChatItems[j].meta.itemStatus {
|
||||
if case .rcvNew = self.reversedChatItems[j].meta.itemStatus {
|
||||
unreadBelow += 1
|
||||
}
|
||||
j -= 1
|
||||
@@ -303,6 +314,12 @@ final class ChatModel: ObservableObject {
|
||||
markChatItemsRead(cInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
||||
_updateChat(cInfo.id) { chat in
|
||||
chat.chatStats.unreadChat = unreadChat
|
||||
}
|
||||
}
|
||||
|
||||
func clearChat(_ cInfo: ChatInfo) {
|
||||
// clear preview
|
||||
@@ -369,6 +386,11 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func upsertGroupMember(_ groupInfo: GroupInfo, _ member: GroupMember) -> Bool {
|
||||
// user member was updated
|
||||
if groupInfo.membership.groupMemberId == member.groupMemberId {
|
||||
updateGroup(groupInfo)
|
||||
return false
|
||||
}
|
||||
// update current chat
|
||||
if chatId == groupInfo.id {
|
||||
if let i = groupMembers.firstIndex(where: { $0.id == member.id }) {
|
||||
|
||||
@@ -357,6 +357,14 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiSwitchContact(contactId: Int64) async throws {
|
||||
try await sendCommandOkResp(.apiSwitchContact(contactId: contactId))
|
||||
}
|
||||
|
||||
func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws {
|
||||
try await sendCommandOkResp(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId))
|
||||
}
|
||||
|
||||
func apiAddContact() async -> String? {
|
||||
let r = await chatSendCmd(.addContact, bgTask: false)
|
||||
if case let .invitation(connReqInvitation) = r { return connReqInvitation }
|
||||
@@ -371,9 +379,13 @@ func apiConnect(connReq: String) async -> ConnReqType? {
|
||||
case .sentConfirmation: return .invitation
|
||||
case .sentInvitation: return .contact
|
||||
case let .contactAlreadyExists(contact):
|
||||
let m = ChatModel.shared
|
||||
if let c = m.getContactChat(contact.contactId) {
|
||||
await MainActor.run { m.chatId = c.id }
|
||||
}
|
||||
am.showAlertMsg(
|
||||
title: "Contact already exists",
|
||||
message: "You are already connected to \(contact.displayName) via this link."
|
||||
message: "You are already connected to \(contact.displayName)."
|
||||
)
|
||||
return nil
|
||||
case .chatCmdError(.error(.invalidConnReq)):
|
||||
@@ -426,18 +438,10 @@ func deleteChat(_ chat: Chat) async {
|
||||
DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) }
|
||||
} catch let error {
|
||||
logger.error("deleteChat apiDeleteChat error: \(responseError(error))")
|
||||
switch error as? ChatResponse {
|
||||
case let .chatCmdError(.error(.contactGroups(contact, groupNames))):
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Can't delete contact!",
|
||||
message: "Contact \(contact.displayName) cannot be deleted, they are a member of the group(s) \(groupNames.joined(separator: ", "))."
|
||||
)
|
||||
default:
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Error deleting chat!",
|
||||
message: "Error: \(responseError(error))"
|
||||
)
|
||||
}
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Error deleting chat!",
|
||||
message: "Error: \(responseError(error))"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,13 +500,20 @@ func apiDeleteUserAddress() async throws {
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetUserAddress() throws -> String? {
|
||||
func apiGetUserAddress() throws -> UserContactLink? {
|
||||
let r = chatSendCmdSync(.showMyAddress)
|
||||
switch r {
|
||||
case let .userContactLink(connReq):
|
||||
return connReq
|
||||
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))
|
||||
switch r {
|
||||
case let .userContactLinkUpdated(contactLink): return contactLink
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
@@ -537,14 +548,19 @@ func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async thr
|
||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
|
||||
}
|
||||
|
||||
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 {
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId) {
|
||||
let inline = privacyTransferImagesInlineGroupDefault.get()
|
||||
if let chatItem = await apiReceiveFile(fileId: fileId, inline: inline) {
|
||||
DispatchQueue.main.async { chatItemSimpleUpdate(chatItem) }
|
||||
}
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId))
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) async -> AChatItem? {
|
||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
let am = AlertManager.shared
|
||||
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
|
||||
if case .rcvFileAcceptedSndCancelled = r {
|
||||
@@ -642,16 +658,31 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
|
||||
|
||||
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
|
||||
do {
|
||||
let minItemId = chat.chatStats.minUnreadItemId
|
||||
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
|
||||
DispatchQueue.main.async { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
let minItemId = chat.chatStats.minUnreadItemId
|
||||
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
|
||||
await MainActor.run { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
await markChatUnread(chat, unreadChat: false)
|
||||
}
|
||||
} catch {
|
||||
logger.error("markChatRead apiChatRead error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
|
||||
do {
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiChatUnread(type: cInfo.chatType, id: cInfo.apiId, unreadChat: unreadChat)
|
||||
await MainActor.run { ChatModel.shared.markChatUnread(cInfo, unreadChat: unreadChat) }
|
||||
} catch {
|
||||
logger.error("markChatUnread apiChatUnread error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
|
||||
do {
|
||||
logger.debug("apiMarkChatItemRead: \(cItem.id)")
|
||||
@@ -689,7 +720,7 @@ 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 let .userAcceptedGroupSent(groupInfo, _): return .joined(groupInfo: groupInfo)
|
||||
case .chatCmdError(.errorAgent(.SMP(.AUTH))): return .invitationRemoved
|
||||
case .chatCmdError(.errorStore(.groupNotFound)): return .groupNotFound
|
||||
default: throw r
|
||||
@@ -743,6 +774,29 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCreateGroupLink(_ groupId: Int64) async throws -> String {
|
||||
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId))
|
||||
if case let .groupLinkCreated(_, connReq) = r { return connReq }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiDeleteGroupLink(_ groupId: Int64) async throws {
|
||||
let r = await chatSendCmd(.apiDeleteGroupLink(groupId: groupId))
|
||||
if case .groupLinkDeleted = r { return }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiGetGroupLink(_ groupId: Int64) throws -> String? {
|
||||
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
|
||||
switch r {
|
||||
case let .groupLink(_, connReq):
|
||||
return connReq
|
||||
case .chatCmdError(chatError: .errorStore(storeError: .groupLinkNotFound)):
|
||||
return nil
|
||||
default: throw r
|
||||
}
|
||||
}
|
||||
|
||||
func initializeChat(start: Bool, dbKey: String? = nil) throws {
|
||||
logger.debug("initializeChat")
|
||||
let m = ChatModel.shared
|
||||
@@ -840,16 +894,20 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
m.updateContactConnection(connection)
|
||||
case let .contactConnectionDeleted(connection):
|
||||
m.removeChat(connection.id)
|
||||
case let .contactConnected(contact):
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
m.updateNetworkStatus(contact.id, .connected)
|
||||
NtfManager.shared.notifyContactConnected(contact)
|
||||
case let .contactConnected(contact, _):
|
||||
if !contact.viaGroupLink {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
m.updateNetworkStatus(contact.id, .connected)
|
||||
NtfManager.shared.notifyContactConnected(contact)
|
||||
}
|
||||
case let .contactConnecting(contact):
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
if !contact.viaGroupLink {
|
||||
m.updateContact(contact)
|
||||
m.dismissConnReqView(contact.activeConn.id)
|
||||
m.removeChat(contact.activeConn.id)
|
||||
}
|
||||
case let .receivedContactRequest(contactRequest):
|
||||
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
|
||||
if m.hasChat(contactRequest.id) {
|
||||
@@ -866,6 +924,13 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
if m.hasChat(toContact.id) {
|
||||
m.updateChatInfo(cInfo)
|
||||
}
|
||||
case let .contactsMerged(intoContact, mergedContact):
|
||||
if m.hasChat(mergedContact.id) {
|
||||
if m.chatId == mergedContact.id {
|
||||
m.chatId = intoContact.id
|
||||
}
|
||||
m.removeChat(mergedContact.id)
|
||||
}
|
||||
case let .contactsSubscribed(_, contactRefs):
|
||||
updateContactsStatus(contactRefs, status: .connected)
|
||||
case let .contactsDisconnected(_, contactRefs):
|
||||
@@ -925,11 +990,14 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
_ = m.upsertChatItem(cInfo, cItem)
|
||||
}
|
||||
case let .receivedGroupInvitation(groupInfo, _, _):
|
||||
m.addChat(Chat(
|
||||
chatInfo: .group(groupInfo: groupInfo),
|
||||
chatItems: []
|
||||
))
|
||||
m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated
|
||||
// NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation?
|
||||
case let .userAcceptedGroupSent(groupInfo, hostContact):
|
||||
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
|
||||
@@ -957,9 +1025,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
case let .sndFileComplete(aChatItem, _):
|
||||
chatItemSimpleUpdate(aChatItem)
|
||||
let cItem = aChatItem.chatItem
|
||||
let mc = cItem.content.msgContent
|
||||
if aChatItem.chatInfo.chatType == .direct,
|
||||
let mc = cItem.content.msgContent,
|
||||
mc.isFile(),
|
||||
case .file = mc,
|
||||
let fileName = cItem.file?.filePath {
|
||||
removeFile(fileName)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,14 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
|
||||
@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]?) -> some View {
|
||||
if let servers = servers,
|
||||
servers.count > 0 {
|
||||
infoRow(title, serverHost(servers[0]))
|
||||
HStack {
|
||||
Text(title).frame(width: 120, alignment: .leading)
|
||||
Button(serverHost(servers[0])) {
|
||||
UIPasteboard.general.string = servers.joined(separator: ";")
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +54,7 @@ struct ChatInfoView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@ObservedObject var chat: Chat
|
||||
var contact: Contact
|
||||
var connectionStats: ConnectionStats?
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
var customUserProfile: Profile?
|
||||
@State var localAlias: String
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
@@ -56,16 +63,18 @@ struct ChatInfoView: View {
|
||||
|
||||
enum ChatInfoViewAlert: Identifiable {
|
||||
case deleteContactAlert
|
||||
case contactGroupsAlert(groupNames: [GroupName])
|
||||
case clearChatAlert
|
||||
case networkStatusAlert
|
||||
case switchAddressAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .deleteContactAlert: return "deleteContactAlert"
|
||||
case .contactGroupsAlert: return "contactGroupsAlert"
|
||||
case .clearChatAlert: return "clearChatAlert"
|
||||
case .networkStatusAlert: return "networkStatusAlert"
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,12 +99,17 @@ struct ChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
networkStatusRow()
|
||||
.onTapGesture {
|
||||
alert = .networkStatusAlert
|
||||
}
|
||||
Section("Servers") {
|
||||
networkStatusRow()
|
||||
.onTapGesture {
|
||||
alert = .networkStatusAlert
|
||||
}
|
||||
if developerTools {
|
||||
Button("Change receiving address (BETA)") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
}
|
||||
if let connStats = connectionStats {
|
||||
smpServers("Receiving via", connStats.rcvServers)
|
||||
smpServers("Sending via", connStats.sndServers)
|
||||
}
|
||||
@@ -119,9 +133,10 @@ struct ChatInfoView: View {
|
||||
.alert(item: $alert) { alertItem in
|
||||
switch(alertItem) {
|
||||
case .deleteContactAlert: return deleteContactAlert()
|
||||
case let .contactGroupsAlert(groupNames): return contactGroupsAlert(groupNames)
|
||||
case .clearChatAlert: return clearChatAlert()
|
||||
case .networkStatusAlert: return networkStatusAlert()
|
||||
case .switchAddressAlert: return switchAddressAlert(switchContactAddress)
|
||||
case let .error(title, error): return mkAlert(title: title, message: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,9 +244,10 @@ struct ChatInfoView: View {
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("deleteContactAlert apiDeleteChat error: \(error.localizedDescription)")
|
||||
if case let .chatCmdError(.error(.contactGroups(_, groupNames))) = error as? ChatResponse {
|
||||
alert = .contactGroupsAlert(groupNames: groupNames)
|
||||
logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error deleting contact")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,13 +256,6 @@ struct ChatInfoView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func contactGroupsAlert(_ groupNames: [GroupName]) -> Alert {
|
||||
Alert(
|
||||
title: Text("Can't delete contact!"),
|
||||
message: Text("Contact \(contact.displayName) cannot be deleted, they are a member of the group(s) \(groupNames.joined(separator: ", ")).")
|
||||
)
|
||||
}
|
||||
|
||||
private func clearChatAlert() -> Alert {
|
||||
Alert(
|
||||
title: Text("Clear conversation?"),
|
||||
@@ -267,10 +276,38 @@ struct ChatInfoView: View {
|
||||
message: Text(chat.serverInfo.networkStatus.statusExplanation)
|
||||
)
|
||||
}
|
||||
|
||||
private func switchContactAddress() {
|
||||
Task {
|
||||
do {
|
||||
try await apiSwitchContact(contactId: contact.apiId)
|
||||
} catch let error {
|
||||
logger.error("switchContactAddress apiSwitchContact error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing address")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert {
|
||||
Alert(
|
||||
title: Text("Change receiving address?"),
|
||||
message: Text("This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)."),
|
||||
primaryButton: .destructive(Text("Change"), action: switchAddress),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
struct ChatInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, localAlias: "")
|
||||
ChatInfoView(
|
||||
chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []),
|
||||
contact: Contact.sampleData,
|
||||
connectionStats: Binding.constant(nil),
|
||||
localAlias: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// CIGroupEventView.swift
|
||||
// CIEventView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by JRoberts on 20.07.2022.
|
||||
@@ -9,7 +9,7 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIGroupEventView: View {
|
||||
struct CIEventView: View {
|
||||
var chatItem: ChatItem
|
||||
|
||||
var body: some View {
|
||||
@@ -43,8 +43,8 @@ struct CIGroupEventView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct CIGroupEventView_Previews: PreviewProvider {
|
||||
struct CIEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIGroupEventView(chatItem: ChatItem.getGroupEventSample())
|
||||
CIEventView(chatItem: ChatItem.getGroupEventSample())
|
||||
}
|
||||
}
|
||||
@@ -68,14 +68,16 @@ struct FramedItemView: View {
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
case let .file(text):
|
||||
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
|
||||
.overlay(DetermineWidth())
|
||||
if text != "" {
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
ciFileView(chatItem, text)
|
||||
case let .link(_, preview):
|
||||
CILinkView(linkPreview: preview)
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
case let .unknown(_, text: text):
|
||||
if chatItem.file == nil {
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
} else {
|
||||
ciFileView(chatItem, text)
|
||||
}
|
||||
default:
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
@@ -191,6 +193,14 @@ struct FramedItemView: View {
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func ciFileView(_ ci: ChatItem, _ text: String) -> some View {
|
||||
CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited)
|
||||
.overlay(DetermineWidth())
|
||||
if text != "" {
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isRightToLeft(_ s: String) -> Bool {
|
||||
@@ -207,8 +217,17 @@ private struct MetaColorPreferenceKey: PreferenceKey {
|
||||
}
|
||||
}
|
||||
|
||||
func onlyImage(_ ci: ChatItem) -> Bool {
|
||||
if case let .image(text, _) = ci.content.msgContent {
|
||||
return ci.quotedItem == nil && text == ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
ci.chatDir.sent
|
||||
onlyImage(ci)
|
||||
? Color.clear
|
||||
: ci.chatDir.sent
|
||||
? (colorScheme == .light ? sentColorLight : sentColorDark)
|
||||
: Color(uiColor: .tertiarySystemGroupedBackground)
|
||||
}
|
||||
|
||||
@@ -27,8 +27,10 @@ struct ChatItemView: View {
|
||||
case .rcvIntegrityError: IntegrityErrorItemView(chatItem: chatItem, showMember: showMember)
|
||||
case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole)
|
||||
case .rcvGroupEvent: groupEventItemView()
|
||||
case .sndGroupEvent: groupEventItemView()
|
||||
case .rcvGroupEvent: eventItemView()
|
||||
case .sndGroupEvent: eventItemView()
|
||||
case .rcvConnEvent: eventItemView()
|
||||
case .sndConnEvent: eventItemView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +54,8 @@ struct ChatItemView: View {
|
||||
CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito)
|
||||
}
|
||||
|
||||
private func groupEventItemView() -> some View {
|
||||
CIGroupEventView(chatItem: chatItem)
|
||||
private func eventItemView() -> some View {
|
||||
CIEventView(chatItem: chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,13 @@ struct ChatView: View {
|
||||
.navigationTitle(cInfo.chatViewName)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.onAppear {
|
||||
if chat.chatStats.unreadChat {
|
||||
Task {
|
||||
await markChatUnread(chat, unreadChat: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
@@ -100,7 +107,7 @@ struct ChatView: View {
|
||||
connectionStats = nil
|
||||
customUserProfile = nil
|
||||
}) {
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
|
||||
ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias)
|
||||
}
|
||||
} else if case let .group(groupInfo) = cInfo {
|
||||
Button {
|
||||
@@ -385,8 +392,11 @@ struct ChatView: View {
|
||||
await MainActor.run { selectedMember = member }
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedMember, onDismiss: { memberConnectionStats = nil }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats)
|
||||
.sheet(item: $selectedMember, onDismiss: {
|
||||
selectedMember = nil
|
||||
memberConnectionStats = nil
|
||||
}) { _ in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: $selectedMember, connectionStats: $memberConnectionStats)
|
||||
}
|
||||
} else {
|
||||
Rectangle().fill(.clear)
|
||||
|
||||
@@ -16,6 +16,7 @@ struct GroupChatInfoView: View {
|
||||
var groupInfo: GroupInfo
|
||||
@ObservedObject private var alertManager = AlertManager.shared
|
||||
@State private var alert: GroupChatInfoViewAlert? = nil
|
||||
@State private var groupLink: String?
|
||||
@State private var showAddMembersSheet: Bool = false
|
||||
@State private var selectedMember: GroupMember? = nil
|
||||
@State private var showGroupProfile: Bool = false
|
||||
@@ -43,6 +44,7 @@ struct GroupChatInfoView: View {
|
||||
|
||||
Section("\(members.count + 1) members") {
|
||||
if groupInfo.canAddMembers {
|
||||
groupLinkButton()
|
||||
if (chat.chatInfo.incognito) {
|
||||
Label("Invite members", systemImage: "plus")
|
||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||
@@ -69,8 +71,11 @@ struct GroupChatInfoView: View {
|
||||
.sheet(isPresented: $showAddMembersSheet) {
|
||||
AddGroupMembersView(chat: chat, groupInfo: groupInfo)
|
||||
}
|
||||
.sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats)
|
||||
.sheet(item: $selectedMember, onDismiss: {
|
||||
selectedMember = nil
|
||||
connectionStats = nil
|
||||
}) { _ in
|
||||
GroupMemberInfoView(groupInfo: groupInfo, member: $selectedMember, connectionStats: $connectionStats)
|
||||
}
|
||||
.sheet(isPresented: $showGroupProfile) {
|
||||
GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile)
|
||||
@@ -105,7 +110,13 @@ struct GroupChatInfoView: View {
|
||||
case .clearChatAlert: return clearChatAlert()
|
||||
case .leaveGroupAlert: return leaveGroupAlert()
|
||||
case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert()
|
||||
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
do {
|
||||
groupLink = try apiGetGroupLink(groupInfo.groupId)
|
||||
} catch let error {
|
||||
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,6 +186,16 @@ struct GroupChatInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func groupLinkButton() -> some View {
|
||||
NavigationLink {
|
||||
GroupLinkView(groupId: groupInfo.groupId, groupLink: $groupLink)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
Label("Group link", systemImage: "link")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
func editGroupButton() -> some View {
|
||||
Button {
|
||||
showGroupProfile = true
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// GroupLinkView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by JRoberts on 15.10.2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct GroupLinkView: View {
|
||||
var groupId: Int64
|
||||
@Binding var groupLink: String?
|
||||
@State private var alert: GroupLinkAlert?
|
||||
|
||||
private enum GroupLinkAlert: Identifiable {
|
||||
case deleteLink
|
||||
case error(title: LocalizedStringKey, error: String = "")
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .deleteLink: return "deleteLink"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack (alignment: .leading) {
|
||||
Text("Group link")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(.bottom)
|
||||
Text("You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.")
|
||||
.padding(.bottom)
|
||||
if let groupLink = groupLink {
|
||||
QRCode(uri: groupLink)
|
||||
HStack {
|
||||
Button {
|
||||
showShareSheet(items: [groupLink])
|
||||
} label: {
|
||||
Label("Share link", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
.padding()
|
||||
|
||||
Button(role: .destructive) { alert = .deleteLink } label: {
|
||||
Label("Delete link", systemImage: "trash")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
groupLink = try await apiCreateGroupLink(groupId)
|
||||
} catch let error {
|
||||
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error creating group link")
|
||||
alert = .error(title: a.title, error: "\(a.message)")
|
||||
}
|
||||
}
|
||||
} label: { Label("Create link", systemImage: "link.badge.plus") }
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case .deleteLink:
|
||||
return Alert(
|
||||
title: Text("Delete link?"),
|
||||
message: Text("All group members will remain connected."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteGroupLink(groupId)
|
||||
await MainActor.run {
|
||||
groupLink = nil
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("GroupLinkView apiDeleteGroupLink: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}, secondaryButton: .cancel()
|
||||
)
|
||||
case let .error(title, error):
|
||||
return Alert(title: Text(title), message: Text("\(error)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupLinkView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
@State var groupLink: String? = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
|
||||
@State var noGroupLink: String? = nil
|
||||
|
||||
return Group {
|
||||
GroupLinkView(groupId: 1, groupLink: $groupLink)
|
||||
GroupLinkView(groupId: 1, groupLink: $noGroupLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,23 @@ struct GroupMemberInfoView: View {
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
var groupInfo: GroupInfo
|
||||
@State var member: GroupMember
|
||||
var connectionStats: ConnectionStats?
|
||||
@Binding var member: GroupMember?
|
||||
@Binding var connectionStats: ConnectionStats?
|
||||
@State private var newRole: GroupMemberRole = .member
|
||||
@State private var alert: GroupMemberInfoViewAlert?
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
|
||||
enum GroupMemberInfoViewAlert: Identifiable {
|
||||
case removeMemberAlert
|
||||
case changeMemberRoleAlert(role: GroupMemberRole)
|
||||
case removeMemberAlert(mem: GroupMember)
|
||||
case changeMemberRoleAlert(mem: GroupMember, role: GroupMemberRole)
|
||||
case switchAddressAlert
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey)
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .removeMemberAlert: return "removeMemberAlert"
|
||||
case let .changeMemberRoleAlert(role): return "changeMemberRoleAlert \(role.rawValue)"
|
||||
case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)"
|
||||
case .switchAddressAlert: return "switchAddressAlert"
|
||||
case let .error(title, _): return "error \(title)"
|
||||
}
|
||||
}
|
||||
@@ -35,76 +37,78 @@ struct GroupMemberInfoView: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
groupMemberInfoHeader()
|
||||
.listRowBackground(Color.clear)
|
||||
if let member = member {
|
||||
List {
|
||||
groupMemberInfoHeader(member)
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
if let contactId = member.memberContactId {
|
||||
Section {
|
||||
openDirectChatButton(contactId)
|
||||
if let contactId = member.memberContactId {
|
||||
Section {
|
||||
openDirectChatButton(contactId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
Section("Member") {
|
||||
infoRow("Group", groupInfo.displayName)
|
||||
|
||||
// HStack {
|
||||
// if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
// Picker("Change role", selection: $newRole) {
|
||||
// ForEach(roles) { role in
|
||||
// Text(role.text)
|
||||
// .foregroundStyle(.secondary)
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// Text("Role")
|
||||
// Spacer()
|
||||
// Text(member.memberRole.text)
|
||||
// .foregroundStyle(.secondary)
|
||||
// }
|
||||
// }
|
||||
// .onAppear { newRole = member.memberRole }
|
||||
// .onChange(of: newRole) { _ in
|
||||
// if newRole != member.memberRole {
|
||||
// alert = .changeMemberRoleAlert(role: newRole)
|
||||
// }
|
||||
// }
|
||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||
Picker("Change role", selection: $newRole) {
|
||||
ForEach(roles) { role in
|
||||
Text(role.text)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
infoRow("Role", member.memberRole.text)
|
||||
}
|
||||
|
||||
// TODO invited by - need to get contact by contact id
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
// TODO invited by - need to get contact by contact id
|
||||
if let conn = member.activeConn {
|
||||
let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel)
|
||||
infoRow("Connection", connLevelDesc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let connStats = connectionStats {
|
||||
Section("Servers") {
|
||||
// TODO network connection status
|
||||
smpServers("Receiving via", connStats.rcvServers)
|
||||
smpServers("Sending via", connStats.sndServers)
|
||||
if developerTools {
|
||||
Button("Change receiving address (BETA)") {
|
||||
alert = .switchAddressAlert
|
||||
}
|
||||
}
|
||||
if let connStats = connectionStats {
|
||||
smpServers("Receiving via", connStats.rcvServers)
|
||||
smpServers("Sending via", connStats.sndServers)
|
||||
}
|
||||
}
|
||||
|
||||
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
Section {
|
||||
removeMemberButton(member)
|
||||
}
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if member.canBeRemoved(groupInfo: groupInfo) {
|
||||
Section {
|
||||
removeMemberButton()
|
||||
}
|
||||
}
|
||||
|
||||
if developerTools {
|
||||
Section("For console") {
|
||||
infoRow("Local name", member.localDisplayName)
|
||||
infoRow("Database ID", "\(member.groupMemberId)")
|
||||
.navigationBarHidden(true)
|
||||
.onAppear { newRole = member.memberRole }
|
||||
.onChange(of: newRole) { _ in
|
||||
if newRole != member.memberRole {
|
||||
alert = .changeMemberRoleAlert(mem: member, role: newRole)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.alert(item: $alert) { alertItem in
|
||||
switch(alertItem) {
|
||||
case .removeMemberAlert: return removeMemberAlert()
|
||||
case .changeMemberRoleAlert: return changeMemberRoleAlert()
|
||||
case let .removeMemberAlert(mem): return removeMemberAlert(mem)
|
||||
case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem)
|
||||
case .switchAddressAlert: return switchAddressAlert(switchMemberAddress)
|
||||
case let .error(title, error): return Alert(title: Text(title), message: Text(error))
|
||||
}
|
||||
}
|
||||
@@ -134,18 +138,18 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func groupMemberInfoHeader() -> some View {
|
||||
private func groupMemberInfoHeader(_ mem: GroupMember) -> some View {
|
||||
VStack {
|
||||
ProfileImage(imageStr: member.image, color: Color(uiColor: .tertiarySystemFill))
|
||||
ProfileImage(imageStr: mem.image, color: Color(uiColor: .tertiarySystemFill))
|
||||
.frame(width: 192, height: 192)
|
||||
.padding(.top, 12)
|
||||
.padding()
|
||||
Text(member.displayName)
|
||||
Text(mem.displayName)
|
||||
.font(.largeTitle)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, 2)
|
||||
if member.fullName != "" && member.fullName != member.displayName {
|
||||
Text(member.fullName)
|
||||
if mem.fullName != "" && mem.fullName != mem.displayName {
|
||||
Text(mem.fullName)
|
||||
.font(.title2)
|
||||
.lineLimit(2)
|
||||
}
|
||||
@@ -153,25 +157,25 @@ struct GroupMemberInfoView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
func removeMemberButton() -> some View {
|
||||
func removeMemberButton(_ mem: GroupMember) -> some View {
|
||||
Button(role: .destructive) {
|
||||
alert = .removeMemberAlert
|
||||
alert = .removeMemberAlert(mem: mem)
|
||||
} label: {
|
||||
Label("Remove member", systemImage: "trash")
|
||||
.foregroundColor(Color.red)
|
||||
}
|
||||
}
|
||||
|
||||
private func removeMemberAlert() -> Alert {
|
||||
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
|
||||
Alert(
|
||||
title: Text("Remove member?"),
|
||||
message: Text("Member will be removed from group - this cannot be undone!"),
|
||||
primaryButton: .destructive(Text("Remove")) {
|
||||
Task {
|
||||
do {
|
||||
let member = try await apiRemoveMember(groupInfo.groupId, member.groupMemberId)
|
||||
let updatedMember = try await apiRemoveMember(groupInfo.groupId, mem.groupMemberId)
|
||||
await MainActor.run {
|
||||
_ = chatModel.upsertGroupMember(groupInfo, member)
|
||||
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
dismiss()
|
||||
}
|
||||
} catch let error {
|
||||
@@ -185,20 +189,21 @@ struct GroupMemberInfoView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private func changeMemberRoleAlert() -> Alert {
|
||||
private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert {
|
||||
Alert(
|
||||
title: Text("Change member role?"),
|
||||
message: member.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."),
|
||||
message: mem.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."),
|
||||
primaryButton: .default(Text("Change")) {
|
||||
Task {
|
||||
do {
|
||||
let mem = try await apiMemberRole(groupInfo.groupId, member.groupMemberId, newRole)
|
||||
let updatedMember = try await apiMemberRole(groupInfo.groupId, mem.groupMemberId, newRole)
|
||||
await MainActor.run {
|
||||
member = mem
|
||||
_ = chatModel.upsertGroupMember(groupInfo, mem)
|
||||
member = updatedMember
|
||||
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||
}
|
||||
|
||||
} catch let error {
|
||||
newRole = member.memberRole
|
||||
newRole = mem.memberRole
|
||||
logger.error("apiMemberRole error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing role")
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
@@ -206,14 +211,34 @@ struct GroupMemberInfoView: View {
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel {
|
||||
newRole = member.memberRole
|
||||
newRole = mem.memberRole
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func switchMemberAddress() {
|
||||
Task {
|
||||
do {
|
||||
if let member = member {
|
||||
try await apiSwitchGroupMember(groupInfo.apiId, member.groupMemberId)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("switchMemberAddress apiSwitchGroupMember error: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error changing address")
|
||||
await MainActor.run {
|
||||
alert = .error(title: a.title, error: a.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GroupMemberInfoView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData)
|
||||
GroupMemberInfoView(
|
||||
groupInfo: GroupInfo.sampleData,
|
||||
member: Binding.constant(GroupMember.sampleData),
|
||||
connectionStats: Binding.constant(nil)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +53,7 @@ struct ChatListNavLink: View {
|
||||
disabled: !contact.ready
|
||||
)
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
markReadButton()
|
||||
}
|
||||
markReadButton()
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if !chat.chatItems.isEmpty {
|
||||
@@ -89,7 +87,7 @@ struct ChatListNavLink: View {
|
||||
ChatPreviewView(chat: chat)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
joinGroupButton(groupInfo.hostConnCustomUserProfileId)
|
||||
joinGroupButton()
|
||||
if groupInfo.canDelete {
|
||||
deleteGroupChatButton(groupInfo)
|
||||
}
|
||||
@@ -116,9 +114,7 @@ struct ChatListNavLink: View {
|
||||
)
|
||||
.frame(height: rowHeights[dynamicTypeSize])
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
markReadButton()
|
||||
}
|
||||
markReadButton()
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
if !chat.chatItems.isEmpty {
|
||||
@@ -141,7 +137,7 @@ struct ChatListNavLink: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func joinGroupButton(_ hostConnCustomUserProfileId: Int64?) -> some View {
|
||||
private func joinGroupButton() -> some View {
|
||||
Button {
|
||||
joinGroup(chat.chatInfo.apiId)
|
||||
} label: {
|
||||
@@ -150,13 +146,23 @@ struct ChatListNavLink: View {
|
||||
.tint(chat.chatInfo.incognito ? .indigo : .accentColor)
|
||||
}
|
||||
|
||||
private func markReadButton() -> some View {
|
||||
Button {
|
||||
Task { await markChatRead(chat) }
|
||||
} label: {
|
||||
Label("Read", systemImage: "checkmark")
|
||||
@ViewBuilder private func markReadButton() -> some View {
|
||||
if chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat {
|
||||
Button {
|
||||
Task { await markChatRead(chat) }
|
||||
} label: {
|
||||
Label("Read", systemImage: "checkmark")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
} else {
|
||||
Button {
|
||||
Task { await markChatUnread(chat) }
|
||||
} label: {
|
||||
Label("Unread", systemImage: "circlebadge.fill")
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
}
|
||||
.tint(Color.accentColor)
|
||||
|
||||
}
|
||||
|
||||
private func clearChatButton() -> some View {
|
||||
|
||||
@@ -17,7 +17,6 @@ struct ChatPreviewView: View {
|
||||
|
||||
var body: some View {
|
||||
let cItem = chat.chatItems.last
|
||||
let unread = chat.chatStats.unreadCount
|
||||
return HStack(spacing: 8) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ChatInfoImage(chat: chat)
|
||||
@@ -41,7 +40,7 @@ struct ChatPreviewView: View {
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
ZStack(alignment: .topTrailing) {
|
||||
chatPreviewText(cItem, unread)
|
||||
chatPreviewText(cItem)
|
||||
if case .direct = chat.chatInfo {
|
||||
chatStatusImage()
|
||||
.padding(.top, 24)
|
||||
@@ -100,7 +99,7 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?, _ unread: Int) -> some View {
|
||||
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View {
|
||||
if let cItem = cItem {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
(itemStatusMark(cItem) + messageText(cItem.text, cItem.formattedText, cItem.memberDisplayName, preview: true))
|
||||
@@ -109,8 +108,9 @@ struct ChatPreviewView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .topLeading)
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, 36)
|
||||
if unread > 0 {
|
||||
unreadCountText(unread)
|
||||
let s = chat.chatStats
|
||||
if s.unreadCount > 0 || s.unreadChat {
|
||||
unreadCountText(s.unreadCount)
|
||||
.font(.caption)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 4)
|
||||
@@ -185,7 +185,7 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
|
||||
func unreadCountText(_ n: Int) -> Text {
|
||||
Text(n > 999 ? "\(n / 1000)k" : "\(n)")
|
||||
Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "")
|
||||
}
|
||||
|
||||
struct ChatPreviewView_Previews: PreviewProvider {
|
||||
|
||||
@@ -14,7 +14,6 @@ struct ContactConnectionInfo: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@State var contactConnection: PendingContactConnection
|
||||
@State private var alert: CCInfoAlert?
|
||||
@State private var showQRCodeView = false
|
||||
@State private var localAlias = ""
|
||||
@FocusState private var aliasTextFieldFocused: Bool
|
||||
|
||||
@@ -47,22 +46,27 @@ struct ContactConnectionInfo: View {
|
||||
.onTapGesture { aliasTextFieldFocused = false }
|
||||
|
||||
Section {
|
||||
HStack(spacing: 20) {
|
||||
Image(systemName: "pencil")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 6)
|
||||
.onTapGesture { aliasTextFieldFocused = true }
|
||||
TextField("Set contact name…", text: $localAlias)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled(true)
|
||||
.focused($aliasTextFieldFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit(setConnectionAlias)
|
||||
if contactConnection.groupLinkId == nil {
|
||||
HStack(spacing: 20) {
|
||||
Image(systemName: "pencil")
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.leading, 6)
|
||||
.onTapGesture { aliasTextFieldFocused = true }
|
||||
TextField("Set contact name…", text: $localAlias)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled(true)
|
||||
.focused($aliasTextFieldFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit(setConnectionAlias)
|
||||
}
|
||||
}
|
||||
|
||||
if contactConnection.initiated && contactConnection.connReqInv != nil {
|
||||
Button {
|
||||
showQRCodeView = true
|
||||
if contactConnection.initiated,
|
||||
let connReqInv = contactConnection.connReqInv {
|
||||
NavigationLink {
|
||||
AddContactView(contactConnection: contactConnection, connReqInvitation: connReqInv)
|
||||
.navigationTitle(CreateLinkTab.oneTime.title)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
Label("Show QR code", systemImage: "qrcode")
|
||||
.foregroundColor(contactConnection.incognito ? .indigo : .accentColor)
|
||||
@@ -79,11 +83,6 @@ struct ContactConnectionInfo: View {
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
.sheet(isPresented: $showQRCodeView) {
|
||||
if let connReqInv = contactConnection.connReqInv {
|
||||
AddContactView(contactConnection: contactConnection, connReqInvitation: connReqInv)
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { _alert in
|
||||
switch _alert {
|
||||
case .deleteInvitationAlert:
|
||||
@@ -126,7 +125,10 @@ struct ContactConnectionInfo: View {
|
||||
|
||||
private func contactConnectionText(_ contactConnection: PendingContactConnection) -> LocalizedStringKey {
|
||||
contactConnection.viaContactUri
|
||||
? "You will be connected when your connection request is accepted, please wait or check later!"
|
||||
? (contactConnection.groupLinkId != nil
|
||||
? "You will be connected to group when the group host's device is online, please wait or check later!"
|
||||
: "You will be connected when your connection request is accepted, please wait or check later!"
|
||||
)
|
||||
: "You will be connected when your contact's device is online, please wait or check later!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,15 +14,10 @@ struct AddContactView: View {
|
||||
@EnvironmentObject private var chatModel: ChatModel
|
||||
var contactConnection: PendingContactConnection? = nil
|
||||
var connReqInvitation: String
|
||||
var viaSettings = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("One-time invitation link")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(viaSettings ? .bottom : .vertical)
|
||||
Text("Your contact can scan it from the app.")
|
||||
.padding(.bottom, 4)
|
||||
if (contactConnection?.incognito ?? chatModel.incognito) {
|
||||
@@ -61,6 +56,7 @@ struct AddContactView: View {
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
.onAppear { chatModel.connReqInv = connReqInvitation }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,13 @@ import SwiftUI
|
||||
enum CreateLinkTab {
|
||||
case oneTime
|
||||
case longTerm
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .oneTime: return "One-time invitation link"
|
||||
case .longTerm: return "Your contact address"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateLinkView: View {
|
||||
@@ -18,11 +25,21 @@ struct CreateLinkView: View {
|
||||
@State var selection: CreateLinkTab
|
||||
@State var connReqInvitation: String = ""
|
||||
@State private var creatingConnReq = false
|
||||
var viaSettings = false
|
||||
var viaNavLink = false
|
||||
|
||||
var body: some View {
|
||||
if viaNavLink {
|
||||
createLinkView()
|
||||
} else {
|
||||
NavigationView {
|
||||
createLinkView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createLinkView() -> some View {
|
||||
TabView(selection: $selection) {
|
||||
AddContactView(connReqInvitation: connReqInvitation, viaSettings: viaSettings)
|
||||
AddContactView(connReqInvitation: connReqInvitation)
|
||||
.tabItem {
|
||||
Label(
|
||||
connReqInvitation == ""
|
||||
@@ -32,7 +49,7 @@ struct CreateLinkView: View {
|
||||
)
|
||||
}
|
||||
.tag(CreateLinkTab.oneTime)
|
||||
UserAddress(viaSettings: viaSettings)
|
||||
UserAddress()
|
||||
.tabItem {
|
||||
Label("Your contact address", systemImage: "infinity.circle")
|
||||
}
|
||||
@@ -45,6 +62,8 @@ struct CreateLinkView: View {
|
||||
}
|
||||
.onAppear { m.connReqInv = connReqInvitation }
|
||||
.onDisappear { m.connReqInv = nil }
|
||||
.navigationTitle(selection.title)
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
||||
private func createInvitation() {
|
||||
|
||||
@@ -78,6 +78,39 @@ func connectViaLink(_ connectionLink: String, _ dismiss: DismissAction? = nil) {
|
||||
}
|
||||
}
|
||||
|
||||
struct CReqClientData: Decodable {
|
||||
var type: String
|
||||
var groupLinkId: String?
|
||||
}
|
||||
|
||||
func parseLinkQueryData(_ connectionLink: String) -> CReqClientData? {
|
||||
if let hashIndex = connectionLink.firstIndex(of: "#"),
|
||||
let urlQuery = URL(string: String(connectionLink[connectionLink.index(after: hashIndex)...])),
|
||||
let components = URLComponents(url: urlQuery, resolvingAgainstBaseURL: false),
|
||||
let data = components.queryItems?.first(where: { $0.name == "data" })?.value,
|
||||
let d = data.data(using: .utf8),
|
||||
let crData = try? getJSONDecoder().decode(CReqClientData.self, from: d) {
|
||||
return crData
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkCRDataGroup(_ crData: CReqClientData) -> Bool {
|
||||
return crData.type == "group" && crData.groupLinkId != nil
|
||||
}
|
||||
|
||||
func groupLinkAlert(_ connectionLink: String) -> Alert {
|
||||
return Alert(
|
||||
title: Text("Connect via group link?"),
|
||||
message: Text("You will join a group this link refers to and connect to its group members."),
|
||||
primaryButton: .default(Text("Connect")) {
|
||||
connectViaLink(connectionLink)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
func connectionReqSentAlert(_ type: ConnReqType) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Connection request sent!",
|
||||
|
||||
@@ -81,7 +81,14 @@ struct PasteToConnectView: View {
|
||||
}
|
||||
|
||||
private func connect() {
|
||||
connectViaLink(connectionLink.trimmingCharacters(in: .whitespaces), dismiss)
|
||||
let link = connectionLink.trimmingCharacters(in: .whitespaces)
|
||||
if let crData = parseLinkQueryData(link),
|
||||
checkCRDataGroup(crData) {
|
||||
dismiss()
|
||||
AlertManager.shared.showAlert(groupLinkAlert(link))
|
||||
} else {
|
||||
connectViaLink(link, dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,13 @@ struct ScanToConnectView: View {
|
||||
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||
switch resp {
|
||||
case let .success(r):
|
||||
Task { connectViaLink(r.string, dismiss) }
|
||||
if let crData = parseLinkQueryData(r.string),
|
||||
checkCRDataGroup(crData) {
|
||||
dismiss()
|
||||
AlertManager.shared.showAlert(groupLinkAlert(r.string))
|
||||
} else {
|
||||
Task { connectViaLink(r.string, dismiss) }
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)")
|
||||
dismiss()
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
//
|
||||
// AcceptRequestsView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 23/10/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct AcceptRequestsView: View {
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@State var contactLink: UserContactLink
|
||||
@State private var a = AutoAcceptState()
|
||||
@State private var saved = AutoAcceptState()
|
||||
@FocusState private var keyboardVisible: Bool
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
settingsRow("checkmark") {
|
||||
Toggle("Automatically", isOn: $a.enable)
|
||||
}
|
||||
if a.enable {
|
||||
settingsRow(
|
||||
a.incognito ? "theatermasks.fill" : "theatermasks",
|
||||
color: a.incognito ? .indigo : .secondary
|
||||
) {
|
||||
Toggle("Incognito", isOn: $a.incognito)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Accept requests")
|
||||
} footer: {
|
||||
saveButtons()
|
||||
}
|
||||
if a.enable {
|
||||
Section {
|
||||
TextEditor(text: $a.welcomeText)
|
||||
.focused($keyboardVisible)
|
||||
.padding(.horizontal, -5)
|
||||
.padding(.top, -8)
|
||||
.frame(height: 90, alignment: .topLeading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} header: {
|
||||
Text("Welcome message")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
a = AutoAcceptState(contactLink: contactLink)
|
||||
saved = a
|
||||
}
|
||||
.onChange(of: a.enable) { _ in
|
||||
if !a.enable { a = AutoAcceptState() }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func saveButtons() -> some View {
|
||||
HStack {
|
||||
Button {
|
||||
a = saved
|
||||
} label: {
|
||||
Label("Cancel", systemImage: "arrow.counterclockwise")
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
if let link = try await userAddressAutoAccept(a.autoAccept) {
|
||||
contactLink = link
|
||||
m.userAddress = link
|
||||
saved = a
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("userAddressAutoAccept error: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Save", systemImage: "checkmark")
|
||||
}
|
||||
}
|
||||
.font(.body)
|
||||
.disabled(a == saved)
|
||||
}
|
||||
|
||||
private struct AutoAcceptState: Equatable {
|
||||
var enable = false
|
||||
var incognito = false
|
||||
var welcomeText = ""
|
||||
|
||||
init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") {
|
||||
self.enable = enable
|
||||
self.incognito = incognito
|
||||
self.welcomeText = welcomeText
|
||||
}
|
||||
|
||||
init(contactLink: UserContactLink) {
|
||||
if let aa = contactLink.autoAccept {
|
||||
enable = true
|
||||
incognito = aa.acceptIncognito
|
||||
if let msg = aa.autoReply {
|
||||
welcomeText = msg.text
|
||||
} else {
|
||||
welcomeText = ""
|
||||
}
|
||||
} else {
|
||||
enable = false
|
||||
incognito = false
|
||||
welcomeText = ""
|
||||
}
|
||||
}
|
||||
|
||||
var autoAccept: AutoAccept? {
|
||||
if enable {
|
||||
var autoReply: MsgContent? = nil
|
||||
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if s != "" { autoReply = .text(s) }
|
||||
return AutoAccept(acceptIncognito: incognito, autoReply: autoReply)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AcceptRequestsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AcceptRequestsView(contactLink: UserContactLink(connReqContact: ""))
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import SimpleXChat
|
||||
struct PrivacySettings: View {
|
||||
@AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true
|
||||
@AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE, store: groupDefaults) private var transferImagesInline = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -22,7 +24,14 @@ struct PrivacySettings: View {
|
||||
Section("Chats") {
|
||||
settingsRow("photo") {
|
||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||
.onChange(of: autoAcceptImages) { privacyAcceptImagesGroupDefault.set($0) }
|
||||
.onChange(of: autoAcceptImages) {
|
||||
privacyAcceptImagesGroupDefault.set($0)
|
||||
}
|
||||
}
|
||||
if developerTools {
|
||||
settingsRow("photo.on.rectangle") {
|
||||
Toggle("Transfer images faster (BETA)", isOn: $transferImagesInline)
|
||||
}
|
||||
}
|
||||
settingsRow("network") {
|
||||
Toggle("Send link previews", isOn: $useLinkPreviews)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
import SimpleXChat
|
||||
|
||||
let simplexTeamURL = URL(string: "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")!
|
||||
@@ -70,6 +71,7 @@ func setGroupDefaults() {
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var chatModel: ChatModel
|
||||
@EnvironmentObject var sceneDelegate: SceneDelegate
|
||||
@Binding var showSettings: Bool
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@State private var settingsSheet: SettingsSheet?
|
||||
@@ -93,7 +95,7 @@ struct SettingsView: View {
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
|
||||
NavigationLink {
|
||||
CreateLinkView(selection: .longTerm, viaSettings: true)
|
||||
CreateLinkView(selection: .longTerm, viaNavLink: true)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} label: {
|
||||
settingsRow("qrcode") { Text("Your SimpleX contact address") }
|
||||
@@ -179,36 +181,55 @@ struct SettingsView: View {
|
||||
settingsRow("textformat") { Text("Markdown in messages") }
|
||||
}
|
||||
settingsRow("number") {
|
||||
Button {
|
||||
Button("Send questions and ideas") {
|
||||
showSettings = false
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.open(simplexTeamURL)
|
||||
}
|
||||
} label: {
|
||||
Text("Chat with the developers")
|
||||
}
|
||||
}
|
||||
.disabled(chatModel.chatRunning != true)
|
||||
settingsRow("envelope") { Text("[Send us email](mailto:chat@simplex.chat)") }
|
||||
}
|
||||
|
||||
Section("Develop") {
|
||||
NavigationLink {
|
||||
TerminalView()
|
||||
} label: {
|
||||
settingsRow("terminal") { Text("Chat console") }
|
||||
}
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||
Toggle("Developer tools", isOn: $developerTools)
|
||||
Section("Support SimpleX Chat") {
|
||||
settingsRow("keyboard") { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") }
|
||||
settingsRow("star") {
|
||||
Button("Rate the app") {
|
||||
if let scene = sceneDelegate.windowScene {
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
}
|
||||
}
|
||||
}
|
||||
ZStack(alignment: .leading) {
|
||||
Image(colorScheme == .dark ? "github_light" : "github")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.opacity(0.5)
|
||||
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
|
||||
Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)")
|
||||
.padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Develop") {
|
||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||
Toggle("Developer tools", isOn: $developerTools)
|
||||
}
|
||||
if developerTools {
|
||||
NavigationLink {
|
||||
TerminalView()
|
||||
} label: {
|
||||
settingsRow("terminal") { Text("Chat console") }
|
||||
}
|
||||
ZStack(alignment: .leading) {
|
||||
Image(colorScheme == .dark ? "github_light" : "github")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.opacity(0.5)
|
||||
Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)")
|
||||
.padding(.leading, indent)
|
||||
}
|
||||
}
|
||||
// NavigationLink {
|
||||
// ExperimentalFeaturesView()
|
||||
// .navigationTitle("Experimental features")
|
||||
|
||||
@@ -12,7 +12,7 @@ import SimpleXChat
|
||||
struct UserAddress: View {
|
||||
@EnvironmentObject private var chatModel: ChatModel
|
||||
@State private var alert: UserAddressAlert?
|
||||
var viaSettings = false
|
||||
@State private var showAcceptRequests = false
|
||||
|
||||
private enum UserAddressAlert: Identifiable {
|
||||
case deleteAddress
|
||||
@@ -29,38 +29,49 @@ struct UserAddress: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack (alignment: .leading) {
|
||||
Text("Your contact address")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(viaSettings ? .bottom : .vertical)
|
||||
Text("You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.")
|
||||
.padding(.bottom)
|
||||
if let userAdress = chatModel.userAddress {
|
||||
QRCode(uri: userAdress)
|
||||
QRCode(uri: userAdress.connReqContact)
|
||||
HStack {
|
||||
Button {
|
||||
showShareSheet(items: [userAdress])
|
||||
showShareSheet(items: [userAdress.connReqContact])
|
||||
} label: {
|
||||
Label("Share link", systemImage: "square.and.arrow.up")
|
||||
HStack {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
Text("Share link")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
Button(role: .destructive) { alert = .deleteAddress } label: {
|
||||
Label("Delete address", systemImage: "trash")
|
||||
NavigationLink {
|
||||
if let contactLink = chatModel.userAddress {
|
||||
AcceptRequestsView(contactLink: contactLink)
|
||||
.navigationTitle("Contact requests")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Contact requests")
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
Button(role: .destructive) { alert = .deleteAddress } label: {
|
||||
Label("Delete address", systemImage: "trash")
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
Button {
|
||||
Task {
|
||||
do {
|
||||
let userAddress = try await apiCreateUserAddress()
|
||||
let connReqContact = try await apiCreateUserAddress()
|
||||
DispatchQueue.main.async {
|
||||
chatModel.userAddress = userAddress
|
||||
chatModel.userAddress = UserContactLink(connReqContact: connReqContact)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("UserAddress apiCreateUserAddress: \(error.localizedDescription)")
|
||||
logger.error("UserAddress apiCreateUserAddress: \(responseError(error))")
|
||||
let a = getErrorAlert(error, "Error creating address")
|
||||
alert = .error(title: a.title, error: "\(a.message)")
|
||||
}
|
||||
@@ -71,6 +82,11 @@ struct UserAddress: View {
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.sheet(isPresented: $showAcceptRequests) {
|
||||
if let contactLink = chatModel.userAddress {
|
||||
AcceptRequestsView(contactLink: contactLink)
|
||||
}
|
||||
}
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case .deleteAddress:
|
||||
@@ -85,7 +101,7 @@ struct UserAddress: View {
|
||||
chatModel.userAddress = nil
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("UserAddress apiDeleteUserAddress: \(error.localizedDescription)")
|
||||
logger.error("UserAddress apiDeleteUserAddress: \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}, secondaryButton: .cancel()
|
||||
@@ -101,7 +117,7 @@ struct UserAddress: View {
|
||||
struct UserAddress_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"
|
||||
chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D")
|
||||
return Group {
|
||||
UserAddress()
|
||||
.environmentObject(chatModel)
|
||||
|
||||
@@ -179,17 +179,17 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="1 day" xml:space="preserve">
|
||||
<source>1 day</source>
|
||||
<target>1 Tag</target>
|
||||
<target>täglich</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 month" xml:space="preserve">
|
||||
<source>1 month</source>
|
||||
<target>1 Monat</target>
|
||||
<target>monatlich</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="1 week" xml:space="preserve">
|
||||
<source>1 week</source>
|
||||
<target>1 Woche</target>
|
||||
<target>wöchentlich</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="6" xml:space="preserve">
|
||||
@@ -253,11 +253,21 @@
|
||||
<target>Inkognito akzeptieren</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept requests" xml:space="preserve">
|
||||
<source>Accept requests</source>
|
||||
<target>Anfragen annehmen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced network settings" xml:space="preserve">
|
||||
<source>Advanced network settings</source>
|
||||
<target>Erweiterte Netzwerkeinstellungen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All group members will remain connected." xml:space="preserve">
|
||||
<source>All group members will remain connected.</source>
|
||||
<target>Alle Gruppenmitglieder bleiben verbunden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
|
||||
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
|
||||
<target>Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.</target>
|
||||
@@ -313,6 +323,11 @@
|
||||
<target>Bilder automatisch akzeptieren</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Automatically" xml:space="preserve">
|
||||
<source>Automatically</source>
|
||||
<target>Automatisch</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Anruf ist bereits beendet!</target>
|
||||
@@ -323,11 +338,6 @@
|
||||
<target>Anrufe</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't delete contact!" xml:space="preserve">
|
||||
<source>Can't delete contact!</source>
|
||||
<target>Der Kontakt kann nicht gelöscht werden!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contact!" xml:space="preserve">
|
||||
<source>Can't invite contact!</source>
|
||||
<target>Kontakt kann nicht eingeladen werden!</target>
|
||||
@@ -368,6 +378,16 @@
|
||||
<target>Die Mitgliederrolle ändern?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
|
||||
<source>Change receiving address (BETA)</source>
|
||||
<target>Wechseln der Empfängeradresse (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address?" xml:space="preserve">
|
||||
<source>Change receiving address?</source>
|
||||
<target>Empfängeradresse wechseln?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change role" xml:space="preserve">
|
||||
<source>Change role</source>
|
||||
<target>Rolle ändern</target>
|
||||
@@ -408,11 +428,6 @@
|
||||
<target>Der Chat ist beendet</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat with the developers" xml:space="preserve">
|
||||
<source>Chat with the developers</source>
|
||||
<target>Chatten Sie mit den Entwicklern</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chats" xml:space="preserve">
|
||||
<source>Chats</source>
|
||||
<target>Chats</target>
|
||||
@@ -478,6 +493,11 @@
|
||||
<target>Über den Kontakt-Link verbinden?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via group link?" xml:space="preserve">
|
||||
<source>Connect via group link?</source>
|
||||
<target>Über den Gruppen-Link verbinden?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via link" xml:space="preserve">
|
||||
<source>Connect via link</source>
|
||||
<target>Über einen Link verbinden</target>
|
||||
@@ -538,11 +558,6 @@
|
||||
<target>Verbindungszeitüberschreitung</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact %@ cannot be deleted, they are a member of the group(s) %@." xml:space="preserve">
|
||||
<source>Contact %@ cannot be deleted, they are a member of the group(s) %@.</source>
|
||||
<target>Der Kontakt mit %@ kann nicht gelöscht werden, da er Mitglied einer oder mehrerer dieser Gruppen ist %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact already exists" xml:space="preserve">
|
||||
<source>Contact already exists</source>
|
||||
<target>Der Kontakt ist bereits vorhanden</target>
|
||||
@@ -573,6 +588,11 @@
|
||||
<target>Kontaktname</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact requests" xml:space="preserve">
|
||||
<source>Contact requests</source>
|
||||
<target>Kontaktanfragen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Kopieren</target>
|
||||
@@ -588,6 +608,11 @@
|
||||
<target>Adresse erstellen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create link" xml:space="preserve">
|
||||
<source>Create link</source>
|
||||
<target>Link erzeugen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Erstellen Sie einen einmaligen Einladungslink</target>
|
||||
@@ -801,6 +826,16 @@
|
||||
<target>Einladung löschen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link" xml:space="preserve">
|
||||
<source>Delete link</source>
|
||||
<target>Link löschen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link?" xml:space="preserve">
|
||||
<source>Delete link?</source>
|
||||
<target>Link löschen?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete message?" xml:space="preserve">
|
||||
<source>Delete message?</source>
|
||||
<target>Die Nachricht löschen?</target>
|
||||
@@ -813,7 +848,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete messages after" xml:space="preserve">
|
||||
<source>Delete messages after</source>
|
||||
<target>Löschen der Nachrichten nach</target>
|
||||
<target>Löschen der Nachrichten</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete old database" xml:space="preserve">
|
||||
@@ -996,6 +1031,11 @@
|
||||
<target>Fehler beim Hinzufügen von Mitgliedern</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>Fehler beim Wechseln der Adresse</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Fehler beim Ändern der Rolle</target>
|
||||
@@ -1016,6 +1056,11 @@
|
||||
<target>Fehler beim Erzeugen der Gruppe</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating group link" xml:space="preserve">
|
||||
<source>Error creating group link</source>
|
||||
<target>Fehler beim Erzeugen des Gruppen-Links</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
<source>Error deleting chat database</source>
|
||||
<target>Fehler beim Löschen der Chat-Datenbank</target>
|
||||
@@ -1031,6 +1076,11 @@
|
||||
<target>Fehler beim Löschen der Verbindung</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting contact" xml:space="preserve">
|
||||
<source>Error deleting contact</source>
|
||||
<target>Fehler beim Löschen des Kontakts</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting database" xml:space="preserve">
|
||||
<source>Error deleting database</source>
|
||||
<target>Fehler beim Löschen der Datenbank</target>
|
||||
@@ -1158,7 +1208,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
<source>File will be received when your contact is online, please wait or check later!</source>
|
||||
<target>Die Datei wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<target>Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="File: %@" xml:space="preserve">
|
||||
@@ -1166,11 +1216,6 @@
|
||||
<target>Datei: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files" xml:space="preserve">
|
||||
<source>Files</source>
|
||||
<target>Dateien</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="For console" xml:space="preserve">
|
||||
<source>For console</source>
|
||||
<target>Für Konsole</target>
|
||||
@@ -1221,6 +1266,11 @@
|
||||
<target>Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group link" xml:space="preserve">
|
||||
<source>Group link</source>
|
||||
<target>Gruppen-Link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
<source>Group message:</source>
|
||||
<target>Grppennachricht:</target>
|
||||
@@ -1308,7 +1358,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve">
|
||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||
<target>Das Bild wird empfangen, sobald Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<target>Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
||||
@@ -1735,7 +1785,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
</trans-unit>
|
||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
||||
<target>Open-Source-Protokoll und -Code – Jeder kann seine eigenen Server aufsetzen und nutzen.</target>
|
||||
<target>Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="PING interval" xml:space="preserve">
|
||||
@@ -1790,7 +1840,7 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
</trans-unit>
|
||||
<trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve">
|
||||
<source>Please enter the previous password after restoring database backup. This action can not be undone.</source>
|
||||
<target>Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.</target>
|
||||
<target>Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve">
|
||||
@@ -1833,6 +1883,11 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
<target>Push-Benachrichtigungen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Rate the app" xml:space="preserve">
|
||||
<source>Rate the app</source>
|
||||
<target>Bewerte die App</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Read" xml:space="preserve">
|
||||
<source>Read</source>
|
||||
<target>Lesen</target>
|
||||
@@ -2048,6 +2103,11 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
<target>Benachrichtigungen senden:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send questions and ideas" xml:space="preserve">
|
||||
<source>Send questions and ideas</source>
|
||||
<target>Senden Sie Fragen und Ideen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
|
||||
<source>Sender cancelled file transfer.</source>
|
||||
<target>Der Absender hat die Dateiübertragung abgebrochen.</target>
|
||||
@@ -2113,11 +2173,6 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
<target>Einmal-Einladungslink teilen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Shared one-time link" xml:space="preserve">
|
||||
<source>Shared one-time link</source>
|
||||
<target>Geteilter Einmal-Link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<target>QR-Code anzeigen</target>
|
||||
@@ -2198,6 +2253,11 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
<target>Chat beenden?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Support SimpleX Chat" xml:space="preserve">
|
||||
<source>Support SimpleX Chat</source>
|
||||
<target>Unterstützen SimpleX Chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="System" xml:space="preserve">
|
||||
<source>System</source>
|
||||
<target>System</target>
|
||||
@@ -2320,17 +2380,22 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve">
|
||||
<source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source>
|
||||
<target>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.</target>
|
||||
<target>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.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve">
|
||||
<source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source>
|
||||
<target>Diese Aktion kann nicht rückgängig gemacht werden - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, werden gelöscht. Dies kann mehrere Minuten dauern.</target>
|
||||
<target>Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve">
|
||||
<source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source>
|
||||
<target>Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</target>
|
||||
<target>Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve">
|
||||
<source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source>
|
||||
<target>Diese Funktion ist experimentell! Sie wird nur funktionieren, wenn der Kontakt ebenfalls die SimpleX-Version 4.2 installiert hat. Sobald der Adress-Wechsel abgeschlossen ist, sollten Sie die Nachricht im Chat-Verlauf sehen. Bitte prüfen Sie, ob Sie weiterhin Nachrichten von diesem Kontakt (oder Gruppenmitglied) empfangen können.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This group no longer exists." xml:space="preserve">
|
||||
@@ -2375,6 +2440,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt
|
||||
<target>Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
|
||||
<source>Transfer images faster (BETA)</source>
|
||||
<target>Bilder schneller übertragen (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve">
|
||||
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
|
||||
<target>Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@).</target>
|
||||
@@ -2437,6 +2507,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Stummschaltung aufheben</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unread" xml:space="preserve">
|
||||
<source>Unread</source>
|
||||
<target>Ungelesen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update" xml:space="preserve">
|
||||
<source>Update</source>
|
||||
<target>Aktualisieren</target>
|
||||
@@ -2517,6 +2592,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Willkommen %@!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message" xml:space="preserve">
|
||||
<source>Welcome message</source>
|
||||
<target>Begrüßungsmeldung</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When available" xml:space="preserve">
|
||||
<source>When available</source>
|
||||
<target>Wenn verfügbar</target>
|
||||
@@ -2547,9 +2627,9 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Sie haben die Verbindung akzeptiert</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are already connected to %@ via this link." xml:space="preserve">
|
||||
<source>You are already connected to %@ via this link.</source>
|
||||
<target>Sie sind bereits über diesen Link mit %@ verbunden.</target>
|
||||
<trans-unit id="You are already connected to %@." xml:space="preserve">
|
||||
<source>You are already connected to %@.</source>
|
||||
<target>Sie sind bereits mit %@ verbunden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
|
||||
@@ -2577,9 +2657,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve">
|
||||
<source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source>
|
||||
<target>Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve">
|
||||
<source>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</source>
|
||||
<target>Sie können Ihre Adresse als Link oder als QR-Code teilen – Jeder kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</target>
|
||||
<target>Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve">
|
||||
@@ -2642,14 +2727,19 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Sie haben eine Gruppeneinladung gesendet</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected to group when the group host's device is online, please wait or check later!</source>
|
||||
<target>Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<target>Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your contact's device is online, please wait or check later!</source>
|
||||
<target>Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<target>Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve">
|
||||
@@ -2657,6 +2747,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s
|
||||
<target>Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
|
||||
<source>You will join a group this link refers to and connect to its group members.</source>
|
||||
<target>Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht und sich mit deren Gruppenmitgliedern verbinden.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve">
|
||||
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
|
||||
<target>Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.</target>
|
||||
@@ -2781,11 +2876,21 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Meine Einstellungen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve">
|
||||
<source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source>
|
||||
<target>[Beitragen](https://github.com/simplex-chat/simplex-chat#contribute)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve">
|
||||
<source>[Send us email](mailto:chat@simplex.chat)</source>
|
||||
<target>[Senden Sie uns eine E-Mail](mailto:chat@simplex.chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve">
|
||||
<source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source>
|
||||
<target>[Stern auf GitHub](https://github.com/simplex-chat/simplex-chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="_italic_" xml:space="preserve">
|
||||
<source>\_italic_</source>
|
||||
<target>\_kursiv_</target>
|
||||
@@ -2846,6 +2951,31 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Anrufen…</target>
|
||||
<note>call status</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed address for you" xml:space="preserve">
|
||||
<source>changed address for you</source>
|
||||
<target>wechselte die Adresse für Sie</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
|
||||
<source>changed role of %1$@ to %2$@</source>
|
||||
<target>änderte die Rolle von %1$@ auf %2$@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed your role to %@" xml:space="preserve">
|
||||
<source>changed your role to %@</source>
|
||||
<target>änderte Ihre Rolle auf %@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address for %@..." xml:space="preserve">
|
||||
<source>changing address for %@...</source>
|
||||
<target>Wechseln der Adresse für %@ ...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address..." xml:space="preserve">
|
||||
<source>changing address...</source>
|
||||
<target>Wechseln der Adresse ...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="colored" xml:space="preserve">
|
||||
<source>colored</source>
|
||||
<target>farbig</target>
|
||||
@@ -2988,7 +3118,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via contact address link" xml:space="preserve">
|
||||
<source>incognito via contact address link</source>
|
||||
<target>Inkognito über einen Kontaktadressen Link</target>
|
||||
<target>Inkognito über einen Kontaktadressen-Link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via group link" xml:space="preserve">
|
||||
<source>incognito via group link</source>
|
||||
<target>Inkognito über einen Gruppen-Link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via one-time link" xml:space="preserve">
|
||||
@@ -2998,7 +3133,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="indirect (%d)" xml:space="preserve">
|
||||
<source>indirect (%d)</source>
|
||||
<target>Indirekt (%d)</target>
|
||||
<target>indirekt (%d)</target>
|
||||
<note>connection level description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invitation to group %@" xml:space="preserve">
|
||||
@@ -3013,7 +3148,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited %@" xml:space="preserve">
|
||||
<source>invited %@</source>
|
||||
<target>Sie haben %@ eingeladen</target>
|
||||
<target>hat %@ eingeladen.</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited to connect" xml:space="preserve">
|
||||
@@ -3021,6 +3156,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Für eine Verbindung eingeladen</target>
|
||||
<note>chat list item title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited via your group link" xml:space="preserve">
|
||||
<source>invited via your group link</source>
|
||||
<target>wurde über Ihren Gruppen-Link eingeladen</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="italic" xml:space="preserve">
|
||||
<source>italic</source>
|
||||
<target>kursiv</target>
|
||||
@@ -3033,7 +3173,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="left" xml:space="preserve">
|
||||
<source>left</source>
|
||||
<target>verlassen</target>
|
||||
<target>hat die Gruppe verlassen</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="member" xml:space="preserve">
|
||||
@@ -3041,15 +3181,9 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Mitglied</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="member %@ role: %@" xml:space="preserve">
|
||||
<source>member %1$@ role: %2$@</source>
|
||||
<target>Mitglieder %1$@ Rolle: %2$@</target>
|
||||
<note>rcv group event chat item
|
||||
snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="member connected" xml:space="preserve">
|
||||
<source>connected</source>
|
||||
<target>verbunden</target>
|
||||
<target>ist der Gruppe beigetreten</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="message received" xml:space="preserve">
|
||||
@@ -3114,12 +3248,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="removed %@" xml:space="preserve">
|
||||
<source>removed %@</source>
|
||||
<target>Entfernt %@</target>
|
||||
<target>hat %@ aus der Gruppe entfernt</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="removed you" xml:space="preserve">
|
||||
<source>removed you</source>
|
||||
<target>hat Sie entfernt</target>
|
||||
<target>hat Sie aus der Gruppe entfernt</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="sec" xml:space="preserve">
|
||||
@@ -3164,7 +3298,12 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="via contact address link" xml:space="preserve">
|
||||
<source>via contact address link</source>
|
||||
<target>über einen Kontaktadressen Link</target>
|
||||
<target>über einen Kontaktadressen-Link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via group link" xml:space="preserve">
|
||||
<source>via group link</source>
|
||||
<target>über einen Gruppen-Link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via one-time link" xml:space="preserve">
|
||||
@@ -3202,6 +3341,26 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Sie sind zur Gruppe eingeladen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address" xml:space="preserve">
|
||||
<source>you changed address</source>
|
||||
<target>Sie haben die Adresse gewechselt</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address for %@" xml:space="preserve">
|
||||
<source>you changed address for %@</source>
|
||||
<target>Sie haben die Adresse für %@ gewechselt</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
|
||||
<source>you changed role for yourself to %@</source>
|
||||
<target>Sie haben Ihre eigene Rolle auf %@ geändert</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role of %@ to %@" xml:space="preserve">
|
||||
<source>you changed role of %1$@ to %2$@</source>
|
||||
<target>Sie haben die Rolle von %1$@ auf %2$@ geändert</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you left" xml:space="preserve">
|
||||
<source>you left</source>
|
||||
<target>Sie haben verlassen</target>
|
||||
@@ -3209,7 +3368,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="you removed %@" xml:space="preserve">
|
||||
<source>you removed %@</source>
|
||||
<target>Sie haben %@ entfernt</target>
|
||||
<target>entfernt %@ aus der Gruppe</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you shared one-time link" xml:space="preserve">
|
||||
@@ -3227,12 +3386,6 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target>
|
||||
<target>Sie: </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="your role: %@" xml:space="preserve">
|
||||
<source>your role: %@</source>
|
||||
<target>Meine Rolle: %@</target>
|
||||
<note>rcv group event chat item
|
||||
snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="~strike~" xml:space="preserve">
|
||||
<source>\~strike~</source>
|
||||
<target>\~durchstreichen~</target>
|
||||
|
||||
@@ -253,11 +253,21 @@
|
||||
<target>Accept incognito</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept requests" xml:space="preserve">
|
||||
<source>Accept requests</source>
|
||||
<target>Accept requests</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced network settings" xml:space="preserve">
|
||||
<source>Advanced network settings</source>
|
||||
<target>Advanced network settings</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All group members will remain connected." xml:space="preserve">
|
||||
<source>All group members will remain connected.</source>
|
||||
<target>All group members will remain connected.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
|
||||
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
|
||||
<target>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</target>
|
||||
@@ -313,6 +323,11 @@
|
||||
<target>Auto-accept images</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Automatically" xml:space="preserve">
|
||||
<source>Automatically</source>
|
||||
<target>Automatically</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Call already ended!</target>
|
||||
@@ -323,11 +338,6 @@
|
||||
<target>Calls</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't delete contact!" xml:space="preserve">
|
||||
<source>Can't delete contact!</source>
|
||||
<target>Can't delete contact!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contact!" xml:space="preserve">
|
||||
<source>Can't invite contact!</source>
|
||||
<target>Can't invite contact!</target>
|
||||
@@ -368,6 +378,16 @@
|
||||
<target>Change member role?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
|
||||
<source>Change receiving address (BETA)</source>
|
||||
<target>Change receiving address (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address?" xml:space="preserve">
|
||||
<source>Change receiving address?</source>
|
||||
<target>Change receiving address?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change role" xml:space="preserve">
|
||||
<source>Change role</source>
|
||||
<target>Change role</target>
|
||||
@@ -408,11 +428,6 @@
|
||||
<target>Chat is stopped</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat with the developers" xml:space="preserve">
|
||||
<source>Chat with the developers</source>
|
||||
<target>Chat with the developers</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chats" xml:space="preserve">
|
||||
<source>Chats</source>
|
||||
<target>Chats</target>
|
||||
@@ -478,6 +493,11 @@
|
||||
<target>Connect via contact link?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via group link?" xml:space="preserve">
|
||||
<source>Connect via group link?</source>
|
||||
<target>Connect via group link?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via link" xml:space="preserve">
|
||||
<source>Connect via link</source>
|
||||
<target>Connect via link</target>
|
||||
@@ -538,11 +558,6 @@
|
||||
<target>Connection timeout</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact %@ cannot be deleted, they are a member of the group(s) %@." xml:space="preserve">
|
||||
<source>Contact %@ cannot be deleted, they are a member of the group(s) %@.</source>
|
||||
<target>Contact %@ cannot be deleted, they are a member of the group(s) %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact already exists" xml:space="preserve">
|
||||
<source>Contact already exists</source>
|
||||
<target>Contact already exists</target>
|
||||
@@ -573,6 +588,11 @@
|
||||
<target>Contact name</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact requests" xml:space="preserve">
|
||||
<source>Contact requests</source>
|
||||
<target>Contact requests</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Copy</target>
|
||||
@@ -588,6 +608,11 @@
|
||||
<target>Create address</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create link" xml:space="preserve">
|
||||
<source>Create link</source>
|
||||
<target>Create link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Create one-time invitation link</target>
|
||||
@@ -801,6 +826,16 @@
|
||||
<target>Delete invitation</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link" xml:space="preserve">
|
||||
<source>Delete link</source>
|
||||
<target>Delete link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link?" xml:space="preserve">
|
||||
<source>Delete link?</source>
|
||||
<target>Delete link?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete message?" xml:space="preserve">
|
||||
<source>Delete message?</source>
|
||||
<target>Delete message?</target>
|
||||
@@ -996,6 +1031,11 @@
|
||||
<target>Error adding member(s)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>Error changing address</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Error changing role</target>
|
||||
@@ -1016,6 +1056,11 @@
|
||||
<target>Error creating group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating group link" xml:space="preserve">
|
||||
<source>Error creating group link</source>
|
||||
<target>Error creating group link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
<source>Error deleting chat database</source>
|
||||
<target>Error deleting chat database</target>
|
||||
@@ -1031,6 +1076,11 @@
|
||||
<target>Error deleting connection</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting contact" xml:space="preserve">
|
||||
<source>Error deleting contact</source>
|
||||
<target>Error deleting contact</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting database" xml:space="preserve">
|
||||
<source>Error deleting database</source>
|
||||
<target>Error deleting database</target>
|
||||
@@ -1166,11 +1216,6 @@
|
||||
<target>File: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files" xml:space="preserve">
|
||||
<source>Files</source>
|
||||
<target>Files</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="For console" xml:space="preserve">
|
||||
<source>For console</source>
|
||||
<target>For console</target>
|
||||
@@ -1221,6 +1266,11 @@
|
||||
<target>Group invitation is no longer valid, it was removed by sender.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group link" xml:space="preserve">
|
||||
<source>Group link</source>
|
||||
<target>Group link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
<source>Group message:</source>
|
||||
<target>Group message:</target>
|
||||
@@ -1833,6 +1883,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Push notifications</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Rate the app" xml:space="preserve">
|
||||
<source>Rate the app</source>
|
||||
<target>Rate the app</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Read" xml:space="preserve">
|
||||
<source>Read</source>
|
||||
<target>Read</target>
|
||||
@@ -2048,6 +2103,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Send notifications:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send questions and ideas" xml:space="preserve">
|
||||
<source>Send questions and ideas</source>
|
||||
<target>Send questions and ideas</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
|
||||
<source>Sender cancelled file transfer.</source>
|
||||
<target>Sender cancelled file transfer.</target>
|
||||
@@ -2113,11 +2173,6 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Share one-time invitation link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Shared one-time link" xml:space="preserve">
|
||||
<source>Shared one-time link</source>
|
||||
<target>Shared one-time link</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<target>Show QR code</target>
|
||||
@@ -2198,6 +2253,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Stop chat?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Support SimpleX Chat" xml:space="preserve">
|
||||
<source>Support SimpleX Chat</source>
|
||||
<target>Support SimpleX Chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="System" xml:space="preserve">
|
||||
<source>System</source>
|
||||
<target>System</target>
|
||||
@@ -2333,6 +2393,11 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve">
|
||||
<source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source>
|
||||
<target>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This group no longer exists." xml:space="preserve">
|
||||
<source>This group no longer exists.</source>
|
||||
<target>This group no longer exists.</target>
|
||||
@@ -2375,6 +2440,11 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>To support instant push notifications the chat database has to be migrated.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
|
||||
<source>Transfer images faster (BETA)</source>
|
||||
<target>Transfer images faster (BETA)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve">
|
||||
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
|
||||
<target>Trying to connect to the server used to receive messages from this contact (error: %@).</target>
|
||||
@@ -2437,6 +2507,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Unmute</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unread" xml:space="preserve">
|
||||
<source>Unread</source>
|
||||
<target>Unread</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update" xml:space="preserve">
|
||||
<source>Update</source>
|
||||
<target>Update</target>
|
||||
@@ -2517,6 +2592,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Welcome %@!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message" xml:space="preserve">
|
||||
<source>Welcome message</source>
|
||||
<target>Welcome message</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When available" xml:space="preserve">
|
||||
<source>When available</source>
|
||||
<target>When available</target>
|
||||
@@ -2547,9 +2627,9 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You accepted connection</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are already connected to %@ via this link." xml:space="preserve">
|
||||
<source>You are already connected to %@ via this link.</source>
|
||||
<target>You are already connected to %@ via this link.</target>
|
||||
<trans-unit id="You are already connected to %@." xml:space="preserve">
|
||||
<source>You are already connected to %@.</source>
|
||||
<target>You are already connected to %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
|
||||
@@ -2577,6 +2657,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You can set lock screen notification preview via settings.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve">
|
||||
<source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source>
|
||||
<target>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve">
|
||||
<source>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</source>
|
||||
<target>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</target>
|
||||
@@ -2642,6 +2727,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You sent group invitation</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected to group when the group host's device is online, please wait or check later!</source>
|
||||
<target>You will be connected to group when the group host's device is online, please wait or check later!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>You will be connected when your connection request is accepted, please wait or check later!</target>
|
||||
@@ -2657,6 +2747,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>You will be required to authenticate when you start or resume the app after 30 seconds in background.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
|
||||
<source>You will join a group this link refers to and connect to its group members.</source>
|
||||
<target>You will join a group this link refers to and connect to its group members.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve">
|
||||
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
|
||||
<target>You will stop receiving messages from this group. Chat history will be preserved.</target>
|
||||
@@ -2781,11 +2876,21 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>Your settings</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve">
|
||||
<source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source>
|
||||
<target>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve">
|
||||
<source>[Send us email](mailto:chat@simplex.chat)</source>
|
||||
<target>[Send us email](mailto:chat@simplex.chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve">
|
||||
<source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source>
|
||||
<target>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="_italic_" xml:space="preserve">
|
||||
<source>\_italic_</source>
|
||||
<target>\_italic_</target>
|
||||
@@ -2846,6 +2951,31 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>calling…</target>
|
||||
<note>call status</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed address for you" xml:space="preserve">
|
||||
<source>changed address for you</source>
|
||||
<target>changed address for you</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
|
||||
<source>changed role of %1$@ to %2$@</source>
|
||||
<target>changed role of %1$@ to %2$@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed your role to %@" xml:space="preserve">
|
||||
<source>changed your role to %@</source>
|
||||
<target>changed your role to %@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address for %@..." xml:space="preserve">
|
||||
<source>changing address for %@...</source>
|
||||
<target>changing address for %@...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address..." xml:space="preserve">
|
||||
<source>changing address...</source>
|
||||
<target>changing address...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="colored" xml:space="preserve">
|
||||
<source>colored</source>
|
||||
<target>colored</target>
|
||||
@@ -2991,6 +3121,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>incognito via contact address link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via group link" xml:space="preserve">
|
||||
<source>incognito via group link</source>
|
||||
<target>incognito via group link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via one-time link" xml:space="preserve">
|
||||
<source>incognito via one-time link</source>
|
||||
<target>incognito via one-time link</target>
|
||||
@@ -3021,6 +3156,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>invited to connect</target>
|
||||
<note>chat list item title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited via your group link" xml:space="preserve">
|
||||
<source>invited via your group link</source>
|
||||
<target>invited via your group link</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="italic" xml:space="preserve">
|
||||
<source>italic</source>
|
||||
<target>italic</target>
|
||||
@@ -3041,12 +3181,6 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>member</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="member %@ role: %@" xml:space="preserve">
|
||||
<source>member %1$@ role: %2$@</source>
|
||||
<target>member %1$@ role: %2$@</target>
|
||||
<note>rcv group event chat item
|
||||
snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="member connected" xml:space="preserve">
|
||||
<source>connected</source>
|
||||
<target>connected</target>
|
||||
@@ -3167,6 +3301,11 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>via contact address link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via group link" xml:space="preserve">
|
||||
<source>via group link</source>
|
||||
<target>via group link</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via one-time link" xml:space="preserve">
|
||||
<source>via one-time link</source>
|
||||
<target>via one-time link</target>
|
||||
@@ -3202,6 +3341,26 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>you are invited to group</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address" xml:space="preserve">
|
||||
<source>you changed address</source>
|
||||
<target>you changed address</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address for %@" xml:space="preserve">
|
||||
<source>you changed address for %@</source>
|
||||
<target>you changed address for %@</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
|
||||
<source>you changed role for yourself to %@</source>
|
||||
<target>you changed role for yourself to %@</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role of %@ to %@" xml:space="preserve">
|
||||
<source>you changed role of %1$@ to %2$@</source>
|
||||
<target>you changed role of %1$@ to %2$@</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you left" xml:space="preserve">
|
||||
<source>you left</source>
|
||||
<target>you left</target>
|
||||
@@ -3227,12 +3386,6 @@ SimpleX servers cannot see your profile.</target>
|
||||
<target>you: </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="your role: %@" xml:space="preserve">
|
||||
<source>your role: %@</source>
|
||||
<target>your role: %@</target>
|
||||
<note>rcv group event chat item
|
||||
snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="~strike~" xml:space="preserve">
|
||||
<source>\~strike~</source>
|
||||
<target>\~strike~</target>
|
||||
|
||||
@@ -253,11 +253,21 @@
|
||||
<target>Принять инкогнито</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Accept requests" xml:space="preserve">
|
||||
<source>Accept requests</source>
|
||||
<target>Принимать запросы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advanced network settings" xml:space="preserve">
|
||||
<source>Advanced network settings</source>
|
||||
<target>Настройки сети</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All group members will remain connected." xml:space="preserve">
|
||||
<source>All group members will remain connected.</source>
|
||||
<target>Все члены группы, которые соединились через эту ссылку, останутся в группе.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve">
|
||||
<source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source>
|
||||
<target>Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для вас.</target>
|
||||
@@ -313,6 +323,11 @@
|
||||
<target>Автоприем изображений</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Automatically" xml:space="preserve">
|
||||
<source>Automatically</source>
|
||||
<target>Автоматически</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Call already ended!" xml:space="preserve">
|
||||
<source>Call already ended!</source>
|
||||
<target>Звонок уже завершен!</target>
|
||||
@@ -323,11 +338,6 @@
|
||||
<target>Звонки</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't delete contact!" xml:space="preserve">
|
||||
<source>Can't delete contact!</source>
|
||||
<target>Невозможно удалить контакт!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Can't invite contact!" xml:space="preserve">
|
||||
<source>Can't invite contact!</source>
|
||||
<target>Нельзя пригласить контакт!</target>
|
||||
@@ -368,6 +378,16 @@
|
||||
<target>Поменять роль члена группы?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address (BETA)" xml:space="preserve">
|
||||
<source>Change receiving address (BETA)</source>
|
||||
<target>Поменять адрес получения (БЕТА)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change receiving address?" xml:space="preserve">
|
||||
<source>Change receiving address?</source>
|
||||
<target>Поменять адрес получения?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change role" xml:space="preserve">
|
||||
<source>Change role</source>
|
||||
<target>Поменять роль</target>
|
||||
@@ -408,11 +428,6 @@
|
||||
<target>Чат остановлен</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chat with the developers" xml:space="preserve">
|
||||
<source>Chat with the developers</source>
|
||||
<target>Соединиться с разработчиками</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Chats" xml:space="preserve">
|
||||
<source>Chats</source>
|
||||
<target>Чаты</target>
|
||||
@@ -478,6 +493,11 @@
|
||||
<target>Соединиться через ссылку-контакт?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via group link?" xml:space="preserve">
|
||||
<source>Connect via group link?</source>
|
||||
<target>Соединиться через ссылку группы?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect via link" xml:space="preserve">
|
||||
<source>Connect via link</source>
|
||||
<target>Соединиться через ссылку</target>
|
||||
@@ -538,11 +558,6 @@
|
||||
<target>Превышено время соединения</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact %@ cannot be deleted, they are a member of the group(s) %@." xml:space="preserve">
|
||||
<source>Contact %@ cannot be deleted, they are a member of the group(s) %@.</source>
|
||||
<target>Контакт %@ не может быть удален, так как является членом групп(ы) %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact already exists" xml:space="preserve">
|
||||
<source>Contact already exists</source>
|
||||
<target>Существующий контакт</target>
|
||||
@@ -573,6 +588,11 @@
|
||||
<target>Имена контактов</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact requests" xml:space="preserve">
|
||||
<source>Contact requests</source>
|
||||
<target>Запросы контактов</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy" xml:space="preserve">
|
||||
<source>Copy</source>
|
||||
<target>Скопировать</target>
|
||||
@@ -588,6 +608,11 @@
|
||||
<target>Создать адрес</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create link" xml:space="preserve">
|
||||
<source>Create link</source>
|
||||
<target>Создать ссылку</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create one-time invitation link" xml:space="preserve">
|
||||
<source>Create one-time invitation link</source>
|
||||
<target>Создать ссылку-приглашение</target>
|
||||
@@ -801,6 +826,16 @@
|
||||
<target>Удалить приглашение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link" xml:space="preserve">
|
||||
<source>Delete link</source>
|
||||
<target>Удалить ссылку</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete link?" xml:space="preserve">
|
||||
<source>Delete link?</source>
|
||||
<target>Удалить ссылку?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Delete message?" xml:space="preserve">
|
||||
<source>Delete message?</source>
|
||||
<target>Удалить сообщение?</target>
|
||||
@@ -996,6 +1031,11 @@
|
||||
<target>Ошибка при добавлении членов группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing address" xml:space="preserve">
|
||||
<source>Error changing address</source>
|
||||
<target>Ошибка при изменении адреса</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error changing role" xml:space="preserve">
|
||||
<source>Error changing role</source>
|
||||
<target>Ошибка при изменении роли</target>
|
||||
@@ -1016,6 +1056,11 @@
|
||||
<target>Ошибка при создании группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error creating group link" xml:space="preserve">
|
||||
<source>Error creating group link</source>
|
||||
<target>Ошибка при создании ссылки группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting chat database" xml:space="preserve">
|
||||
<source>Error deleting chat database</source>
|
||||
<target>Ошибка при удалении данных чата</target>
|
||||
@@ -1031,6 +1076,11 @@
|
||||
<target>Ошибка при удалении соединения</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting contact" xml:space="preserve">
|
||||
<source>Error deleting contact</source>
|
||||
<target>Ошибка при удалении контакта</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error deleting database" xml:space="preserve">
|
||||
<source>Error deleting database</source>
|
||||
<target>Ошибка при удалении данных чата</target>
|
||||
@@ -1166,11 +1216,6 @@
|
||||
<target>Файл: %@</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Files" xml:space="preserve">
|
||||
<source>Files</source>
|
||||
<target>Файлы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="For console" xml:space="preserve">
|
||||
<source>For console</source>
|
||||
<target>Для консоли</target>
|
||||
@@ -1221,6 +1266,11 @@
|
||||
<target>Приглашение в группу больше не действительно, оно было удалено отправителем.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group link" xml:space="preserve">
|
||||
<source>Group link</source>
|
||||
<target>Ссылка группы</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Group message:" xml:space="preserve">
|
||||
<source>Group message:</source>
|
||||
<target>Групповое сообщение:</target>
|
||||
@@ -1833,6 +1883,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Доставка уведомлений</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Rate the app" xml:space="preserve">
|
||||
<source>Rate the app</source>
|
||||
<target>Оценить приложение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Read" xml:space="preserve">
|
||||
<source>Read</source>
|
||||
<target>Прочитано</target>
|
||||
@@ -2048,6 +2103,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Отправлять уведомления:</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send questions and ideas" xml:space="preserve">
|
||||
<source>Send questions and ideas</source>
|
||||
<target>Отправьте вопросы и идеи</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sender cancelled file transfer." xml:space="preserve">
|
||||
<source>Sender cancelled file transfer.</source>
|
||||
<target>Отправитель отменил передачу файла.</target>
|
||||
@@ -2113,11 +2173,6 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Поделиться ссылкой-приглашением</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Shared one-time link" xml:space="preserve">
|
||||
<source>Shared one-time link</source>
|
||||
<target>Одноразовая ссылка-приглашение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show QR code" xml:space="preserve">
|
||||
<source>Show QR code</source>
|
||||
<target>Показать QR код</target>
|
||||
@@ -2198,6 +2253,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Остановить чат?</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Support SimpleX Chat" xml:space="preserve">
|
||||
<source>Support SimpleX Chat</source>
|
||||
<target>Поддержать SimpleX Chat</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="System" xml:space="preserve">
|
||||
<source>System</source>
|
||||
<target>Системная</target>
|
||||
@@ -2333,6 +2393,11 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Это действие нельзя отменить — ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve">
|
||||
<source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source>
|
||||
<target>Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This group no longer exists." xml:space="preserve">
|
||||
<source>This group no longer exists.</source>
|
||||
<target>Эта группа больше не существует.</target>
|
||||
@@ -2375,6 +2440,11 @@ You will be prompted to complete authentication before this feature is enabled.<
|
||||
<target>Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Transfer images faster (BETA)" xml:space="preserve">
|
||||
<source>Transfer images faster (BETA)</source>
|
||||
<target>Передавать изображения быстрее (БЕТА)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve">
|
||||
<source>Trying to connect to the server used to receive messages from this contact (error: %@).</source>
|
||||
<target>Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@).</target>
|
||||
@@ -2437,6 +2507,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Уведомлять</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unread" xml:space="preserve">
|
||||
<source>Unread</source>
|
||||
<target>Не прочитано</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Update" xml:space="preserve">
|
||||
<source>Update</source>
|
||||
<target>Обновить</target>
|
||||
@@ -2517,6 +2592,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Здравствуйте %@!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Welcome message" xml:space="preserve">
|
||||
<source>Welcome message</source>
|
||||
<target>Приветственное сообщение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="When available" xml:space="preserve">
|
||||
<source>When available</source>
|
||||
<target>Когда возможно</target>
|
||||
@@ -2547,9 +2627,9 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Вы приняли приглашение соединиться</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are already connected to %@ via this link." xml:space="preserve">
|
||||
<source>You are already connected to %@ via this link.</source>
|
||||
<target>Вы уже соединены с %@ через эту ссылку.</target>
|
||||
<trans-unit id="You are already connected to %@." xml:space="preserve">
|
||||
<source>You are already connected to %@.</source>
|
||||
<target>Вы уже соединены с контактом %@.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve">
|
||||
@@ -2577,6 +2657,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Вы можете установить просмотр уведомлений на экране блокировки в настройках.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve">
|
||||
<source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source>
|
||||
<target>Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve">
|
||||
<source>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</source>
|
||||
<target>Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.</target>
|
||||
@@ -2642,6 +2727,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Вы отправили приглашение в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected to group when the group host's device is online, please wait or check later!</source>
|
||||
<target>Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve">
|
||||
<source>You will be connected when your connection request is accepted, please wait or check later!</source>
|
||||
<target>Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!</target>
|
||||
@@ -2657,6 +2747,11 @@ To connect, please ask your contact to create another connection link and check
|
||||
<target>Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve">
|
||||
<source>You will join a group this link refers to and connect to its group members.</source>
|
||||
<target>Вы вступите в группу, на которую ссылается эта ссылка, и соединитесь с её членами.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve">
|
||||
<source>You will stop receiving messages from this group. Chat history will be preserved.</source>
|
||||
<target>Вы перестанете получать сообщения от этой группы. История чата будет сохранена.</target>
|
||||
@@ -2781,11 +2876,21 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>Настройки</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve">
|
||||
<source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source>
|
||||
<target>[Внести свой вклад](https://github.com/simplex-chat/simplex-chat#contribute)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve">
|
||||
<source>[Send us email](mailto:chat@simplex.chat)</source>
|
||||
<target>[Отправить email](mailto:chat@simplex.chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve">
|
||||
<source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source>
|
||||
<target>[Поставить звездочку в GitHub](https://github.com/simplex-chat/simplex-chat)</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="_italic_" xml:space="preserve">
|
||||
<source>\_italic_</source>
|
||||
<target>\_курсив_</target>
|
||||
@@ -2846,6 +2951,31 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>входящий звонок…</target>
|
||||
<note>call status</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed address for you" xml:space="preserve">
|
||||
<source>changed address for you</source>
|
||||
<target>поменял(а) адрес для вас</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed role of %@ to %@" xml:space="preserve">
|
||||
<source>changed role of %1$@ to %2$@</source>
|
||||
<target>поменял(а) роль члена %1$@ на: %2$@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changed your role to %@" xml:space="preserve">
|
||||
<source>changed your role to %@</source>
|
||||
<target>поменял(а) вашу роль на: %@</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address for %@..." xml:space="preserve">
|
||||
<source>changing address for %@...</source>
|
||||
<target>смена адреса для %@...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="changing address..." xml:space="preserve">
|
||||
<source>changing address...</source>
|
||||
<target>смена адреса...</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="colored" xml:space="preserve">
|
||||
<source>colored</source>
|
||||
<target>цвет</target>
|
||||
@@ -2991,6 +3121,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>инкогнито через ссылку-контакт</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via group link" xml:space="preserve">
|
||||
<source>incognito via group link</source>
|
||||
<target>инкогнито через ссылку группы</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="incognito via one-time link" xml:space="preserve">
|
||||
<source>incognito via one-time link</source>
|
||||
<target>инкогнито через одноразовую ссылку</target>
|
||||
@@ -3021,6 +3156,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>приглашение</target>
|
||||
<note>chat list item title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="invited via your group link" xml:space="preserve">
|
||||
<source>invited via your group link</source>
|
||||
<target>приглашен(а) через вашу ссылку группы</target>
|
||||
<note>rcv group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="italic" xml:space="preserve">
|
||||
<source>italic</source>
|
||||
<target>курсив</target>
|
||||
@@ -3041,12 +3181,6 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>член группы</target>
|
||||
<note>member role</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="member %@ role: %@" xml:space="preserve">
|
||||
<source>member %1$@ role: %2$@</source>
|
||||
<target>роль %1$@: %2$@</target>
|
||||
<note>rcv group event chat item
|
||||
snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="member connected" xml:space="preserve">
|
||||
<source>connected</source>
|
||||
<target>соединен(а)</target>
|
||||
@@ -3167,6 +3301,11 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>через ссылку-контакт</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via group link" xml:space="preserve">
|
||||
<source>via group link</source>
|
||||
<target>через ссылку группы</target>
|
||||
<note>chat list item description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="via one-time link" xml:space="preserve">
|
||||
<source>via one-time link</source>
|
||||
<target>через одноразовую ссылку</target>
|
||||
@@ -3202,6 +3341,26 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>вы приглашены в группу</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address" xml:space="preserve">
|
||||
<source>you changed address</source>
|
||||
<target>вы поменяли адрес</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed address for %@" xml:space="preserve">
|
||||
<source>you changed address for %@</source>
|
||||
<target>вы поменяли адрес для %@</target>
|
||||
<note>chat item text</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role for yourself to %@" xml:space="preserve">
|
||||
<source>you changed role for yourself to %@</source>
|
||||
<target>вы поменяли роль себе на: %@</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you changed role of %@ to %@" xml:space="preserve">
|
||||
<source>you changed role of %1$@ to %2$@</source>
|
||||
<target>вы поменяли роль члена %1$@ на: %2$@</target>
|
||||
<note>snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="you left" xml:space="preserve">
|
||||
<source>you left</source>
|
||||
<target>вы покинули группу</target>
|
||||
@@ -3227,12 +3386,6 @@ SimpleX серверы не могут получить доступ к ваше
|
||||
<target>вы: </target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="your role: %@" xml:space="preserve">
|
||||
<source>your role: %@</source>
|
||||
<target>ваша роль: %@</target>
|
||||
<note>rcv group event chat item
|
||||
snd group event chat item</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="~strike~" xml:space="preserve">
|
||||
<source>\~strike~</source>
|
||||
<target>\~зачеркнуть~</target>
|
||||
|
||||
@@ -207,7 +207,7 @@ func chatRecvMsg() async -> ChatResponse? {
|
||||
func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotificationContent)? {
|
||||
logger.debug("NotificationService processReceivedMsg: \(res.responseType)")
|
||||
switch res {
|
||||
case let .contactConnected(contact):
|
||||
case let .contactConnected(contact, _):
|
||||
return (contact.id, createContactConnectedNtf(contact))
|
||||
// case let .contactConnecting(contact):
|
||||
// TODO profile update
|
||||
@@ -220,7 +220,8 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, UNMutableNotification
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= maxImageSize,
|
||||
privacyAcceptImagesGroupDefault.get() {
|
||||
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem
|
||||
let inline = privacyTransferImagesInlineGroupDefault.get()
|
||||
cItem = apiReceiveFile(fileId: file.fileId, inline: inline)?.chatItem ?? cItem
|
||||
}
|
||||
}
|
||||
return cItem.isCall() ? nil : (aChatItem.chatId, createMessageReceivedNtf(cInfo, cItem))
|
||||
@@ -274,8 +275,8 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiReceiveFile(fileId: Int64) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId))
|
||||
func apiReceiveFile(fileId: Int64, inline: Bool) -> AChatItem? {
|
||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, inline: inline))
|
||||
if case let .rcvFileAccepted(chatItem) = r { return chatItem }
|
||||
logger.error("receiveFile error: \(responseError(r))")
|
||||
return nil
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; };
|
||||
5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; };
|
||||
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; };
|
||||
5C58BCD0292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */; };
|
||||
5C58BCD1292AA93700AF9E4F /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCC292AA93700AF9E4F /* libgmp.a */; };
|
||||
5C58BCD2292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */; };
|
||||
5C58BCD3292AA93700AF9E4F /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCE292AA93700AF9E4F /* libffi.a */; };
|
||||
5C58BCD4292AA93700AF9E4F /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C58BCCF292AA93700AF9E4F /* libgmpxx.a */; };
|
||||
5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */; };
|
||||
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
|
||||
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
|
||||
@@ -89,6 +94,7 @@
|
||||
5CC1C99527A6CF7F000D9FF6 /* ShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */; };
|
||||
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; };
|
||||
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; };
|
||||
5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */; };
|
||||
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
|
||||
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
||||
@@ -119,15 +125,11 @@
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
|
||||
6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIGroupEventView.swift */; };
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
|
||||
6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; };
|
||||
6448BBA628EAF728000D2AB9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA128EAF728000D2AB9 /* libffi.a */; };
|
||||
6448BBA728EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */; };
|
||||
6448BBA828EAF728000D2AB9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA328EAF728000D2AB9 /* libgmpxx.a */; };
|
||||
6448BBA928EAF728000D2AB9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA428EAF728000D2AB9 /* libgmp.a */; };
|
||||
6448BBAA28EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */; };
|
||||
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; };
|
||||
6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; };
|
||||
646BB38C283BEEB9001CE359 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */; };
|
||||
646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */; };
|
||||
@@ -235,6 +237,11 @@
|
||||
5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = "<group>"; };
|
||||
5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = "<group>"; };
|
||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = "<group>"; };
|
||||
5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5C58BCCC292AA93700AF9E4F /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a"; sourceTree = "<group>"; };
|
||||
5C58BCCE292AA93700AF9E4F /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5C58BCCF292AA93700AF9E4F /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettings.swift; sourceTree = "<group>"; };
|
||||
5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCallView.swift; sourceTree = "<group>"; };
|
||||
5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.swift; sourceTree = "<group>"; };
|
||||
@@ -288,6 +295,7 @@
|
||||
5CC1C99427A6CF7F000D9FF6 /* ShareSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheet.swift; sourceTree = "<group>"; };
|
||||
5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptRequestsView.swift; sourceTree = "<group>"; };
|
||||
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
|
||||
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
|
||||
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -316,15 +324,11 @@
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIGroupEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupEventView.swift; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
|
||||
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; };
|
||||
6448BBA128EAF728000D2AB9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
6448BBA328EAF728000D2AB9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
6448BBA428EAF728000D2AB9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a"; sourceTree = "<group>"; };
|
||||
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
|
||||
6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = "<group>"; };
|
||||
646BB38B283BEEB9001CE359 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.4.sdk/System/Library/Frameworks/LocalAuthentication.framework; sourceTree = DEVELOPER_DIR; };
|
||||
646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationUtils.swift; sourceTree = "<group>"; };
|
||||
@@ -370,13 +374,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6448BBAA28EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a in Frameworks */,
|
||||
5C58BCD2292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a in Frameworks */,
|
||||
5C58BCD3292AA93700AF9E4F /* libffi.a in Frameworks */,
|
||||
5C58BCD0292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
6448BBA828EAF728000D2AB9 /* libgmpxx.a in Frameworks */,
|
||||
6448BBA728EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a in Frameworks */,
|
||||
6448BBA628EAF728000D2AB9 /* libffi.a in Frameworks */,
|
||||
6448BBA928EAF728000D2AB9 /* libgmp.a in Frameworks */,
|
||||
5C58BCD1292AA93700AF9E4F /* libgmp.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5C58BCD4292AA93700AF9E4F /* libgmpxx.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -431,11 +435,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6448BBA128EAF728000D2AB9 /* libffi.a */,
|
||||
6448BBA428EAF728000D2AB9 /* libgmp.a */,
|
||||
6448BBA328EAF728000D2AB9 /* libgmpxx.a */,
|
||||
6448BBA228EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI-ghc8.10.7.a */,
|
||||
6448BBA528EAF728000D2AB9 /* libHSsimplex-chat-4.0.1-FvHSNBJjHeqKQoixUekUiI.a */,
|
||||
5C58BCCE292AA93700AF9E4F /* libffi.a */,
|
||||
5C58BCCC292AA93700AF9E4F /* libgmp.a */,
|
||||
5C58BCCF292AA93700AF9E4F /* libgmpxx.a */,
|
||||
5C58BCCB292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */,
|
||||
5C58BCCD292AA93700AF9E4F /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -571,6 +575,7 @@
|
||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */,
|
||||
5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */,
|
||||
5CB924E327A8683A00ACCCDD /* UserAddress.swift */,
|
||||
5CCA7DF22905735700C8FEBA /* AcceptRequestsView.swift */,
|
||||
5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
|
||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
||||
640F50E227CF991C001E05C2 /* SMPServers.swift */,
|
||||
@@ -642,7 +647,7 @@
|
||||
5C029EA72837DBB3004A9677 /* CICallItemView.swift */,
|
||||
5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */,
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */,
|
||||
6440C9FF288857A10062C672 /* CIGroupEventView.swift */,
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */,
|
||||
);
|
||||
path = ChatItem;
|
||||
sourceTree = "<group>";
|
||||
@@ -679,6 +684,7 @@
|
||||
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */,
|
||||
647F090D288EA27B00644C40 /* GroupMemberInfoView.swift */,
|
||||
5C9C2DA42894777E00CC63B1 /* GroupProfileView.swift */,
|
||||
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */,
|
||||
);
|
||||
path = Group;
|
||||
sourceTree = "<group>";
|
||||
@@ -889,6 +895,7 @@
|
||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
||||
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
|
||||
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
|
||||
5CCA7DF32905735700C8FEBA /* AcceptRequestsView.swift in Sources */,
|
||||
5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */,
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */,
|
||||
648010AB281ADD15009009B9 /* CIFileView.swift in Sources */,
|
||||
@@ -943,6 +950,7 @@
|
||||
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */,
|
||||
5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */,
|
||||
5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */,
|
||||
6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */,
|
||||
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
|
||||
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */,
|
||||
@@ -958,7 +966,7 @@
|
||||
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
|
||||
5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */,
|
||||
3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */,
|
||||
6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */,
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
|
||||
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
||||
@@ -1203,7 +1211,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1224,7 +1232,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.1;
|
||||
MARKETING_VERSION = 4.2.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1245,7 +1253,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1266,7 +1274,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.1;
|
||||
MARKETING_VERSION = 4.2.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1285,7 +1293,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 9767FTRA3G;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1305,7 +1313,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 9767FTRA3G;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.2;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -1324,7 +1332,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1337,7 +1345,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.1;
|
||||
MARKETING_VERSION = 4.2.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1354,7 +1362,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 83;
|
||||
CURRENT_PROJECT_VERSION = 92;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -1367,7 +1375,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 4.1;
|
||||
MARKETING_VERSION = 4.2.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableUBSanitizer = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
|
||||
@@ -43,6 +43,9 @@ public enum ChatCommand {
|
||||
case apiLeaveGroup(groupId: Int64)
|
||||
case apiListMembers(groupId: Int64)
|
||||
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
|
||||
case apiCreateGroupLink(groupId: Int64)
|
||||
case apiDeleteGroupLink(groupId: Int64)
|
||||
case apiGetGroupLink(groupId: Int64)
|
||||
case getUserSMPServers
|
||||
case setUserSMPServers(smpServers: [String])
|
||||
case apiSetChatItemTTL(seconds: Int64?)
|
||||
@@ -52,6 +55,8 @@ public enum ChatCommand {
|
||||
case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings)
|
||||
case apiContactInfo(contactId: Int64)
|
||||
case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64)
|
||||
case apiSwitchContact(contactId: Int64)
|
||||
case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64)
|
||||
case addContact
|
||||
case connect(connReq: String)
|
||||
case apiDeleteChat(type: ChatType, id: Int64)
|
||||
@@ -63,6 +68,7 @@ public enum ChatCommand {
|
||||
case createMyAddress
|
||||
case deleteMyAddress
|
||||
case showMyAddress
|
||||
case addressAutoAccept(autoAccept: AutoAccept?)
|
||||
case apiAcceptContact(contactReqId: Int64)
|
||||
case apiRejectContact(contactReqId: Int64)
|
||||
// WebRTC calls
|
||||
@@ -75,7 +81,8 @@ public enum ChatCommand {
|
||||
case apiGetCallInvitations
|
||||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||
case receiveFile(fileId: Int64)
|
||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||
case receiveFile(fileId: Int64, inline: Bool)
|
||||
case string(String)
|
||||
|
||||
public var cmdString: String {
|
||||
@@ -114,6 +121,9 @@ public enum ChatCommand {
|
||||
case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
|
||||
case let .apiListMembers(groupId): return "/_members #\(groupId)"
|
||||
case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
|
||||
case let .apiCreateGroupLink(groupId): return "/_create link #\(groupId)"
|
||||
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
|
||||
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
|
||||
case .getUserSMPServers: return "/smp_servers"
|
||||
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
|
||||
case let .apiSetChatItemTTL(seconds): return "/_ttl \(chatItemTTLStr(seconds: seconds))"
|
||||
@@ -123,6 +133,8 @@ public enum ChatCommand {
|
||||
case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))"
|
||||
case let .apiContactInfo(contactId): return "/_info @\(contactId)"
|
||||
case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)"
|
||||
case let .apiSwitchContact(contactId): return "/_switch @\(contactId)"
|
||||
case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)"
|
||||
case .addContact: return "/connect"
|
||||
case let .connect(connReq): return "/connect \(connReq)"
|
||||
case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))"
|
||||
@@ -134,6 +146,7 @@ public enum ChatCommand {
|
||||
case .createMyAddress: return "/address"
|
||||
case .deleteMyAddress: return "/delete_address"
|
||||
case .showMyAddress: return "/show_address"
|
||||
case let .addressAutoAccept(autoAccept): return "/auto_accept \(AutoAccept.cmdString(autoAccept))"
|
||||
case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)"
|
||||
case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)"
|
||||
case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))"
|
||||
@@ -145,7 +158,8 @@ public enum ChatCommand {
|
||||
case .apiGetCallInvitations: return "/_call get"
|
||||
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
|
||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||
case let .receiveFile(fileId): return "/freceive \(fileId)"
|
||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||
case let .receiveFile(fileId, inline): return "/freceive \(fileId) inline=\(onOff(inline))"
|
||||
case let .string(str): return str
|
||||
}
|
||||
}
|
||||
@@ -184,6 +198,9 @@ public enum ChatCommand {
|
||||
case .apiLeaveGroup: return "apiLeaveGroup"
|
||||
case .apiListMembers: return "apiListMembers"
|
||||
case .apiUpdateGroupProfile: return "apiUpdateGroupProfile"
|
||||
case .apiCreateGroupLink: return "apiCreateGroupLink"
|
||||
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
|
||||
case .apiGetGroupLink: return "apiGetGroupLink"
|
||||
case .getUserSMPServers: return "getUserSMPServers"
|
||||
case .setUserSMPServers: return "setUserSMPServers"
|
||||
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
|
||||
@@ -193,6 +210,8 @@ public enum ChatCommand {
|
||||
case .apiSetChatSettings: return "apiSetChatSettings"
|
||||
case .apiContactInfo: return "apiContactInfo"
|
||||
case .apiGroupMemberInfo: return "apiGroupMemberInfo"
|
||||
case .apiSwitchContact: return "apiSwitchContact"
|
||||
case .apiSwitchGroupMember: return "apiSwitchGroupMember"
|
||||
case .addContact: return "addContact"
|
||||
case .connect: return "connect"
|
||||
case .apiDeleteChat: return "apiDeleteChat"
|
||||
@@ -204,6 +223,7 @@ public enum ChatCommand {
|
||||
case .createMyAddress: return "createMyAddress"
|
||||
case .deleteMyAddress: return "deleteMyAddress"
|
||||
case .showMyAddress: return "showMyAddress"
|
||||
case .addressAutoAccept: return "addressAutoAccept"
|
||||
case .apiAcceptContact: return "apiAcceptContact"
|
||||
case .apiRejectContact: return "apiRejectContact"
|
||||
case .apiSendCallInvitation: return "apiSendCallInvitation"
|
||||
@@ -215,6 +235,7 @@ public enum ChatCommand {
|
||||
case .apiGetCallInvitations: return "apiGetCallInvitations"
|
||||
case .apiCallStatus: return "apiCallStatus"
|
||||
case .apiChatRead: return "apiChatRead"
|
||||
case .apiChatUnread: return "apiChatUnread"
|
||||
case .receiveFile: return "receiveFile"
|
||||
case .string: return "console command"
|
||||
}
|
||||
@@ -226,7 +247,7 @@ public enum ChatCommand {
|
||||
}
|
||||
|
||||
func smpServersStr(smpServers: [String]) -> String {
|
||||
smpServers.isEmpty ? "default" : smpServers.joined(separator: ",")
|
||||
smpServers.isEmpty ? "default" : smpServers.joined(separator: ";")
|
||||
}
|
||||
|
||||
func chatItemTTLStr(seconds: Int64?) -> String {
|
||||
@@ -282,10 +303,11 @@ public enum ChatResponse: Decodable, Error {
|
||||
case userProfileUpdated(fromProfile: Profile, toProfile: Profile)
|
||||
case contactAliasUpdated(toContact: Contact)
|
||||
case connectionAliasUpdated(toConnection: PendingContactConnection)
|
||||
case userContactLink(connReqContact: String)
|
||||
case userContactLink(contactLink: UserContactLink)
|
||||
case userContactLinkUpdated(contactLink: UserContactLink)
|
||||
case userContactLinkCreated(connReqContact: String)
|
||||
case userContactLinkDeleted
|
||||
case contactConnected(contact: Contact)
|
||||
case contactConnected(contact: Contact, userCustomProfile: Profile?)
|
||||
case contactConnecting(contact: Contact)
|
||||
case receivedContactRequest(contactRequest: UserContactRequest)
|
||||
case acceptingContactRequest(contact: Contact)
|
||||
@@ -307,7 +329,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
// group events
|
||||
case groupCreated(groupInfo: GroupInfo)
|
||||
case sentGroupInvitation(groupInfo: GroupInfo, contact: Contact, member: GroupMember)
|
||||
case userAcceptedGroupSent(groupInfo: GroupInfo)
|
||||
case userAcceptedGroupSent(groupInfo: GroupInfo, hostContact: Contact?)
|
||||
case userDeletedMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case leftMemberUser(groupInfo: GroupInfo)
|
||||
case groupMembers(group: Group)
|
||||
@@ -327,6 +349,9 @@ public enum ChatResponse: Decodable, Error {
|
||||
case connectedToGroupMember(groupInfo: GroupInfo, member: GroupMember)
|
||||
case groupRemoved(groupInfo: GroupInfo) // unused
|
||||
case groupUpdated(toGroup: GroupInfo)
|
||||
case groupLinkCreated(groupInfo: GroupInfo, connReqContact: String)
|
||||
case groupLink(groupInfo: GroupInfo, connReqContact: String)
|
||||
case groupLinkDeleted(groupInfo: GroupInfo)
|
||||
// receiving file events
|
||||
case rcvFileAccepted(chatItem: AChatItem)
|
||||
case rcvFileAcceptedSndCancelled(rcvFileTransfer: RcvFileTransfer)
|
||||
@@ -380,6 +405,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .contactAliasUpdated: return "contactAliasUpdated"
|
||||
case .connectionAliasUpdated: return "connectionAliasUpdated"
|
||||
case .userContactLink: return "userContactLink"
|
||||
case .userContactLinkUpdated: return "userContactLinkUpdated"
|
||||
case .userContactLinkCreated: return "userContactLinkCreated"
|
||||
case .userContactLinkDeleted: return "userContactLinkDeleted"
|
||||
case .contactConnected: return "contactConnected"
|
||||
@@ -423,6 +449,9 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .connectedToGroupMember: return "connectedToGroupMember"
|
||||
case .groupRemoved: return "groupRemoved"
|
||||
case .groupUpdated: return "groupUpdated"
|
||||
case .groupLinkCreated: return "groupLinkCreated"
|
||||
case .groupLink: return "groupLink"
|
||||
case .groupLinkDeleted: return "groupLinkDeleted"
|
||||
case .rcvFileAccepted: return "rcvFileAccepted"
|
||||
case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled"
|
||||
case .rcvFileStart: return "rcvFileStart"
|
||||
@@ -476,10 +505,11 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .userProfileUpdated(_, toProfile): return String(describing: toProfile)
|
||||
case let .contactAliasUpdated(toContact): return String(describing: toContact)
|
||||
case let .connectionAliasUpdated(toConnection): return String(describing: toConnection)
|
||||
case let .userContactLink(connReq): return connReq
|
||||
case let .userContactLink(contactLink): return contactLink.responseDetails
|
||||
case let .userContactLinkUpdated(contactLink): return contactLink.responseDetails
|
||||
case let .userContactLinkCreated(connReq): return connReq
|
||||
case .userContactLinkDeleted: return noDetails
|
||||
case let .contactConnected(contact): return String(describing: contact)
|
||||
case let .contactConnected(contact, _): return String(describing: contact)
|
||||
case let .contactConnecting(contact): return String(describing: contact)
|
||||
case let .receivedContactRequest(contactRequest): return String(describing: contactRequest)
|
||||
case let .acceptingContactRequest(contact): return String(describing: contact)
|
||||
@@ -500,7 +530,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .contactsList(contacts): return String(describing: contacts)
|
||||
case let .groupCreated(groupInfo): return String(describing: groupInfo)
|
||||
case let .sentGroupInvitation(groupInfo, contact, member): return "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)"
|
||||
case let .userAcceptedGroupSent(groupInfo): return String(describing: groupInfo)
|
||||
case let .userAcceptedGroupSent(groupInfo, hostContact): return "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))"
|
||||
case let .userDeletedMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .leftMemberUser(groupInfo): return String(describing: groupInfo)
|
||||
case let .groupMembers(group): return String(describing: group)
|
||||
@@ -520,6 +550,9 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .connectedToGroupMember(groupInfo, member): return "groupInfo: \(groupInfo)\nmember: \(member)"
|
||||
case let .groupRemoved(groupInfo): return String(describing: groupInfo)
|
||||
case let .groupUpdated(toGroup): return String(describing: toGroup)
|
||||
case let .groupLinkCreated(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
|
||||
case let .groupLink(groupInfo, connReqContact): return "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)"
|
||||
case let .groupLinkDeleted(groupInfo): return String(describing: groupInfo)
|
||||
case let .rcvFileAccepted(chatItem): return String(describing: chatItem)
|
||||
case .rcvFileAcceptedSndCancelled: return noDetails
|
||||
case let .rcvFileStart(chatItem): return String(describing: chatItem)
|
||||
@@ -601,16 +634,16 @@ public struct NetCfg: Codable, Equatable {
|
||||
|
||||
public static let defaults: NetCfg = NetCfg(
|
||||
socksProxy: nil,
|
||||
tcpConnectTimeout: 7_500_000,
|
||||
tcpTimeout: 5_000_000,
|
||||
tcpConnectTimeout: 10_000_000,
|
||||
tcpTimeout: 7_000_000,
|
||||
tcpKeepAlive: KeepAliveOpts.defaults,
|
||||
smpPingInterval: 600_000_000
|
||||
)
|
||||
|
||||
public static let proxyDefaults: NetCfg = NetCfg(
|
||||
socksProxy: nil,
|
||||
tcpConnectTimeout: 15_000_000,
|
||||
tcpTimeout: 10_000_000,
|
||||
tcpConnectTimeout: 20_000_000,
|
||||
tcpTimeout: 15_000_000,
|
||||
tcpKeepAlive: KeepAliveOpts.defaults,
|
||||
smpPingInterval: 600_000_000
|
||||
)
|
||||
@@ -681,6 +714,37 @@ public struct ConnectionStats: Codable {
|
||||
public var sndServers: [String]?
|
||||
}
|
||||
|
||||
public struct UserContactLink: Decodable {
|
||||
public var connReqContact: String
|
||||
public var autoAccept: AutoAccept?
|
||||
|
||||
public init(connReqContact: String, autoAccept: AutoAccept? = nil) {
|
||||
self.connReqContact = connReqContact
|
||||
self.autoAccept = autoAccept
|
||||
}
|
||||
|
||||
var responseDetails: String {
|
||||
"connReqContact: \(connReqContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))"
|
||||
}
|
||||
}
|
||||
|
||||
public struct AutoAccept: Codable {
|
||||
public var acceptIncognito: Bool
|
||||
public var autoReply: MsgContent?
|
||||
|
||||
public init(acceptIncognito: Bool, autoReply: MsgContent? = nil) {
|
||||
self.acceptIncognito = acceptIncognito
|
||||
self.autoReply = autoReply
|
||||
}
|
||||
|
||||
static func cmdString(_ autoAccept: AutoAccept?) -> String {
|
||||
guard let autoAccept = autoAccept else { return "off" }
|
||||
let s = "on" + (autoAccept.acceptIncognito ? " incognito=on" : "")
|
||||
guard let msg = autoAccept.autoReply else { return s }
|
||||
return s + " " + msg.cmdString
|
||||
}
|
||||
}
|
||||
|
||||
public protocol SelectableItem: Hashable, Identifiable {
|
||||
var label: LocalizedStringKey { get }
|
||||
static var values: [Self] { get }
|
||||
@@ -783,7 +847,6 @@ public enum ChatErrorType: Decodable {
|
||||
case invalidConnReq
|
||||
case invalidChatMessage(message: String)
|
||||
case contactNotReady(contact: Contact)
|
||||
case contactGroups(contact: Contact, groupNames: [GroupName])
|
||||
case groupUserRole
|
||||
case groupContactRole(contactName: ContactName)
|
||||
case groupDuplicateMember(contactName: ContactName)
|
||||
@@ -841,6 +904,8 @@ public enum StoreError: Decodable {
|
||||
case quotedChatItemNotFound
|
||||
case chatItemSharedMsgIdNotFound(sharedMsgId: String)
|
||||
case chatItemNotFoundByFileId(fileId: Int64)
|
||||
case duplicateGroupLink(groupInfo: GroupInfo)
|
||||
case groupLinkNotFound(groupInfo: GroupInfo)
|
||||
}
|
||||
|
||||
public enum DatabaseError: Decodable {
|
||||
|
||||
@@ -14,6 +14,7 @@ let GROUP_DEFAULT_DB_CONTAINER = "dbContainer"
|
||||
public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart"
|
||||
let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode"
|
||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout"
|
||||
@@ -92,6 +93,8 @@ public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey:
|
||||
|
||||
public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||
|
||||
public let privacyTransferImagesInlineGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE)
|
||||
|
||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||
|
||||
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
||||
|
||||
@@ -303,13 +303,15 @@ public struct ChatData: Decodable, Identifiable {
|
||||
}
|
||||
|
||||
public struct ChatStats: Decodable {
|
||||
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0) {
|
||||
public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) {
|
||||
self.unreadCount = unreadCount
|
||||
self.minUnreadItemId = minUnreadItemId
|
||||
self.unreadChat = unreadChat
|
||||
}
|
||||
|
||||
public var unreadCount: Int = 0
|
||||
public var minUnreadItemId: Int64 = 0
|
||||
public var unreadChat: Bool = false
|
||||
}
|
||||
|
||||
public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
@@ -335,6 +337,10 @@ public struct Contact: Identifiable, Decodable, NamedChat {
|
||||
activeConn.connLevel > 0 || viaGroup != nil
|
||||
}
|
||||
|
||||
public var viaGroupLink: Bool {
|
||||
activeConn.viaGroupLink
|
||||
}
|
||||
|
||||
public var contactConnIncognito: Bool {
|
||||
activeConn.customUserProfileId != nil
|
||||
}
|
||||
@@ -366,6 +372,7 @@ public struct Connection: Decodable {
|
||||
var connId: Int64
|
||||
var connStatus: ConnStatus
|
||||
public var connLevel: Int
|
||||
public var viaGroupLink: Bool
|
||||
public var customUserProfileId: Int64?
|
||||
|
||||
public var id: ChatId { get { ":\(connId)" } }
|
||||
@@ -373,7 +380,8 @@ public struct Connection: Decodable {
|
||||
static let sampleData = Connection(
|
||||
connId: 1,
|
||||
connStatus: .ready,
|
||||
connLevel: 0
|
||||
connLevel: 0,
|
||||
viaGroupLink: false
|
||||
)
|
||||
}
|
||||
|
||||
@@ -425,6 +433,7 @@ public struct PendingContactConnection: Decodable, NamedChat {
|
||||
var pccAgentConnId: String
|
||||
var pccConnStatus: ConnStatus
|
||||
public var viaContactUri: Bool
|
||||
public var groupLinkId: String?
|
||||
public var customUserProfileId: Int64?
|
||||
public var connReqInv: String?
|
||||
public var localAlias: String
|
||||
@@ -469,10 +478,18 @@ public struct PendingContactConnection: Decodable, NamedChat {
|
||||
desc = NSLocalizedString("you shared one-time link", comment: "chat list item description")
|
||||
}
|
||||
} else if viaContactUri {
|
||||
if incognito {
|
||||
desc = NSLocalizedString("incognito via contact address link", comment: "chat list item description")
|
||||
if groupLinkId != nil {
|
||||
if incognito {
|
||||
desc = NSLocalizedString("incognito via group link", comment: "chat list item description")
|
||||
} else {
|
||||
desc = NSLocalizedString("via group link", comment: "chat list item description")
|
||||
}
|
||||
} else {
|
||||
desc = NSLocalizedString("via contact address link", comment: "chat list item description")
|
||||
if incognito {
|
||||
desc = NSLocalizedString("incognito via contact address link", comment: "chat list item description")
|
||||
} else {
|
||||
desc = NSLocalizedString("via contact address link", comment: "chat list item description")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if incognito {
|
||||
@@ -703,6 +720,11 @@ public struct GroupMember: Identifiable, Decodable {
|
||||
)
|
||||
}
|
||||
|
||||
public struct GroupMemberRef: Decodable {
|
||||
var groupMemberId: Int64
|
||||
var profile: Profile
|
||||
}
|
||||
|
||||
public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable {
|
||||
case member = "member"
|
||||
case admin = "admin"
|
||||
@@ -913,6 +935,7 @@ public struct ChatItem: Identifiable, Decodable {
|
||||
case .memberAdded: return false
|
||||
case .memberLeft: return false
|
||||
case .memberDeleted: return false
|
||||
case .invitedViaGroupLink: return false
|
||||
}
|
||||
case .sndGroupEvent: return true
|
||||
default: return false
|
||||
@@ -1087,6 +1110,8 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole)
|
||||
case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent)
|
||||
case sndGroupEvent(sndGroupEvent: SndGroupEvent)
|
||||
case rcvConnEvent(rcvConnEvent: RcvConnEvent)
|
||||
case sndConnEvent(sndConnEvent: SndConnEvent)
|
||||
|
||||
public var text: String {
|
||||
get {
|
||||
@@ -1102,6 +1127,8 @@ public enum CIContent: Decodable, ItemContent {
|
||||
case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text
|
||||
case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text
|
||||
case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text
|
||||
case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text
|
||||
case let .sndConnEvent(sndConnEvent): return sndConnEvent.text
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1196,7 +1223,7 @@ public enum MsgContent {
|
||||
// TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift
|
||||
case unknown(type: String, text: String)
|
||||
|
||||
var text: String {
|
||||
public var text: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .text(text): return text
|
||||
@@ -1217,13 +1244,6 @@ public enum MsgContent {
|
||||
}
|
||||
}
|
||||
|
||||
public func isFile() -> Bool {
|
||||
switch self {
|
||||
case .file: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case text
|
||||
@@ -1446,6 +1466,7 @@ public enum RcvGroupEvent: Decodable {
|
||||
case userDeleted
|
||||
case groupDeleted
|
||||
case groupUpdated(groupProfile: GroupProfile)
|
||||
case invitedViaGroupLink
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
@@ -1454,14 +1475,15 @@ public enum RcvGroupEvent: Decodable {
|
||||
case .memberConnected: return NSLocalizedString("member connected", comment: "rcv group event chat item")
|
||||
case .memberLeft: return NSLocalizedString("left", comment: "rcv group event chat item")
|
||||
case let .memberRole(_, profile, role):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("member %@ role: %@", comment: "rcv group event chat item"), profile.profileViewName, role.text)
|
||||
return String.localizedStringWithFormat(NSLocalizedString("changed role of %@ to %@", comment: "rcv group event chat item"), profile.profileViewName, role.text)
|
||||
case let .userRole(role):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("your role: %@", comment: "rcv group event chat item"), role.text)
|
||||
return String.localizedStringWithFormat(NSLocalizedString("changed your role to %@", comment: "rcv group event chat item"), role.text)
|
||||
case let .memberDeleted(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("removed %@", comment: "rcv group event chat item"), profile.profileViewName)
|
||||
case .userDeleted: return NSLocalizedString("removed you", comment: "rcv group event chat item")
|
||||
case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item")
|
||||
case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item")
|
||||
case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1476,9 +1498,9 @@ public enum SndGroupEvent: Decodable {
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .memberRole(_, profile, role):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("member %@ role: %@", comment: "snd group event chat item"), profile.profileViewName, role.text)
|
||||
return String.localizedStringWithFormat(NSLocalizedString("you changed role of %@ to %@", comment: "snd group event chat item"), profile.profileViewName, role.text)
|
||||
case let .userRole(role):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("your role: %@", comment: "snd group event chat item"), role.text)
|
||||
return String.localizedStringWithFormat(NSLocalizedString("you changed role for yourself to %@", comment: "snd group event chat item"), role.text)
|
||||
case let .memberDeleted(_, profile):
|
||||
return String.localizedStringWithFormat(NSLocalizedString("you removed %@", comment: "snd group event chat item"), profile.profileViewName)
|
||||
case .userLeft: return NSLocalizedString("you left", comment: "snd group event chat item")
|
||||
@@ -1487,6 +1509,44 @@ public enum SndGroupEvent: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RcvConnEvent: Decodable {
|
||||
case switchQueue(phase: SwitchPhase)
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .switchQueue(phase):
|
||||
if case .completed = phase {
|
||||
return NSLocalizedString("changed address for you", comment: "chat item text")
|
||||
}
|
||||
return NSLocalizedString("changing address...", comment: "chat item text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SndConnEvent: Decodable {
|
||||
case switchQueue(phase: SwitchPhase, member: GroupMemberRef?)
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case let .switchQueue(phase, member):
|
||||
if let name = member?.profile.profileViewName {
|
||||
return phase == .completed
|
||||
? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name)
|
||||
: String.localizedStringWithFormat(NSLocalizedString("changing address for %@...", comment: "chat item text"), name)
|
||||
}
|
||||
return phase == .completed
|
||||
? NSLocalizedString("you changed address", comment: "chat item text")
|
||||
: NSLocalizedString("changing address...", comment: "chat item text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum SwitchPhase: String, Decodable {
|
||||
case started
|
||||
case confirmed
|
||||
case completed
|
||||
}
|
||||
|
||||
public enum ChatItemTTL: Hashable, Identifiable, Comparable {
|
||||
case day
|
||||
case week
|
||||
|
||||
@@ -28,9 +28,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
")" = ")";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Beitragen](https://github.com/simplex-chat/simplex-chat#contribute)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"[Send us email](mailto:chat@simplex.chat)" = "[Senden Sie uns eine E-Mail](mailto:chat@simplex.chat)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Stern auf GitHub](https://github.com/simplex-chat/simplex-chat)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Fügen Sie einen neuen Kontakt hinzu**: Erzeugen Sie einen Einmal-QR-Code oder -Link für Ihren Kontakt.";
|
||||
|
||||
@@ -116,13 +122,13 @@
|
||||
"~strike~" = "\\~durchstreichen~";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"1 day" = "1 Tag";
|
||||
"1 day" = "täglich";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"1 month" = "1 Monat";
|
||||
"1 month" = "monatlich";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"1 week" = "1 Woche";
|
||||
"1 week" = "wöchentlich";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"6" = "6";
|
||||
@@ -161,6 +167,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Accept incognito" = "Inkognito akzeptieren";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Accept requests" = "Anfragen annehmen";
|
||||
|
||||
/* call status */
|
||||
"accepted call" = "Anruf angenommen";
|
||||
|
||||
@@ -170,6 +179,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Advanced network settings" = "Erweiterte Netzwerkeinstellungen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht.";
|
||||
|
||||
@@ -206,6 +218,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Auto-accept images" = "Bilder automatisch akzeptieren";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Automatically" = "Automatisch";
|
||||
|
||||
/* integrity error chat item */
|
||||
"bad message hash" = "Ungültiger Nachrichten-Hash";
|
||||
|
||||
@@ -230,9 +245,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Calls" = "Anrufe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't delete contact!" = "Der Kontakt kann nicht gelöscht werden!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contact!" = "Kontakt kann nicht eingeladen werden!";
|
||||
|
||||
@@ -257,6 +269,30 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Change member role?" = "Die Mitgliederrolle ändern?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address (BETA)" = "Wechseln der Empfängeradresse (BETA)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address?" = "Empfängeradresse wechseln?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change role" = "Rolle ändern";
|
||||
|
||||
/* chat item text */
|
||||
"changed address for you" = "wechselte die Adresse für Sie";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed role of %@ to %@" = "änderte die Rolle von %1$@ auf %2$@";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed your role to %@" = "änderte Ihre Rolle auf %@";
|
||||
|
||||
/* chat item text */
|
||||
"changing address for %@..." = "Wechseln der Adresse für %@ ...";
|
||||
|
||||
/* chat item text */
|
||||
"changing address..." = "Wechseln der Adresse ...";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat archive" = "Datenbank Archiv";
|
||||
|
||||
@@ -278,9 +314,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Chat is stopped" = "Der Chat ist beendet";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat with the developers" = "Chatten Sie mit den Entwicklern";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chats" = "Chats";
|
||||
|
||||
@@ -329,6 +362,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via contact link?" = "Über den Kontakt-Link verbinden?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via group link?" = "Über den Gruppen-Link verbinden?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via link" = "Über einen Link verbinden";
|
||||
|
||||
@@ -395,9 +431,6 @@
|
||||
/* connection information */
|
||||
"connection:%@" = "Verbindung:%@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact %@ cannot be deleted, they are a member of the group(s) %@." = "Der Kontakt mit %@ kann nicht gelöscht werden, da er Mitglied einer oder mehrerer dieser Gruppen ist %@.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact already exists" = "Der Kontakt ist bereits vorhanden";
|
||||
|
||||
@@ -422,6 +455,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Contact name" = "Kontaktname";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact requests" = "Kontaktanfragen";
|
||||
|
||||
/* chat item action */
|
||||
"Copy" = "Kopieren";
|
||||
|
||||
@@ -431,6 +467,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Create address" = "Adresse erstellen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Create link" = "Link erzeugen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Create one-time invitation link" = "Erstellen Sie einen einmaligen Einladungslink";
|
||||
|
||||
@@ -557,6 +596,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Delete invitation" = "Einladung löschen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete link" = "Link löschen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete link?" = "Link löschen?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete message?" = "Die Nachricht löschen?";
|
||||
|
||||
@@ -564,7 +609,7 @@
|
||||
"Delete messages" = "Nachrichten löschen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete messages after" = "Löschen der Nachrichten nach";
|
||||
"Delete messages after" = "Löschen der Nachrichten";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete old database" = "Alte Datenbank löschen";
|
||||
@@ -698,6 +743,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error adding member(s)" = "Fehler beim Hinzufügen von Mitgliedern";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing address" = "Fehler beim Wechseln der Adresse";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing role" = "Fehler beim Ändern der Rolle";
|
||||
|
||||
@@ -710,6 +758,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error creating group" = "Fehler beim Erzeugen der Gruppe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error creating group link" = "Fehler beim Erzeugen des Gruppen-Links";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank";
|
||||
|
||||
@@ -719,6 +770,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting connection" = "Fehler beim Löschen der Verbindung";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting contact" = "Fehler beim Löschen des Kontakts";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting database" = "Fehler beim Löschen der Datenbank";
|
||||
|
||||
@@ -795,14 +849,11 @@
|
||||
"Exporting database archive..." = "Export des Datenbankarchivs...";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"File will be received when your contact is online, please wait or check later!" = "Die Datei wird empfangen, wenn Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!";
|
||||
"File will be received when your contact is online, please wait or check later!" = "Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"File: %@" = "Datei: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Files" = "Dateien";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"For console" = "Für Konsole";
|
||||
|
||||
@@ -836,6 +887,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Group link" = "Gruppen-Link";
|
||||
|
||||
/* notification */
|
||||
"Group message:" = "Grppennachricht:";
|
||||
|
||||
@@ -891,7 +945,7 @@
|
||||
"Ignore" = "Ignorieren";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Image will be received when your contact is online, please wait or check later!" = "Das Bild wird empfangen, sobald Ihr Kontakt online ist, bitte warten oder schauen Sie später nochmal nach!";
|
||||
"Image will be received when your contact is online, please wait or check later!" = "Das Bild wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Immune to spam and abuse" = "Immun gegen Spam und Missbrauch";
|
||||
@@ -918,7 +972,10 @@
|
||||
"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Der Inkognito-Modus schützt die Privatsphäre Ihres Hauptprofilnamens und -bildes – für jeden neuen Kontakt wird ein neues Zufallsprofil erstellt.";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via contact address link" = "Inkognito über einen Kontaktadressen Link";
|
||||
"incognito via contact address link" = "Inkognito über einen Kontaktadressen-Link";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via group link" = "Inkognito über einen Gruppen-Link";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via one-time link" = "Inkognito über einen Einmal-Link";
|
||||
@@ -933,7 +990,7 @@
|
||||
"Incoming video call" = "Eingehender Videoanruf";
|
||||
|
||||
/* connection level description */
|
||||
"indirect (%d)" = "Indirekt (%d)";
|
||||
"indirect (%d)" = "indirekt (%d)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Installieren Sie [SimpleX Chat als Terminalanwendung](https://github.com/simplex-chat/simplex-chat)";
|
||||
@@ -963,11 +1020,14 @@
|
||||
"invited" = "eingeladen";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"invited %@" = "Sie haben %@ eingeladen";
|
||||
"invited %@" = "hat %@ eingeladen.";
|
||||
|
||||
/* chat list item title */
|
||||
"invited to connect" = "Für eine Verbindung eingeladen";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"invited via your group link" = "wurde über Ihren Gruppen-Link eingeladen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Für die sichere Speicherung des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen.";
|
||||
|
||||
@@ -1017,7 +1077,7 @@
|
||||
"Leave group?" = "Die Gruppe verlassen?";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"left" = "verlassen";
|
||||
"left" = "hat die Gruppe verlassen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Light" = "Hell";
|
||||
@@ -1052,12 +1112,8 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Member" = "Mitglied";
|
||||
|
||||
/* rcv group event chat item
|
||||
snd group event chat item */
|
||||
"member %@ role: %@" = "Mitglieder %1$@ Rolle: %2$@";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"member connected" = "verbunden";
|
||||
"member connected" = "ist der Gruppe beigetreten";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.";
|
||||
@@ -1201,7 +1257,7 @@
|
||||
"Open Settings" = "Geräte-Einstellungen öffnen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Open-source protocol and code – anybody can run the servers." = "Open-Source-Protokoll und -Code – Jeder kann seine eigenen Server aufsetzen und nutzen.";
|
||||
"Open-source protocol and code – anybody can run the servers." = "Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"or chat with the developers" = "oder chatten Sie mit den Entwicklern";
|
||||
@@ -1243,7 +1299,7 @@
|
||||
"Please enter correct current passphrase." = "Bitte geben Sie das korrekte, aktuelle Passwort ein.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please enter the previous password after restoring database backup. This action can not be undone." = "Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden.";
|
||||
"Please enter the previous password after restoring database backup. This action can not be undone." = "Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Please restart the app and migrate the database to enable push notifications." = "Bitte führen Sie einen Neustart der App durch und migrieren Sie die Datenbank, um Benachrichtigungen zu aktivieren.";
|
||||
@@ -1269,6 +1325,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Push notifications" = "Push-Benachrichtigungen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Rate the app" = "Bewerte die App";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Read" = "Lesen";
|
||||
|
||||
@@ -1324,10 +1383,10 @@
|
||||
"removed" = "entfernt";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"removed %@" = "Entfernt %@";
|
||||
"removed %@" = "hat %@ aus der Gruppe entfernt";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"removed you" = "hat Sie entfernt";
|
||||
"removed you" = "hat Sie aus der Gruppe entfernt";
|
||||
|
||||
/* chat item action */
|
||||
"Reply" = "Antwort";
|
||||
@@ -1362,6 +1421,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Revert" = "Zurückkehren";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Role" = "Rolle";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Run chat" = "Chat starten";
|
||||
|
||||
@@ -1413,6 +1475,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Send notifications:" = "Benachrichtigungen senden:";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send questions and ideas" = "Senden Sie Fragen und Ideen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sender cancelled file transfer." = "Der Absender hat die Dateiübertragung abgebrochen.";
|
||||
|
||||
@@ -1452,9 +1517,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Share one-time invitation link" = "Einmal-Einladungslink teilen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Shared one-time link" = "Geteilter Einmal-Link";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show preview" = "Vorschau anzeigen";
|
||||
|
||||
@@ -1515,6 +1577,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"strike" = "durchstreichen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Support SimpleX Chat" = "Unterstützen SimpleX Chat";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"System" = "System";
|
||||
|
||||
@@ -1588,17 +1653,20 @@
|
||||
"Theme" = "Design";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "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.";
|
||||
"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "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.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden - Die gesendeten und empfangenen Nachrichten, welche älter als die Ausgewählten sind, werden gelöscht. Dies kann mehrere Minuten dauern.";
|
||||
"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.";
|
||||
"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.";
|
||||
|
||||
/* notification title */
|
||||
"this contact" = "Dieser Kontakt";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." = "Diese Funktion ist experimentell! Sie wird nur funktionieren, wenn der Kontakt ebenfalls die SimpleX-Version 4.2 installiert hat. Sobald der Adress-Wechsel abgeschlossen ist, sollten Sie die Nachricht im Chat-Verlauf sehen. Bitte prüfen Sie, ob Sie weiterhin Nachrichten von diesem Kontakt (oder Gruppenmitglied) empfangen können.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This group no longer exists." = "Diese Gruppe existiert nicht mehr.";
|
||||
|
||||
@@ -1623,6 +1691,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"To support instant push notifications the chat database has to be migrated." = "Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Transfer images faster (BETA)" = "Bilder schneller übertragen (BETA)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@).";
|
||||
|
||||
@@ -1662,6 +1733,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Unmute" = "Stummschaltung aufheben";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unread" = "Ungelesen";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update" = "Aktualisieren";
|
||||
|
||||
@@ -1702,7 +1776,10 @@
|
||||
"v%@ (%@)" = "v%@ (%@)";
|
||||
|
||||
/* chat list item description */
|
||||
"via contact address link" = "über einen Kontaktadressen Link";
|
||||
"via contact address link" = "über einen Kontaktadressen-Link";
|
||||
|
||||
/* chat list item description */
|
||||
"via group link" = "über einen Gruppen-Link";
|
||||
|
||||
/* chat list item description */
|
||||
"via one-time link" = "über einen Einmal-Link";
|
||||
@@ -1737,6 +1814,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome %@!" = "Willkommen %@!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome message" = "Begrüßungsmeldung";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"When available" = "Wenn verfügbar";
|
||||
|
||||
@@ -1756,7 +1836,7 @@
|
||||
"You accepted connection" = "Sie haben die Verbindung akzeptiert";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You are already connected to %@ via this link." = "Sie sind bereits über diesen Link mit %@ verbunden.";
|
||||
"You are already connected to %@." = "Sie sind bereits mit %@ verbunden.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You are connected to the server used to receive messages from this contact." = "Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.";
|
||||
@@ -1777,7 +1857,10 @@
|
||||
"You can set lock screen notification preview via settings." = "Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." = "Sie können Ihre Adresse als Link oder als QR-Code teilen – Jeder kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.";
|
||||
"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." = "Sie können Ihre Adresse als Link oder als QR-Code teilen – Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can start chat via app Settings / Database or by restarting the app" = "Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten.";
|
||||
@@ -1785,6 +1868,18 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:";
|
||||
|
||||
/* chat item text */
|
||||
"you changed address" = "Sie haben die Adresse gewechselt";
|
||||
|
||||
/* chat item text */
|
||||
"you changed address for %@" = "Sie haben die Adresse für %@ gewechselt";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role for yourself to %@" = "Sie haben Ihre eigene Rolle auf %@ geändert";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role of %@ to %@" = "Sie haben die Rolle von %1$@ auf %2$@ geändert";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sie legen fest, über welche Server Sie Ihre Nachrichten **empfangen** und an Ihre Kontakte **senden**.";
|
||||
|
||||
@@ -1816,7 +1911,7 @@
|
||||
"You rejected group invitation" = "Sie haben die Gruppeneinladung abgelehnt";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you removed %@" = "Sie haben %@ entfernt";
|
||||
"you removed %@" = "entfernt %@ aus der Gruppe";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You sent group invitation" = "Sie haben eine Gruppeneinladung gesendet";
|
||||
@@ -1828,14 +1923,20 @@
|
||||
"you shared one-time link incognito" = "Sie haben Inkognito einen Einmal-Link geteilt";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected when your connection request is accepted, please wait or check later!" = "Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird, bitte warten oder schauen Sie später nochmal nach!";
|
||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected when your contact's device is online, please wait or check later!" = "Sie werden verbunden, wenn das Gerät Ihres Kontakts online ist, bitte warten oder schauen Sie später nochmal nach!";
|
||||
"You will be connected when your connection request is accepted, please wait or check later!" = "Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected when your contact's device is online, please wait or check later!" = "Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will join a group this link refers to and connect to its group members." = "Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht und sich mit deren Gruppenmitgliedern verbinden.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will stop receiving messages from this group. Chat history will be preserved." = "Sie werden von dieser Gruppe keine Nachrichten mehr erhalten. Der Chatverlauf wird beibehalten.";
|
||||
|
||||
@@ -1902,10 +2003,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Your random profile" = "Ihr Zufallsprofil";
|
||||
|
||||
/* rcv group event chat item
|
||||
snd group event chat item */
|
||||
"your role: %@" = "Meine Rolle: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your settings" = "Meine Einstellungen";
|
||||
|
||||
|
||||
@@ -28,9 +28,15 @@
|
||||
/* No comment provided by engineer. */
|
||||
")" = ")";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Внести свой вклад](https://github.com/simplex-chat/simplex-chat#contribute)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"[Send us email](mailto:chat@simplex.chat)" = "[Отправить email](mailto:chat@simplex.chat)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Поставить звездочку в GitHub](https://github.com/simplex-chat/simplex-chat)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Добавить новый контакт**: чтобы создать одноразовый QR код или ссылку для вашего контакта.";
|
||||
|
||||
@@ -161,6 +167,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Accept incognito" = "Принять инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Accept requests" = "Принимать запросы";
|
||||
|
||||
/* call status */
|
||||
"accepted call" = " принятый звонок";
|
||||
|
||||
@@ -170,6 +179,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Advanced network settings" = "Настройки сети";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All group members will remain connected." = "Все члены группы, которые соединились через эту ссылку, останутся в группе.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для вас.";
|
||||
|
||||
@@ -206,6 +218,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Auto-accept images" = "Автоприем изображений";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Automatically" = "Автоматически";
|
||||
|
||||
/* integrity error chat item */
|
||||
"bad message hash" = "ошибка хэш сообщения";
|
||||
|
||||
@@ -230,9 +245,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Calls" = "Звонки";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't delete contact!" = "Невозможно удалить контакт!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Can't invite contact!" = "Нельзя пригласить контакт!";
|
||||
|
||||
@@ -257,9 +269,30 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Change member role?" = "Поменять роль члена группы?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address (BETA)" = "Поменять адрес получения (БЕТА)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address?" = "Поменять адрес получения?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change role" = "Поменять роль";
|
||||
|
||||
/* chat item text */
|
||||
"changed address for you" = "поменял(а) адрес для вас";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed role of %@ to %@" = "поменял(а) роль члена %1$@ на: %2$@";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"changed your role to %@" = "поменял(а) вашу роль на: %@";
|
||||
|
||||
/* chat item text */
|
||||
"changing address for %@..." = "смена адреса для %@...";
|
||||
|
||||
/* chat item text */
|
||||
"changing address..." = "смена адреса...";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat archive" = "Архив чата";
|
||||
|
||||
@@ -281,9 +314,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Chat is stopped" = "Чат остановлен";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chat with the developers" = "Соединиться с разработчиками";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Chats" = "Чаты";
|
||||
|
||||
@@ -332,6 +362,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via contact link?" = "Соединиться через ссылку-контакт?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via group link?" = "Соединиться через ссылку группы?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Connect via link" = "Соединиться через ссылку";
|
||||
|
||||
@@ -398,9 +431,6 @@
|
||||
/* connection information */
|
||||
"connection:%@" = "connection:%@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact %@ cannot be deleted, they are a member of the group(s) %@." = "Контакт %@ не может быть удален, так как является членом групп(ы) %@.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact already exists" = "Существующий контакт";
|
||||
|
||||
@@ -425,6 +455,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Contact name" = "Имена контактов";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Contact requests" = "Запросы контактов";
|
||||
|
||||
/* chat item action */
|
||||
"Copy" = "Скопировать";
|
||||
|
||||
@@ -434,6 +467,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Create address" = "Создать адрес";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Create link" = "Создать ссылку";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Create one-time invitation link" = "Создать ссылку-приглашение";
|
||||
|
||||
@@ -560,6 +596,12 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Delete invitation" = "Удалить приглашение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete link" = "Удалить ссылку";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete link?" = "Удалить ссылку?";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Delete message?" = "Удалить сообщение?";
|
||||
|
||||
@@ -701,6 +743,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error adding member(s)" = "Ошибка при добавлении членов группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing address" = "Ошибка при изменении адреса";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error changing role" = "Ошибка при изменении роли";
|
||||
|
||||
@@ -713,6 +758,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error creating group" = "Ошибка при создании группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error creating group link" = "Ошибка при создании ссылки группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting chat database" = "Ошибка при удалении данных чата";
|
||||
|
||||
@@ -722,6 +770,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting connection" = "Ошибка при удалении соединения";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting contact" = "Ошибка при удалении контакта";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Error deleting database" = "Ошибка при удалении данных чата";
|
||||
|
||||
@@ -803,9 +854,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"File: %@" = "Файл: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Files" = "Файлы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"For console" = "Для консоли";
|
||||
|
||||
@@ -839,6 +887,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Group invitation is no longer valid, it was removed by sender." = "Приглашение в группу больше не действительно, оно было удалено отправителем.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Group link" = "Ссылка группы";
|
||||
|
||||
/* notification */
|
||||
"Group message:" = "Групповое сообщение:";
|
||||
|
||||
@@ -923,6 +974,9 @@
|
||||
/* chat list item description */
|
||||
"incognito via contact address link" = "инкогнито через ссылку-контакт";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via group link" = "инкогнито через ссылку группы";
|
||||
|
||||
/* chat list item description */
|
||||
"incognito via one-time link" = "инкогнито через одноразовую ссылку";
|
||||
|
||||
@@ -971,6 +1025,9 @@
|
||||
/* chat list item title */
|
||||
"invited to connect" = "приглашение";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"invited via your group link" = "приглашен(а) через вашу ссылку группы";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain используется для безопасного хранения пароля - это позволяет получать мгновенные уведомления.";
|
||||
|
||||
@@ -1055,10 +1112,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Member" = "Член группы";
|
||||
|
||||
/* rcv group event chat item
|
||||
snd group event chat item */
|
||||
"member %@ role: %@" = "роль %1$@: %2$@";
|
||||
|
||||
/* rcv group event chat item */
|
||||
"member connected" = "соединен(а)";
|
||||
|
||||
@@ -1272,6 +1325,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Push notifications" = "Доставка уведомлений";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Rate the app" = "Оценить приложение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Read" = "Прочитано";
|
||||
|
||||
@@ -1419,6 +1475,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Send notifications:" = "Отправлять уведомления:";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send questions and ideas" = "Отправьте вопросы и идеи";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Sender cancelled file transfer." = "Отправитель отменил передачу файла.";
|
||||
|
||||
@@ -1458,9 +1517,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Share one-time invitation link" = "Поделиться ссылкой-приглашением";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Shared one-time link" = "Одноразовая ссылка-приглашение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Show preview" = "Показывать уведомления";
|
||||
|
||||
@@ -1521,6 +1577,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"strike" = "зачеркнуть";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Support SimpleX Chat" = "Поддержать SimpleX Chat";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"System" = "Системная";
|
||||
|
||||
@@ -1605,6 +1664,9 @@
|
||||
/* notification title */
|
||||
"this contact" = "этот контакт";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." = "Это экспериментальная функция! Она будет работать, только если на другом клиенте установлена версия 4.2. После завершения смены адреса вы увидите сообщение — убедитесь, что вы все еще можете получать сообщения от этого контакта (или члена группы).";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"This group no longer exists." = "Эта группа больше не существует.";
|
||||
|
||||
@@ -1629,6 +1691,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"To support instant push notifications the chat database has to be migrated." = "Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Transfer images faster (BETA)" = "Передавать изображения быстрее (БЕТА)";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Устанавливается соединение с сервером, через который вы получаете сообщения от этого контакта (ошибка: %@).";
|
||||
|
||||
@@ -1668,6 +1733,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Unmute" = "Уведомлять";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Unread" = "Не прочитано";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Update" = "Обновить";
|
||||
|
||||
@@ -1710,6 +1778,9 @@
|
||||
/* chat list item description */
|
||||
"via contact address link" = "через ссылку-контакт";
|
||||
|
||||
/* chat list item description */
|
||||
"via group link" = "через ссылку группы";
|
||||
|
||||
/* chat list item description */
|
||||
"via one-time link" = "через одноразовую ссылку";
|
||||
|
||||
@@ -1743,6 +1814,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome %@!" = "Здравствуйте %@!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Welcome message" = "Приветственное сообщение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"When available" = "Когда возможно";
|
||||
|
||||
@@ -1762,7 +1836,7 @@
|
||||
"You accepted connection" = "Вы приняли приглашение соединиться";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You are already connected to %@ via this link." = "Вы уже соединены с %@ через эту ссылку.";
|
||||
"You are already connected to %@." = "Вы уже соединены с контактом %@.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You are connected to the server used to receive messages from this contact." = "Установлено соединение с сервером, через который вы получаете сообщения от этого контакта.";
|
||||
@@ -1782,6 +1856,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Вы можете поделиться ссылкой или QR кодом - через них можно присоединиться к группе. Вы сможете удалить ссылку, сохранив членов группы, которые через нее соединились.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." = "Вы можете использовать ваш адрес как ссылку или как QR код - кто угодно сможет соединиться с вами. Вы сможете удалить адрес, сохранив контакты, которые через него соединились.";
|
||||
|
||||
@@ -1791,6 +1868,18 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You can use markdown to format messages:" = "Вы можете форматировать сообщения:";
|
||||
|
||||
/* chat item text */
|
||||
"you changed address" = "вы поменяли адрес";
|
||||
|
||||
/* chat item text */
|
||||
"you changed address for %@" = "вы поменяли адрес для %@";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role for yourself to %@" = "вы поменяли роль себе на: %@";
|
||||
|
||||
/* snd group event chat item */
|
||||
"you changed role of %@ to %@" = "вы поменяли роль члена %1$@ на: %2$@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Вы определяете через какие серверы вы **получаете сообщения**, ваши контакты - серверы, которые вы используете для отправки.";
|
||||
|
||||
@@ -1833,6 +1922,9 @@
|
||||
/* chat list item description */
|
||||
"you shared one-time link incognito" = "вы создали ссылку инкогнито";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected to group when the group host's device is online, please wait or check later!" = "Соединение с группой будет установлено, когда хост группы будет онлайн. Пожалуйста, подождите или проверьте позже!";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will be connected when your connection request is accepted, please wait or check later!" = "Соединение будет установлено, когда ваш запрос будет принят. Пожалуйста, подождите или проверьте позже!";
|
||||
|
||||
@@ -1842,6 +1934,9 @@
|
||||
/* No comment provided by engineer. */
|
||||
"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вы будете аутентифицированы при запуске и возобновлении приложения, которое было 30 секунд в фоновом режиме.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will join a group this link refers to and connect to its group members." = "Вы вступите в группу, на которую ссылается эта ссылка, и соединитесь с её членами.";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"You will stop receiving messages from this group. Chat history will be preserved." = "Вы перестанете получать сообщения от этой группы. История чата будет сохранена.";
|
||||
|
||||
@@ -1908,10 +2003,6 @@
|
||||
/* No comment provided by engineer. */
|
||||
"Your random profile" = "Ваш случайный профиль";
|
||||
|
||||
/* rcv group event chat item
|
||||
snd group event chat item */
|
||||
"your role: %@" = "ваша роль: %@";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Your settings" = "Настройки";
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ main = do
|
||||
welcome opts
|
||||
t <- withTerminal pure
|
||||
simplexChatTerminal terminalChatConfig opts t
|
||||
else simplexChatCore terminalChatConfig opts Nothing $ \_ cc -> do
|
||||
else simplexChatCore terminalChatConfig opts Nothing $ \user cc -> do
|
||||
r <- sendChatCmd cc chatCmd
|
||||
putStrLn $ serializeChatResponse r
|
||||
putStrLn $ serializeChatResponse (Just user) r
|
||||
threadDelay $ chatCmdDelay opts * 1000000
|
||||
|
||||
welcome :: ChatOpts -> IO ()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
layout: layouts/article.html
|
||||
title: "Simplex Chat"
|
||||
date: 2020-10-22
|
||||
preview: The prototype of SimpleX Messaging Server implementing SMP protocol.
|
||||
permalink: "/blog/20201022-simplex-chat.html"
|
||||
---
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user